一. 引言
我们做启动优化和卡顿优化的时候,发现图片通过不同的加载方式,加载时长相差巨大,尤其在低端机型(iPhone6、iPhone7、iPhone8系列)上,不同方式的图片加载,加载时间可以相差10几倍-几十倍不等,相差至少一个数量级。
试想一下,如果你做某个需求,添加了一些图片的加载,而这个图片加载耗时比较大,直接影响了启动时长和卡顿率,导致稳定性数据有大的下降,到时领导对你的印象和后期绩效评估,估计都会有影响。
因此充分了解图片加载的不同方式之间优缺点,加载的过程是非常有必要,有利于我们针对不同场景采用不同加载方式来提升项目稳定性。
我们都知道图片加载主要有三种方式:
- 放在
Assets.xcassets里面管理,然后通过imageNamed方法进行加载 - 放在
bundle里面通过imageNamed方法进行加载 - 放在
bundle里面通过imageWithContentsOfFile方式进行加载
这里我们对三种图片的加载速度进行一个对比,并分析下原因:
二. 实验
这里分别选了两组图片:
-
一组是图片都是大图,大小在
1.5M-4M之间,分辨率都是在1920*1080及以上。
image.png 另一组是小图,大小在
1k-4k之间,分辨率在60*60左右。
然后分别将两组图片,放在Assets.xcassets和main bundle里面。

放在main bundle的所有图片,加上前缀local

然后在FJFImageLocalLoadVC里面创建三个按钮分别是:
-
放在Assets.xcassets管理通过imageNamed来加载图片的Asset imageName按钮 - 放在
bundle管理通过imageNamed来加载图片的文件夹 imageName按钮 - 放在
bundle管理通过imageWithContentsOfFile来加载图片的文件夹 contentOfFile按钮
每次启动,点击一个按钮,循环去加载对应的图片,并输出每张图片的加载耗时:
1. 大图实验数据
A. 用Assets.xcassets管理通过 imageNamed加载
2022-09-03 16:18:11.412898+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:7.09605 ms
2022-09-03 16:18:11.414388+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.95403 ms
2022-09-03 16:18:11.415612+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.86498 ms
2022-09-03 16:18:11.416765+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.79906 ms
2022-09-03 16:18:11.417784+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.71502 ms
2022-09-03 16:18:11.418706+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.64099 ms
2022-09-03 16:18:11.419683+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.72193 ms
2022-09-03 16:18:11.420537+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.59795 ms
2022-09-03 16:18:11.421399+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.60701 ms
2022-09-03 16:18:11.422386+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.73600 ms
2022-09-03 16:18:11.422987+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.30601 ms
2022-09-03 16:18:11.424036+0800 FJFBlogProjectDemo[15202:794425] --------Asset catalog加载图片 函数耗时:0.81706 ms
B. 用bundle管理通过 imageNamed加载
2022-09-03 16:19:55.879618+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:141.64495 ms
2022-09-03 16:19:55.886850+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:6.81698 ms
2022-09-03 16:19:55.892923+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:5.73993 ms
2022-09-03 16:19:55.897756+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:4.54497 ms
2022-09-03 16:19:55.900638+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:2.71297 ms
2022-09-03 16:19:55.902903+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:2.18391 ms
2022-09-03 16:19:55.905131+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:2.14791 ms
2022-09-03 16:19:55.907324+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:2.11704 ms
2022-09-03 16:19:55.909492+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:2.09796 ms
2022-09-03 16:19:55.911705+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:2.13397 ms
2022-09-03 16:19:55.913859+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:2.08092 ms
2022-09-03 16:19:55.916220+0800 FJFBlogProjectDemo[15217:795488] --------文件 imageName加载图片 函数耗时:2.28393 ms
C. 用bundle管理通过 imageWithContentsOfFile加载
2022-09-03 21:39:53.313360+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:122.10906 ms
2022-09-03 21:39:53.317499+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.84998 ms
2022-09-03 21:39:53.321495+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.74699 ms
2022-09-03 21:39:53.325367+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.62003 ms
2022-09-03 21:39:53.329069+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.44801 ms
2022-09-03 21:39:53.332854+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.51596 ms
2022-09-03 21:39:53.336646+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.54803 ms
2022-09-03 21:39:53.340615+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.70693 ms
2022-09-03 21:39:53.344022+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.15106 ms
2022-09-03 21:39:53.346476+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:2.30098 ms
2022-09-03 21:39:53.348315+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:1.73199 ms
2022-09-03 21:39:53.349933+0800 FJFBlogProjectDemo[18445:937441] --------文件 imageWithContentsOfFile加载图片 函数耗时:1.54901 ms
2. 小图实验数据
A. 用Assets.xcassets管理通过 imageNamed加载
2022-09-03 16:25:07.374076+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:4.29296 ms
2022-09-03 16:25:07.375350+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:0.88501 ms
2022-09-03 16:25:07.375836+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:0.23699 ms
2022-09-03 16:25:07.376785+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:0.75495 ms
2022-09-03 16:25:07.377780+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:0.76401 ms
2022-09-03 16:25:07.378743+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:0.72801 ms
2022-09-03 16:25:07.379187+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:0.20504 ms
2022-09-03 16:25:07.379620+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:0.25606 ms
2022-09-03 16:25:07.380062+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:0.27001 ms
2022-09-03 16:25:07.380984+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:0.73600 ms
2022-09-03 16:25:07.383440+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:2.22194 ms
2022-09-03 16:25:07.384820+0800 FJFBlogProjectDemo[15271:798689] --------Asset catalog加载图片 函数耗时:1.15597 ms
B. 用bundle管理通过 imageNamed加载
2022-09-03 16:27:19.256526+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:189.96501 ms
2022-09-03 16:27:19.266366+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:9.21297 ms
2022-09-03 16:27:19.273456+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:6.66499 ms
2022-09-03 16:27:19.278714+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:4.87900 ms
2022-09-03 16:27:19.283533+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:4.44400 ms
2022-09-03 16:27:19.287708+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:3.83902 ms
2022-09-03 16:27:19.290927+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:2.94399 ms
2022-09-03 16:27:19.292683+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:1.63591 ms
2022-09-03 16:27:19.294186+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:1.41799 ms
2022-09-03 16:27:19.295716+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:1.43397 ms
2022-09-03 16:27:19.296240+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:0.44703 ms
2022-09-03 16:27:19.296694+0800 FJFBlogProjectDemo[15289:800441] --------文件 imageName加载图片 函数耗时:0.39005 ms
C. 用bundle管理通过 imageWithContentsOfFile加载
2022-09-03 16:27:42.501943+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:160.00700 ms
2022-09-03 16:27:42.507223+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:4.72999 ms
2022-09-03 16:27:42.511637+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.99196 ms
2022-09-03 16:27:42.515078+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:3.05498 ms
2022-09-03 16:27:42.517940+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:2.53499 ms
2022-09-03 16:27:42.520895+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:2.61807 ms
2022-09-03 16:27:42.524116+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:2.92397 ms
2022-09-03 16:27:42.525691+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:1.39809 ms
2022-09-03 16:27:42.526938+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:1.10292 ms
2022-09-03 16:27:42.528083+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:1.03700 ms
2022-09-03 16:27:42.528171+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:0.01800 ms
2022-09-03 16:27:42.528236+0800 FJFBlogProjectDemo[15295:801035] --------文件 imageWithContentsOfFile加载图片 函数耗时:0.01597 ms
从以上的实验数据,我们取十次数据的平均值来看,我们可以得出以下几个结果:
相同图片,用
bundle管理通过imageNamed加载,耗时最多,无论是首次加载还是非首次。其次是用
bundle管理通过imageWithContentsOfFile加载耗时第二,首次加载耗时跟用bundle管理通过imageNamed加载相差不大,非首次加载耗时为用bundle管理通过imageNamed加载的耗时一半左右。用
Assets.xcassets管理通过imageNamed加载耗时最小,跟其他两者相比,都有至少1个量级的差距,无论是首次加载还是非首次加载。
因此从这个结果,可以得出如下几个问题:
- 为什么
Assets.xcassets管理通过imageNamed加载耗时最少,用bundle管理通过imageWithContentsOfFile加载耗时第二,用bundle管理通过imageNamed加载,耗时最多。 - 为什么放在
bundle无论是通过imageNamed还是imageWithContentsOfFile来加载,为什么首次加载耗时都如此大。
三. 分析
带着上面的两个疑问,我们通过抓捕堆栈和阅读源码等来分析,为什么三种加载方式会产生差异的原因。
3. 用Assets.xcassets管理通过 imageNamed加载图片
以下是通过Instruments的Time Profiler获取到的用Assets.xcassets管理通过 imageNamed加载图片堆栈。
用Assets.xcassets管理通过 imageNamed首次加载图片:

用Assets.xcassets管理通过 imageNamed非首次加载图片:

从堆栈我们可以看出imageNamed加载方法实际上调用的是一个叫做UIAssetManager的类,每个Bundle会有一个UIAssetManager,它有一个strong-strong的NSMapTable的属性,用来做缓存,这个可以参考SDImageAssetManager。
因此这里可以大概推断下用Assets.xcassets管理通过 imageNamed加载图片过程:

备注:
rendition是 CoreUI.framework 对某一图像资源的不同样式的统称,如@1x,@2x,每一个rendition有一个renditionKey与之对应,renditionKey包含了不同的attribute,用于记录图片资源的参数.
CUIMutalbeStructuredThemeStore与CUIStructuredThemeStore,可以理解为可变和不可变的图像集,包含了不同的图像资源。
从上面的分析我们也可以知道,用Assets.xcassets管理通过 imageNamed加载图片,首次加载图片之所以耗时比较多的原因:
- 首次加载,需要去打开并加载
Assets.car文件,涉及到I/O操作 - 需要
UIAssetManager的初始化操作
那为什么通过Assets.car加载图片资源,会比直接加载从bundle上加载耗时少至少一个量级呢?
这就需要了解下.car文件:
.xcassets里面的所有资源在编译过程结束后会编译为.car文件,而.car文件实际上是一种特殊的bom文件,而bom(Bill of Materials)是从NeXTSTEP继承下来的一种文件格式:
在iOS和macOS操作系统,会通过私有库CoreUI.framework来解析car文件,中间会调用Bom.framework的接口来解析BOM.
我们可以通过在命令行输入: assetutil -I Assets.car, 查看Assets.car文件内部结构
assetutil -I Assets.car
[
{
"Appearances" : {
"UIAppearanceAny" : 0
},
"AssetStorageVersion" : "Xcode 13.4 (13F17a) via AssetCatalogSimulatorAgent",
"Authoring Tool" : "@(#)PROGRAM:CoreThemeDefinition PROJECT:CoreThemeDefinition-520\n",
"CoreUIVersion" : 738,
"DumpToolVersion" : 738.1,
"Key Format" : [
"kCRThemeAppearanceName",
"kCRThemeLocalizationName",
"kCRThemeScaleName",
"kCRThemeIdiomName",
"kCRThemeSubtypeName",
"kCRThemeGlyphWeightName",
"kCRThemeGlyphSizeName",
"kCRThemeDimension2Name",
"kCRThemeDimension1Name",
"kCRThemeDeploymentTargetName",
"kCRThemeDisplayGamutName",
"kCRThemeDirectionName",
"kCRThemeSizeClassHorizontalName",
"kCRThemeSizeClassVerticalName",
"kCRThemeGraphicsClassName",
"kCRThemeMemoryClassName",
"kCRThemeIdentifierName",
"kCRThemeElementName",
"kCRThemePartName",
"kCRThemeStateName",
"kCRThemeValueName"
],
"MainVersion" : "@(#)PROGRAM:CoreUI PROJECT:CoreUI-738.1\n",
"Platform" : "ios",
"PlatformVersion" : "11.0",
"SchemaVersion" : 2,
"StorageVersion" : 17,
"ThinningParameters" : "optimized <idiom 1> <subtype 1792> <scale 2> <gamut 1> <graphics 6> <graphicsfallback (5,4,3,2,1,0)> <memory 4> <deployment 7> <hostedIdioms (4)>",
"Timestamp" : 1662280005
},
{
"AssetType" : "Image",
"BitsPerComponent" : 8,
"ColorModel" : "RGB",
"Colorspace" : "srgb",
"Compression" : "deepmap2",
"DeploymentTarget" : "2019",
"Encoding" : "ARGB",
"Idiom" : "universal",
"Name" : "little_icon_1",
"NameIdentifier" : 1592,
"Opaque" : false,
"PixelHeight" : 60,
"PixelWidth" : 60,
"RenditionName" : "little_icon_1.png",
"Scale" : 1,
"SHA1Digest" : "C161BEAD4ABCF455C8DFA2EF7901CF40EF40F2A5",
"SizeOnDisk" : 334,
"State" : "Normal",
"Template Mode" : "automatic",
"Value" : "Off"
},
从解析出来的json我们可以看出,这个json里面存储图片的所有基本信息,比如名称,宽高、大小,倍数,编码类型,压缩类型,颜色空间等。
我们知道加载图片,不仅要加载图片的二进制数据,还要加载图片相关的基本信息,这里通过Asset.car来加载,可以在将Asset.car解析之后,直接获取到图片的基本信息和加载图片的二进制。
A. 用Assets.xcassets管理通过 imageNamed首次加载为什么耗时相对较大原因
- 首次加载,需要去打开并加载
Assets.car文件,涉及到I/O操作 - 需要
UIAssetManager的初始化操作
B. 用Assets.xcassets管理通过 imageNamed加载为什么耗时相对最小
-
Assets.xcassets编译形成的.car文件里面的资源信息是提前编译好的,当加载完.car文件解析之后,可以直接通过图片名称从.car文件解析后文件去定位读取图片属性和图片资源,没有多余操作。
4. 用bundle管理通过 imageNamed加载
以下是通过Instruments的Time Profiler获取到的用bundle管理通过 imageNamed加载图片的堆栈。
用bundle管理通过 imageNamed首次加载图片的堆栈:


用bundle管理通过 imageNamed非首次加载图片的堆栈:

通过这个调用堆栈,我们可以看出用bundle管理通过 imageNamed加载图片的过程如下:

从这个加载过程,我们可以分析出如下结论:
A. 用bundle管理通过 imageNamed加载首次加载为什么耗时这么大
- 首次加载,需要去打开并加载
Assets.car文件,涉及到I/O操作 - 需要
UIAssetManager的初始化操作 - 需要去初始化读取图片文件的相关环境,比如创建Reader_AVCI,加载
CMPhotoSymbols等读。
B. 为什么bundle管理通过 imageNamed加载图片耗时比用Assets.xcassets管理通过 imageNamed加载图片大(非首次)
-
Assets.xcassets编译形成的.car文件里面的资源信息是提前编译好的,可以直接定位去读取图片属性和图片资源,没有多余操作。 -
bundle的图片加载,需要先去Assets.car里面查询,由于图片资源并不再Assets.car里面,所以在获取rendition和renditionKey时多次调用canGetRenditionWithKey,canGetRenditionWithKey函数里面的通过renditionKey生成keySignature是一个copy操作和位操作,比较耗时,而且从从CUIMutableStructuredThemeStore的字典中取出rendition的操作添加了线程锁,导致耗时也相对高,由于图片资源并不再Assets.car,因此再重新通过mmap加载读取图片资源文件,形成rendition和renditionKey,总体耗时就比Assets.xcassets管理图片资源加载耗时高很多。
5. 用bundle管理通过 imageWithContentsOfFile加载
以下是通过Instruments的Time Profiler获取到的用bundle管理通过 imageNamed加载图片堆栈。
用bundle管理通过 imageWithContentsOfFile首次加载图片的堆栈:

用bundle管理通过 imageWithContentsOfFile非首次加载图片的堆栈:

通过这个调用堆栈,我们可以看出用bundle管理通过 imageWithContentsOfFile加载图片的过程如下:

从这个加载过程,我们可以分析出如下结论:
A. 用bundle管理通过 imageWithContentsOfFile加载首次加载为什么耗时这么大
- 首次加载,需要去初始化读取图片文件的相关环境,比如创建
Reader_AVCI,加载CMPhotoSymbols等操作。
B. 为什么bundle管理通过 imageWithContentsOfFile加载图片耗时比用bundle管理通过 imageWithContentsOfFile加载图片小(非首次)
-
bundle管理通过imageWithContentsOfFile加载图片不需要去Assets.car里面查询,没有生成rendition和renditionKey的相关操作和缓存操作,只有读取图片属性和图片资源的操作耗时。
四. 总结
1. 为什么Assets.xcassets管理通过 imageNamed加载耗时最少,用bundle管理通过 imageWithContentsOfFile加载耗时第二,用bundle管理通过 imageNamed加载,耗时最多。
Assets.xcassets编译形成的.car文件里面的资源信息是提前编译好的,当加载完.car文件解析之后,可以直接通过图片名称从.car文件解析后文件去获取图片属性和读取图片资源,没有多余操作。bundle管理通过imageWithContentsOfFile加载图片不需要去Assets.car里面查询,没有生成rendition和renditionKey的相关操作和缓存操作,只有读取图片属性和图片二进制的操作耗时bundle管理通过 通过imageName的图片加载,需要先去Assets.car里面查询,由于图片资源并不再Assets.car里面,所以在获取rendition和renditionKey时多次调用canGetRenditionWithKey,canGetRenditionWithKey函数里面的通过renditionKey生成keySignature是一个copy操作和位操作,比较耗时,而且从从CUIMutableStructuredThemeStore的字典中取出rendition的操作添加了线程锁,导致耗时也相对高,由于图片资源并不再Assets.car,因此再重新通过mmap加载读取图片属性和图片资源,形成rendition和renditionKey,总体耗时最大。
2. 为什么放在bundle无论是通过imageNamed还是imageWithContentsOfFile来加载,为什么首次加载耗时都如此大。
- 首次加载,需要去初始化读取图片文件的相关环境,比如创建
Reader_AVCI,加载CMPhotoSymbols等操作。
