早在 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 框架對圖片進行渲染工作。
從 Assets Catalogs 中載入圖片
1. 獲取 Rendition
在 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
。
2. CUIStructuredThemeStore
在接下來的流程中,能夠看到生成的ThemeStore
是CUIStructuredThemeStore
,不同於 Folder 中讀取時所使用的CUIMutableStructuredThemeStore
,從名字上就可以猜測,它是**“不可變的”,根據上文其實也很容易推斷出為什麼是不可變了,因為它已經獲取到所需要的rendition
了,不同於 Folder 需要動態的獲取**。
3. 小結
從兩個載入方法的對比來看,rendition
的獲取是整體耗時的關鍵,在 Assets Catalogs 中獲取的影象資源,其rendition
能夠從一個 BOM 檔案中獲取,大大加快了載入的速度,另一方面其renditionKey
也同樣作為資料被儲存到 BOM 檔案中,同樣attribute
也在編譯過程中獲取了,所以無需要再在載入時候進行多餘的操作,可以一步到位直接獲取所需的圖片資源以及其相關資訊,並交由渲染引擎進行渲染。
另一方面,雖然在 Folder 中生成的是CUIMutableStructuredThemeStore
,但是在讀取新的圖片時,仍然會生成新的themeStore
,所以在 I/O 上會消耗較大,而在 Assets Catalogs 中,由於所有影象資源都是儲存在同一個.xcassets
中,所以只需要讀取一次,就可以獲取到所有的影象資訊,那麼在 I/O 次數上有了顯著的優化。
問題回顧
所以我們來回顧一下開頭提出的問題,現在應該都可以清楚的回答了:
- CoreUI.framework 在載入圖片中負責了什麼工作?
CUIMutalbeStructuredThemeStore
與CUIStructuredThemeStore
是什麼東西?rendition
又是什麼東西?- 為什麼 Assets Catalogs 能夠提高這麼多載入速度呢?
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 進行影象的管理。
參考資料
- 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
更多內容可以關注我的部落格