
早在 XCode 5,蘋果引入了 Assets Catalogs ,它作為一個重要的開發(fā)組件,能夠讓開發(fā)者可以更方便的管理項目內(nèi)的圖片資源。
蘋果也在不斷的完善它的功能:
- 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 有什么自己獨特的優(yōu)勢呢?在 WWDC 2016 上提到的 I/O 優(yōu)化是怎么完成的?imageName:、imageWithContentOfFile:這些方法在不同情況下又有什么表現(xiàn)呢,這篇文章就是基于這種種疑問誕生的。
太長不看版:
Assets Catalogs 將會在編譯時生成一個
.car文件,并在其中包含了這個圖像加載所需的一切數(shù)據(jù),當(dāng)圖像需要加載的時候,可以直接獲取其中的數(shù)據(jù)并進(jìn)行加載。
從一次 I/O 優(yōu)化說起
相信大家現(xiàn)在在項目里面都會使用 Assets Catalogs 對圖片資源進(jìn)行管理,但很不幸,我接手的項目依然是把圖片放在 Folder 中,這樣看起來似乎并沒有什么問題,但是如果打開 Time Profile ,就會發(fā)現(xiàn)把圖片放在 Folder 中并使用imageName:加載圖片所用的耗時要比放在 Assets Catalogs 中要慢得多。
保存在 Folder ,并使用imageName:獲取:
展開后的調(diào)用棧耗時:
保存在 Assets Cataglogs ,并使用imageName:獲?。?/strong>
展開后的調(diào)用棧耗時:
而如果使用imageWithContentOfFile:,則兩種存儲方式所用的耗時則相同
使用imageWithContentOfFile:獲取:
由這幾個案例,我們可以推斷出:
- 保存在 Folder 中并不會導(dǎo)致查找時間的增加,因為在
imageWithContentOfFile:中兩者加載圖片的耗時一致 - 使用
imageName:加載圖片時,兩種存儲方式都調(diào)用了底層 CoreUI.framework 的框架,但是調(diào)用的方法有所不同 - 存儲在 Folder 中的圖片加載時生成的是
CUIMutableStructuredThemeStore,而存儲在 Assets Catalogs 中則是生成CUIStructuredThemeStore -
CUIMutableStructuredThemeStore與CUIStrucetedThemeStore都調(diào)用到一些帶有rendition字眼的類,而CUIMutableStrucetedThemeStore還多了一層canGetRenditionWithKey:的方法調(diào)用,導(dǎo)致了耗時的增加
從上面這些推斷,我們可能會產(chǎn)生以下的一些問題:
- CoreUI.framework 在加載圖片中負(fù)責(zé)了什么工作?
-
CUIMutalbeStructuredThemeStore與CUIStructuredThemeStore是什么東西? -
rendition又是什么東西? - 為什么 Assets Catalogs 能夠提高這么多加載速度呢?
-
imageWithContentOfFile:不對圖像進(jìn)行緩存,是否這個原因?qū)е缕浼虞d速度要比imageWithName:要快呢?
針對這些問題,我們一個一個解決。
探秘 Assets Catalogs 與 .car 文件
在研究這些問題之前,我們先來從新認(rèn)識一下 Assets Catalogs。
關(guān)于 Assets Catalogs ,它詳細(xì)的使用方法相信大家已經(jīng)很熟悉了,蘋果也在Asset Catalog Format Reference中給出了.xcassets的組成。
但是可能很少人知道在 XCode 編譯過程中,保存在 Assets Catalogs 中的圖像資源并不是簡單的復(fù)制到 APP 的 Bundle 中,而是會在編譯時生成一個將資源打包并生成索引的.car文件,而它在蘋果開發(fā)者文檔上并沒有介紹,在網(wǎng)上關(guān)于它的信息也是少之又少。
那么.car文件究竟是什么?
要知道.car文件究竟是什么,有什么作用,我們可以先看看它包含了什么。所以我在 Assets Catalogs 中放入了一組PNG文件:
隨后在 XCode 中對項目進(jìn)行編譯,在生成的 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文件中去記錄它們的一些數(shù)據(jù),同時還又一個叫keyFormatter的東西,還有很多東西我們暫時不知道它們是什么意思,所以我們繼續(xù)探究。
反編譯 CoreUI.framework
既然知道了整個圖片的加載過程是與 CoreUI.framework 密不可分,那么想要探究這些問題最好的方法,就是直接去看這些方法做了什么事情。
所以我們利用 Hopper Disassemble 對 CoreUI.framework 進(jìn)行反編譯,看一下圖片加載的過程中究竟發(fā)生了什么事情。
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 了一份,不方便的同學(xué)可以先看一下頭文件。
Folder 中加載圖片的過程
1. 基礎(chǔ)判斷
首先關(guān)注的是保存在 Folder 中,并使用imageName:方法加載的例子,根據(jù) Time Profile 中的調(diào)用棧,我們找到
[CUICatalog _resolvedRenditionKeyForName: scaleFactor: deviceIdiom: deviceSubtype: displayGamut: layoutDirection: sizeClassHorizontal: sizeClassVertical: memoryClass: graphicsClass: appearanceIdentifier: graphicsFallBackOrder: deviceSubtypeFallBackOrder:]
而在方法內(nèi)部我們很容易關(guān)注到它對設(shè)備的型號做了一次判斷,也對加載的圖片的name進(jìn)行了一次檢查,隨后獲取了對應(yīng)name的baseKey,然后調(diào)用下一層的方法
而baseKey則是去取renditionKey,它首先會獲取一個叫themeStore的東西,在調(diào)用棧中我們可以知道,如果圖片存放在 Folder 中,則會生成CUIMutableStructuredThemeStore,隨后它會根據(jù)圖片的名字,獲取CUIRenditionKey對象。
而且從這里我們可以猜測到應(yīng)該每一個rendition都有與之對應(yīng)的renditionKey,在一張圖片資源里,它們可能是一對一的形式,即一個rendition對應(yīng)一個renditionKey。
2. 圖片加載前的最后準(zhǔn)備工作
而在下一層的
[CUICatalog _resolvedRenditionKeyFromThemeRef: withBaseKey: scaleFactor: deviceIdiom: deviceSubtype: displayGamut: layoutDirection: sizeClassHorizontal: sizeClassVertical: memoryClass: graphicsClass: graphicsFallBackOrder: deviceSubtypeFallBackOrder: iconSizeIndex: appearanceIdentifier:]
這一個方法是負(fù)責(zé)完成加載圖片前最后的準(zhǔn)備工作,包括對應(yīng)圖像的分辨率、放大倍數(shù)、方向、水平尺寸、垂直尺寸等參數(shù)的設(shè)置
同時在此方法內(nèi),我們會注意到有很多地方調(diào)用canGetRenditionWithKey:這個方法
而在開始調(diào)用canGetRenditionWithKey:之前,會調(diào)用renditionInfoForIdentifier:去獲取rendition,如果能夠成功獲取,則不會再進(jìn)入到多次調(diào)用canGetRenditionWithKey:的流程中,這一點十分重要,因為只有在 Folder 中加載圖片才不能在這步成功獲取rendition,所以可以假設(shè)rendition是 Assets Catalogs 中附帶的一些屬性,在 Assets Catalogs 中能夠直接獲取,而在 Folder 中則是需要重復(fù)調(diào)用canGetRenditionWithKey:來手動獲取。
3. canGetRendition 的判斷
在canGetRenditionWithKey:方法內(nèi)部可以看到它本質(zhì)上是調(diào)用了renditionWithKey:的方法,再判斷該方法返回值是否為空:
而在renditionWithKey:方法內(nèi),它主要做了兩件事:
- 根據(jù)上一層傳入的
[CUIRenditionKey keyList]獲取keySignature - 根據(jù)
[CUIRenditionKey keyList]與keySignature獲取rendition
先看一下這個keyList:
它其實是獲取自身的的屬性,是一個 getter 方法,拿到的值其實不是一個 List ,而是一個結(jié)構(gòu)體:
里面包含了identifier與value。
所以利用這個keyList,CUIMutableStructuredThemeStore獲取到了keySignature,并根據(jù)它獲取到了對應(yīng)的rendition:
可以看到這個方法被加了一個線程同步鎖objc_sync_enter,以確保它是線程安全的,所以它的耗時會高很多。另一方面,在獲取keySignature的時候,還執(zhí)行了一個叫做__CUICopySortedKeySignature的方法,這個方法是對keySignature進(jìn)行各種位操作,也是會導(dǎo)致耗時的增加。
4. 小結(jié)
從上面的分析可以看出,在 Folder 中加載導(dǎo)致耗時增加的原因如下:
加載圖片過程中由于沒有辦法直接獲取
rendition,所以需要調(diào)用canGetRenditionWithKey:方法進(jìn)行判斷,而該方法會調(diào)用兩個比較耗時的操作,一個是對keySignature的 copy 操作,另一個是在添加了線程鎖并從CUIMutableStructuredThemeStore的字典中取出rendition的操作,這兩個操作是導(dǎo)致耗時增加的元兇。
所以CUIMutableStructuredThemeStore在 CoreUI.framework 中起到了一個類似 imageSet 的作用,其中包括了一個可變字典,能夠存放rendition,所以rendition就是我們需要加載的圖片,而renditionKey則是這個圖像資源的一種標(biāo)識,能夠通過renditionKey獲取到對應(yīng)的rendition,同時renditionKey中包含了各種attribute,是代表該圖片的分辨率、垂直大小、水平大小等參數(shù),這些參數(shù)這也和我們之前解析的.json文件的數(shù)據(jù)也能一一對應(yīng):
{
"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,把圖片轉(zhuǎn)成rendition并保存到其可變數(shù)組中,并根據(jù)圖片名稱生成renditionKey,隨后根據(jù)CUINamedImageDescription這個類,獲取圖片的相關(guān)信息,并填充到renditionKey中,在需要加載圖片的時候,先根據(jù)renditionKey獲取對應(yīng)的圖片資源,然后再從renditionKey中讀取各種attribute信息,并交由 Image I/O 框架對圖片進(jìn)行渲染工作。
從 Assets Catalogs 中加載圖片
1. 獲取 Rendition
在 Assets Catalogs 中加載圖片則是另外一條路徑,在 Time Profile 中能夠看到是調(diào)用
[CUICatalog _namedLookupWithName: scaleFactor: deviceIdiom: deviceSubtype: displayGamut: layoutDirection: sizeClassHorizontal: sizeClassVertical:]
其里面也調(diào)用了在與上面一樣的那兩個resolveXXXX的方法,但是在耗時上并沒有像在 Folder 中加載那樣耗費大量時間在canGetRenditonWithKey:中,所以可以猜測在renditionInfoForIdentifier:中,已經(jīng)獲取了所需的rendition。所以我們來關(guān)注一下這個函數(shù):
略去緩存的情況不談,這個BOM樹是一個比較有意思的東西,BOM——(Bill Of Material)這是一個繼承自 NeXTSTEP 的文件格式,而且是在 macOS 的各種 installer 中用來決定哪些文件要進(jìn)行安裝、移除或者更新,我們可以在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 樹表示其內(nèi)是以樹的形式存儲數(shù)據(jù),在其中應(yīng)該是存儲關(guān)于資源文件的一些東西,同時在 CoreUI.framework 中引用了 BOM.framework 中的相關(guān) API 對這個 BOM 文件進(jìn)行解析并得到相關(guān)數(shù)據(jù),所以我們可以猜測在 Assets Catalogs 中,編譯完成的.car文件應(yīng)該會包含 BOM 數(shù)據(jù),更進(jìn)一步,可能keySignature就是用于在樹中獲取對應(yīng)的rendtion與renditionKey。
2. CUIStructuredThemeStore
在接下來的流程中,能夠看到生成的ThemeStore是CUIStructuredThemeStore,不同于 Folder 中讀取時所使用的CUIMutableStructuredThemeStore,從名字上就可以猜測,它是“不可變的”,根據(jù)上文其實也很容易推斷出為什么是不可變了,因為它已經(jīng)獲取到所需要的rendition了,不同于 Folder 需要動態(tài)的獲取。
3. 小結(jié)
從兩個加載方法的對比來看,rendition的獲取是整體耗時的關(guān)鍵,在 Assets Catalogs 中獲取的圖像資源,其rendition能夠從一個 BOM 文件中獲取,大大加快了加載的速度,另一方面其renditionKey也同樣作為數(shù)據(jù)被保存到 BOM 文件中,同樣attribute也在編譯過程中獲取了,所以無需要再在加載時候進(jìn)行多余的操作,可以一步到位直接獲取所需的圖片資源以及其相關(guān)信息,并交由渲染引擎進(jìn)行渲染。
另一方面,雖然在 Folder 中生成的是CUIMutableStructuredThemeStore,但是在讀取新的圖片時,仍然會生成新的themeStore,所以在 I/O 上會消耗較大,而在 Assets Catalogs 中,由于所有圖像資源都是保存在同一個.xcassets中,所以只需要讀取一次,就可以獲取到所有的圖像信息,那么在 I/O 次數(shù)上有了顯著的優(yōu)化。
問題回顧
所以我們來回顧一下開頭提出的問題,現(xiàn)在應(yīng)該都可以清楚的回答了:
- CoreUI.framework 在加載圖片中負(fù)責(zé)了什么工作?
-
CUIMutalbeStructuredThemeStore與CUIStructuredThemeStore是什么東西? -
rendition又是什么東西? - 為什么 Assets Catalogs 能夠提高這么多加載速度呢?
-
imageWithContentOfFile:不對圖像進(jìn)行緩存,是否這個原因?qū)е缕浼虞d速度要比imageWithName:要快呢?
在現(xiàn)在我們可以一一解答了:
- CoreUI.framework 在加載圖片中負(fù)責(zé)了什么工作?
CoreUI.framework 負(fù)責(zé)進(jìn)行圖片加載的準(zhǔn)備工作,UIImage其實是對 CoreUI 的上層封裝。
CUIMutalbeStructuredThemeStore與CUIStructuredThemeStore是什么東西?
我們可以將它們理解成 imageSet ,其中包含了不同的圖像資源。
rendition又是什么東西?
rendition是 CoreUI.framework 對某一圖像資源的不同樣式的統(tǒng)稱,如@1x,@2x,每一個rendition有一個renditionKey與之對應(yīng),renditionKey包含了不同的attribute,用于記錄圖片資源的參數(shù)。
- 為什么 Assets Catalogs 能夠提高這么多加載速度呢?
因為在編譯過程中其會生成一個.car文件,其中包含了 BOM 文件,BOM文件能夠在加載圖片時直接獲取rendition和renditionKey以及attribute,不同于 Folder 中加載需要先讀取圖像獲取其參數(shù),再生成rendition和renditionKey,并進(jìn)行需要大量耗時的canGetRenditionWithKey操作。
imageWithContentOfFile:不對圖像進(jìn)行緩存,是否這個原因?qū)е缕浼虞d速度要比imageNamed:要快呢?
不是,只不過是imageWithContentOfFile:不需要轉(zhuǎn)換成rendition與生成renditionKey等耗時操作。
總結(jié)
如果你的項目里面還沒有使用 Assets Catalogs ,你應(yīng)該馬上使用,因為它不只是能夠更方便的管理圖像,還可以提供包括切圖等一系列方便的功能,更不用說它在 I/O 上性能的顯著提升了。
那將圖片保存在 Folder 上是否就永遠(yuǎn)不可取呢?其實也不一定,因為保存在 Assets Catalogs 中的圖像無法通過imageWithContentOfFile:獲取,所以一些不常用、占用內(nèi)存多的圖片,可以放在 Folder 中,并通過imageWithContentOfFile:獲取,另一方面,如果你的應(yīng)用是“內(nèi)存緊張”的,或者是想應(yīng)用更長時間存活在后臺,那么可以將圖片都存放在 Folder,以減少imageNamed:對圖片的緩存,換取更低的內(nèi)存占用。不過我還是建議使用 Assets Catalogs 進(jìn)行圖像的管理。
參考資料
- Analysing Assets.car file in iOS
- iOS-Asset-Extractor
- UIImage加載圖片的方式以及Images.xcassets對于加載方法的影響
- How to use create and use a UIImageAsset in iOS 8
- UIImageAsset
- Unleashing the power of asset catalogs and bundles on iOS
更多內(nèi)容可以關(guān)注我的博客