早在 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 文件結構與內存映射方面都有涉及到,你們有興趣能夠去看一下。
更多內容能夠關注個人博客