早在 XCode 5,苹果引入了 Assets Catalogs ,它做为一个重要的开发组件,可以让开发者能够更方便的管理项目内的图片资源。html
苹果也在不断的完善它的功能:ios
High Efficiency Image
和Mojave dark mode的支持(WWDC 2018 Session Optimizing App Assets )那么相比直接存储在根目录下,究竟 Assets Catalogs 有什么本身独特的优点呢?在 WWDC 2016 上提到的 I/O 优化是怎么完成的?imageName:
、imageWithContentOfFile:
这些方法在不一样状况下又有什么表现呢,这篇文章就是基于这种种疑问诞生的。git
太长不看版:github
Assets Catalogs 将会在编译时生成一个
.car
文件,并在其中包含了这个图像加载所需的一切数据,当图像须要加载的时候,能够直接获取其中的数据并进行加载。json
相信你们如今在项目里面都会使用 Assets Catalogs 对图片资源进行管理,但很不幸,我接手的项目依然是把图片放在 Folder 中,这样看起来彷佛并无什么问题,可是若是打开 Time Profile ,就会发现把图片放在 Folder 中并使用imageName:
加载图片所用的耗时要比放在 Assets Catalogs 中要慢得多。数组
保存在 Folder ,并使用imageName:
获取:xcode
展开后的调用栈耗时:缓存
保存在 Assets Cataglogs ,并使用imageName:
获取:sass
展开后的调用栈耗时:安全
而若是使用imageWithContentOfFile:
,则两种存储方式所用的耗时则相同
使用imageWithContentOfFile:
获取:
由这几个案例,咱们能够推断出:
imageWithContentOfFile:
中二者加载图片的耗时一致imageName:
加载图片时,两种存储方式都调用了底层 CoreUI.framework 的框架,可是调用的方法有所不一样CUIMutableStructuredThemeStore
,而存储在 Assets Catalogs 中则是生成CUIStructuredThemeStore
CUIMutableStructuredThemeStore
与CUIStrucetedThemeStore
都调用到一些带有rendition
字眼的类,而CUIMutableStrucetedThemeStore
还多了一层canGetRenditionWithKey:
的方法调用,致使了耗时的增长从上面这些推断,咱们可能会产生如下的一些问题:
CUIMutalbeStructuredThemeStore
与CUIStructuredThemeStore
是什么东西?rendition
又是什么东西?imageWithContentOfFile:
不对图像进行缓存,是否这个缘由致使其加载速度要比imageWithName:
要快呢?针对这些问题,咱们一个一个解决。
在研究这些问题以前,咱们先来重新认识一下 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 密不可分,那么想要探究这些问题最好的方法,就是直接去看这些方法作了什么事情。
因此咱们利用 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 中,并使用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
。
而在下一层的
[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:
来手动获取。
在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
进行各类位操做,也是会致使耗时的增长。
从上面的分析能够看出,在 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 框架对图片进行渲染工做。
在 Assets Catalogs 中加载图片则是另一条路径,在 Time Profile 中可以看到是调用
[CUICatalog _namedLookupWithName: scaleFactor: deviceIdiom: deviceSubtype: displayGamut: layoutDirection: sizeClassHorizontal: sizeClassVertical:]
复制代码
其里面也调用了在与上面同样的那两个resolveXXXX
的方法,可是在耗时上并无像在 Folder 中加载那样耗费大量时间在canGetRenditonWithKey:
中,因此能够猜想在renditionInfoForIdentifier:
中,已经获取了所需的rendition
。因此咱们来关注一下这个函数:
略去缓存的状况不谈,这个BOM树是一个比较有意思的东西,BOM——(Bill Of Material)这是一个继承自 NeXTSTEP 的文件格式,并且是在 macOS 的各类 installer 中用来决定哪些文件要进行安装、移除或者更新,咱们能够在man 5 bom
中找到这些信息:
The Mac OS X Installer uses a file system "bill of materials" to determine which files to install, remove, or upgrade. A bill of materials, bom, contains all the files within a directory, along with some information about each file. File information includes: the file's UNIX permissions, its owner and group, its size, its time of last modification, and so on. Also included are a checksum of each file and information about hard links.
很显然这里的 BOM 树表示其内是以树的形式存储数据,在其中应该是存储关于资源文件的一些东西,同时在 CoreUI.framework 中引用了 BOM.framework 中的相关 API 对这个 BOM 文件进行解析并获得相关数据,因此咱们能够猜想在 Assets Catalogs 中,编译完成的.car
文件应该会包含 BOM 数据,更进一步,可能keySignature
就是用于在树中获取对应的rendtion
与renditionKey
。
在接下来的流程中,可以看到生成的ThemeStore
是CUIStructuredThemeStore
,不一样于 Folder 中读取时所使用的CUIMutableStructuredThemeStore
,从名字上就能够猜想,它是**“不可变的”,根据上文其实也很容易推断出为何是不可变了,由于它已经获取到所须要的rendition
了,不一样于 Folder 须要动态的获取**。
从两个加载方法的对比来看,rendition
的获取是总体耗时的关键,在 Assets Catalogs 中获取的图像资源,其rendition
可以从一个 BOM 文件中获取,大大加快了加载的速度,另外一方面其renditionKey
也一样做为数据被保存到 BOM 文件中,一样attribute
也在编译过程当中获取了,因此无须要再在加载时候进行多余的操做,能够一步到位直接获取所需的图片资源以及其相关信息,并交由渲染引擎进行渲染。
另外一方面,虽然在 Folder 中生成的是CUIMutableStructuredThemeStore
,可是在读取新的图片时,仍然会生成新的themeStore
,因此在 I/O 上会消耗较大,而在 Assets Catalogs 中,因为全部图像资源都是保存在同一个.xcassets
中,因此只须要读取一次,就能够获取到全部的图像信息,那么在 I/O 次数上有了显著的优化。
因此咱们来回顾一下开头提出的问题,如今应该均可以清楚的回答了:
CUIMutalbeStructuredThemeStore
与CUIStructuredThemeStore
是什么东西?rendition
又是什么东西?imageWithContentOfFile:
不对图像进行缓存,是否这个缘由致使其加载速度要比imageWithName:
要快呢?在如今咱们能够一一解答了:
- CoreUI.framework 在加载图片中负责了什么工做?
CoreUI.framework 负责进行图片加载的准备工做,UIImage
实际上是对 CoreUI 的上层封装。
CUIMutalbeStructuredThemeStore
与CUIStructuredThemeStore
是什么东西?
咱们能够将它们理解成 imageSet ,其中包含了不一样的图像资源。
rendition
又是什么东西?
rendition
是 CoreUI.framework 对某一图像资源的不一样样式的统称,如@1x,@2x,每个rendition
有一个renditionKey
与之对应,renditionKey
包含了不一样的attribute
,用于记录图片资源的参数。
- 为何 Assets Catalogs 可以提升这么多加载速度呢?
由于在编译过程当中其会生成一个.car
文件,其中包含了 BOM 文件,BOM文件可以在加载图片时直接获取rendition
和renditionKey
以及attribute
,不一样于 Folder 中加载须要先读取图像获取其参数,再生成rendition
和renditionKey
,并进行须要大量耗时的canGetRenditionWithKey
操做。
imageWithContentOfFile:
不对图像进行缓存,是否这个缘由致使其加载速度要比imageNamed:
要快呢?
不是,只不过是imageWithContentOfFile:
不须要转换成rendition
与生成renditionKey
等耗时操做。
若是你的项目里面尚未使用 Assets Catalogs ,你应该立刻使用,由于它不仅是可以更方便的管理图像,还能够提供包括切图等一系列方便的功能,更不用说它在 I/O 上性能的显著提高了。
那将图片保存在 Folder 上是否就永远不可取呢?其实也不必定,由于保存在 Assets Catalogs 中的图像没法经过imageWithContentOfFile:
获取,因此一些不经常使用、占用内存多的图片,能够放在 Folder 中,并经过imageWithContentOfFile:
获取,另外一方面,若是你的应用是**“内存紧张”**的,或者是想应用更长时间存活在后台,那么能够将图片都存放在 Folder,以减小imageNamed:
对图片的缓存,换取更低的内存占用。不过我仍是建议使用 Assets Catalogs 进行图像的管理。
碰巧在前几天也有其余博主写了一片关于 Assets Catalogs 优化的文章,他文章关注的点更广,从 BOM 文件结构与内存映射方面都有涉及到,你们有兴趣能够去看一下。
更多内容能够关注个人博客