早在 XCode 5,苹果引入了 Assets Catalogs ,它作为一个重要的开发组件,能够让开发者可以更方便的管理项目内的图片资源。
苹果也在不断的完善它的功能:
- XCode 9 中添加了对颜色、矢量图、PDF等的支持(WWDC 2017 Session What's New in Cocoa )
-
XCode 10 中添加了对
High Efficiency Image
和Mojave dark mode的支持(WWDC 2018 Session Optimizing App Assets )
那么相比直接存储在根目录下,究竟 Assets Catalogs 有什么自己独特的优势呢?在 WWDC 2016 上提到的
I/O 优化
是怎么完成的?
imageName:
、
imageWithContentOfFile:
这些方法在不同情况下又有什么表现呢,这篇文章就是基于这种种疑问诞生的。
太长不看版:
Assets Catalogs 将会在编译时生成一个
.car
文件,并在其中包含了这个图像加载所需的一切数据,当图像需要加载的时候,可以直接获取其中的数据并进行加载。
从一次 I/O 优化说起
相信大家现在在项目里面都会使用 Assets Catalogs 对图片资源进行管理,但很不幸,我接手的项目依然是把图片放在 Folder 中,这样看起来似乎并没有什么问题,但是如果打开 Time Profile ,就会发现把图片放在 Folder 中并使用
imageName:
加载图片所用的耗时要比放在 Assets Catalogs 中要
慢得多
。
保存在 Folder ,并使用
imageName:
获取:
展开后的调用栈耗时:
保存在 Assets Cataglogs ,并使用
imageName:
获取:
展开后的调用栈耗时:
而如果使用
imageWithContentOfFile:
,则两种存储方式所用的耗时则相同
使用
imageWithContentOfFile:
获取:
由这几个案例,我们可以推断出:
-
保存在 Folder 中
并不会导致查找时间的增加
,因为在
imageWithContentOfFile:
中两者加载图片的耗时一致 -
使用
imageName:
加载图片时,两种存储方式 都调用 了底层 CoreUI.framework 的框架,但是调用的方法有所不同 -
存储在 Folder 中的图片加载时生成的是
CUIMutableStructuredThemeStore
,而存储在 Assets Catalogs 中则是生成CUIStructuredThemeStore
-
CUIMutableStructuredThemeStore
与CUIStrucetedThemeStore
都调用到一些带有rendition
字眼的类,而CUIMutableStrucetedThemeStore
还多了一层canGetRenditionWithKey:
的方法调用,导致了耗时的增加
从上面这些推断,我们可能会产生以下的一些问题:
- CoreUI.framework 在加载图片中负责了什么工作?
-
CUIMutalbeStructuredThemeStore
与CUIStructuredThemeStore
是什么东西? -
rendition
又是什么东西? - 为什么 Assets Catalogs 能够提高这么多加载速度呢?
-
imageWithContentOfFile:
不对图像进行缓存,是否这个原因导致其加载速度要比imageWithName:
要快呢?
针对这些问题,我们一个一个解决。
探秘 Assets Catalogs 与 .car 文件
在研究这些问题之前,我们先来从新认识一下 Assets Catalogs。
关于 Assets Catalogs ,它
详细的使用方法
相信大家已经很熟悉了,苹果也在
Asset Catalog Format Reference
中给出了
.xcassets
的组成。
但是可能很少人知道在 XCode 编译过程中,保存在 Assets Catalogs 中的图像资源并不是简单的复制到 APP 的 Bundle 中,而是会在编译时生成一个将资源打包并生成索引的
.car
文件,而它在苹果开发者文档上并没有介绍,在网上关于它的信息也是少之又少。
那么
.car
文件究竟是什么?
要知道
.car
文件究竟是什么,有什么作用,我们可以先看看它包含了什么。所以我在 Assets Catalogs 中放入了一组PNG文件:
随后在 XCode 中对项目进行编译,在生成的 APP 包中我们可以找到编译完成的
.car
文件。利用
AssetCatalogTinkerer
我们可以看到在
.car
文件中,包含了各种
图像资源
:@1x的、@2x的、@3x的。而利用 XCode 自带的
assetutil
则能够分析
.car
文件:
sudo xcrun --sdk iphoneos assetutil --info ./Assets.car > ./Assets.json
复制代码
并输出一份
json
文档:
[
{
"AssetStorageVersion" : "IBCocoaTouchImageCatalogTool-10.0",
"Authoring Tool" : "@(#)PROGRAM:CoreThemeDefinition PROJECT:CoreThemeDefinition-346.29\n",
"CoreUIVersion" : 498,
"DumpToolVersion" : 499.1,
"Key Format" : [
"kCRThemeAppearanceName",
"kCRThemeScaleName",
"kCRThemeIdiomName",
"kCRThemeSubtypeName",
"kCRThemeDeploymentTargetName",
"kCRThemeGraphicsClassName",
"kCRThemeMemoryClassName",
"kCRThemeDisplayGamutName",
"kCRThemeDirectionName",
"kCRThemeSizeClassHorizontalName",
"kCRThemeSizeClassVerticalName",
"kCRThemeIdentifierName",
"kCRThemeElementName",
"kCRThemePartName",
"kCRThemeStateName",
"kCRThemeValueName",
"kCRThemeDimension1Name",
"kCRThemeDimension2Name"
],
"MainVersion" : "@(#)PROGRAM:CoreUI PROJECT:CoreUI-498.40.1\n",
"Platform" : "ios",
"PlatformVersion" : "12.0",
"SchemaVersion" : 2,
"StorageVersion" : 15
},
{
"AssetType" : "Image",
"BitsPerComponent" : 8,
"ColorModel" : "RGB",
"Colorspace" : "srgb",
"Compression" : "palette-img",
"Encoding" : "ARGB",
"Idiom" : "universal",
"Image Type" : "kCoreThemeOnePartScale",
"Name" : "MyPNG",
"Opaque" : false,
"PixelHeight" : 28,
"PixelWidth" : 28,
"RenditionName" : "My.png",
"Scale" : 1,
"SizeOnDisk" : 1007,
"Template Mode" : "automatic"
},
{
"AssetType" : "Image",
"BitsPerComponent" : 8,
"ColorModel" : "RGB",
"Colorspace" : "srgb",
"Compression" : "palette-img",
"Encoding" : "ARGB",
"Idiom" : "universal",
"Image Type" : "kCoreThemeOnePartScale",
"Name" : "MyPNG",
"Opaque" : false,
"PixelHeight" : 56,
"PixelWidth" : 56,
"RenditionName" : "My@2x.png",
"Scale" : 2,
"SizeOnDisk" : 1102,
"Template Mode" : "automatic"
},
{
"AssetType" : "Image",
"BitsPerComponent" : 8,
"ColorModel" : "RGB",
"Colorspace" : "srgb",
"Compression" : "palette-img",
"Encoding" : "ARGB",
"Idiom" : "universal",
"Image Type" : "kCoreThemeOnePartScale",
"Name" : "MyPNG",
"Opaque" : false,
"PixelHeight" : 84,
"PixelWidth" : 84,
"RenditionName" : "My@3x.png",
"Scale" : 3,
"SizeOnDisk" : 1961,
"Template Mode" : "automatic"
}
]
复制代码
在这份
.json
文档中揭示了一些有趣的信息,可以看到每一个不同分辨率的图像都会在
.car
文件中去记录它们的一些数据,同时还又一个叫
keyFormatter
的东西,还有很多东西我们暂时不知道它们是什么意思,所以我们继续探究。
反编译 CoreUI.framework
既然知道了整个图片的加载过程是与 CoreUI.framework 密不可分,那么想要探究这些问题最好的方法,就是直接去看这些方法做了什么事情。
所以我们利用 Hopper Disassemble 对 CoreUI.framework 进行反编译,看一下图片加载的过程中究竟发生了什么事情。
CoreUI.framework 位于
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/Library/CoreSimulator/Profiles/Runtimes/iOS.simruntime/Contents/Resources/RuntimeRoot/System/Library/PrivateFrameworks/CoreUI.framework/CoreUI
Hopper 解析完成后会显示这样一个界面:
随后选择右上角的这一个按钮,就可以看到反编译出来的代码了:
在 Github 上也有其他人反编译的 CoreUI.framework 的头文件,我 fork 了一份,不方便的同学可以先看一下头文件。
Folder 中加载图片的过程
1. 基础判断
首先关注的是保存在 Folder 中,并使用
imageName:
方法加载的例子,根据 Time Profile 中的调用栈,我们找到
[CUICatalog _resolvedRenditionKeyForName: scaleFactor: deviceIdiom: deviceSubtype: displayGamut: layoutDirection: sizeClassHorizontal: sizeClassVertical: memoryClass: graphicsClass: appearanceIdentifier: graphicsFallBackOrder: deviceSubtypeFallBackOrder:]
复制代码
而在方法内部我们很容易关注到它对设备的型号做了一次判断,也对加载的图片的
name
进行了一次检查,随后获取了对应
name
的
baseKey
,然后调用下一层的方法
而
baseKey
则是去取
renditionKey
,它首先会获取一个叫
themeStore
的东西,在调用栈中我们可以知道,如果图片存放在 Folder 中,则会生成
CUIMutableStructuredThemeStore
,随后它会根据图片的名字,获取
CUIRenditionKey
对象。
而且从这里我们可以猜测到应该每一个
rendition
都有与之对应的
renditionKey
,在一张图片资源里,它们可能是
一对一
的形式,即一个
rendition
对应一个
renditionKey
。
2. 图片加载前的最后准备工作
而在下一层的
[CUICatalog _resolvedRenditionKeyFromThemeRef: withBaseKey: scaleFactor: deviceIdiom: deviceSubtype: displayGamut: layoutDirection: sizeClassHorizontal: sizeClassVertical: memoryClass: graphicsClass: graphicsFallBackOrder: deviceSubtypeFallBackOrder: iconSizeIndex: appearanceIdentifier:]
复制代码
这一个方法是负责完成加载图片前最后的准备工作,包括对应图像的 分辨率、放大倍数、方向、水平尺寸、垂直尺寸等参数的设置
同时在此方法内,我们会注意到有很多地方调用
canGetRenditionWithKey:
这个方法
而在开始调用
canGetRenditionWithKey:
之前,会调用
renditionInfoForIdentifier:
去获取
rendition
,如果能够成功获取,则不会再进入到多次调用
canGetRenditionWithKey:
的流程中,这一点十分
重要
,因为只有在 Folder 中加载图片才不能在这步成功获取
rendition
,所以可以假设
rendition
是 Assets Catalogs 中
附带的一些属性
,在 Assets Catalogs 中能够直接获取,而在 Folder 中则是需要重复调用
canGetRenditionWithKey:
来手动获取。
3. canGetRendition 的判断
在
canGetRenditionWithKey:
方法内部可以看到它本质上是调用了
renditionWithKey:
的方法,再判断该方法返回值是否为空:
而在
renditionWithKey:
方法内,它主要做了
两件事
:
-
根据上一层传入的
[CUIRenditionKey keyList]
获取keySignature
-
根据
[CUIRenditionKey keyList]
与keySignature
获取rendition
先看一下这个
keyList
:
它其实是获取 自身的的属性 ,是一个 getter 方法,拿到的值其实不是一个 List ,而是一个 结构体 :
里面包含了
identifier
与
value
。
所以利用这个
keyList
,
CUIMutableStructuredThemeStore
获取到了
keySignature
,并根据它获取到了对应的
rendition
:
可以看到这个方法被加了一个线程同步锁
objc_sync_enter
,以确保它是
线程安全
的,所以它的耗时会高很多。另一方面,在获取
keySignature
的时候,还执行了一个叫做
__CUICopySortedKeySignature
的方法,这个方法是对
keySignature
进行各种位操作,也是会导致耗时的增加。
4. 小结
从上面的分析可以看出,在 Folder 中加载导致耗时增加的原因如下:
加载图片过程中由于没有办法直接获取
rendition
,所以需要调用canGetRenditionWithKey:
方法进行判断,而该方法会调用两个比较耗时的操作,一个是对keySignature
的 copy 操作,另一个是在添加了线程锁并从CUIMutableStructuredThemeStore
的字典中取出rendition
的操作,这两个操作是导致耗时增加的元凶。
所以
CUIMutableStructuredThemeStore
在 CoreUI.framework 中起到了一个类似 imageSet 的作用,其中包括了一个
可变字典
,能够存放
rendition
,所以
rendition
就是我们需要加载的图片,而
renditionKey
则是这个图像资源的一种标识,能够通过
renditionKey
获取到对应的
rendition
,同时
renditionKey
中包含了各种
attribute
,是代表该图片的分辨率、垂直大小、水平大小等参数,这些参数这也和我们之前解析的
.json
文件的数据也能一一对应:
{
"AssetType" : "Image",
"BitsPerComponent" : 8,
"ColorModel" : "RGB",
"Colorspace" : "srgb",
"Compression" : "palette-img",
"Encoding" : "ARGB",
"Idiom" : "universal",
"Image Type" : "kCoreThemeOnePartScale",
"Name" : "MyPNG",
"Opaque" : false,
"PixelHeight" : 28,
"PixelWidth" : 28,
"RenditionName" : "My.png",
"Scale" : 1,
"SizeOnDisk" : 1007,
"Template Mode" : "automatic"
},
复制代码
所以在 Folder 中加载图片将会生成
CUIMutableStructuredThemeStore
,把图片转成
rendition
并保存到其可变数组中,并根据图片名称生成
renditionKey
,随后根据
CUINamedImageDescription
这个类,获取图片的相关信息,并填充到
renditionKey
中,在需要加载图片的时候,先根据
renditionKey
获取对应的图片资源,然后再从
renditionKey
中读取各种
attribute
信息,并交由 Image I/O 框架对图片进行渲染工作。