iOS拾遺—— Assets Catalogs 與 I/O 優化

杜瑋發表於2019-04-17

早在 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:獲取:

iOS拾遺—— Assets Catalogs 與 I/O 優化

展開後的呼叫棧耗時:

iOS拾遺—— Assets Catalogs 與 I/O 優化

儲存在 Assets Cataglogs ,並使用imageName:獲取:

iOS拾遺—— Assets Catalogs 與 I/O 優化

展開後的呼叫棧耗時:

iOS拾遺—— Assets Catalogs 與 I/O 優化

而如果使用imageWithContentOfFile:,則兩種儲存方式所用的耗時則相同

使用imageWithContentOfFile:獲取:

iOS拾遺—— Assets Catalogs 與 I/O 優化

由這幾個案例,我們可以推斷出:

  1. 儲存在 Folder 中並不會導致查詢時間的增加,因為在imageWithContentOfFile:中兩者載入圖片的耗時一致
  2. 使用imageName:載入圖片時,兩種儲存方式都呼叫了底層 CoreUI.framework 的框架,但是呼叫的方法有所不同
  3. 儲存在 Folder 中的圖片載入時生成的是CUIMutableStructuredThemeStore,而儲存在 Assets Catalogs 中則是生成CUIStructuredThemeStore
  4. CUIMutableStructuredThemeStoreCUIStrucetedThemeStore都呼叫到一些帶有rendition字眼的類,而CUIMutableStrucetedThemeStore還多了一層canGetRenditionWithKey:的方法呼叫,導致了耗時的增加

從上面這些推斷,我們可能會產生以下的一些問題:

  • CoreUI.framework 在載入圖片中負責了什麼工作?
  • CUIMutalbeStructuredThemeStoreCUIStructuredThemeStore是什麼東西?
  • 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檔案:

iOS拾遺—— Assets Catalogs 與 I/O 優化

隨後在 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 解析完成後會顯示這樣一個介面:

iOS拾遺—— Assets Catalogs 與 I/O 優化

隨後選擇右上角的這一個按鈕,就可以看到反編譯出來的程式碼了:

iOS拾遺—— Assets Catalogs 與 I/O 優化

在 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進行了一次檢查,隨後獲取了對應namebaseKey,然後呼叫下一層的方法

iOS拾遺—— Assets Catalogs 與 I/O 優化

baseKey則是去取renditionKey,它首先會獲取一個叫themeStore的東西,在呼叫棧中我們可以知道,如果圖片存放在 Folder 中,則會生成CUIMutableStructuredThemeStore,隨後它會根據圖片的名字,獲取CUIRenditionKey物件。

iOS拾遺—— Assets Catalogs 與 I/O 優化

而且從這裡我們可以猜測到應該每一個rendition都有與之對應的renditionKey,在一張圖片資源裡,它們可能是一對一的形式,即一個rendition對應一個renditionKey

2. 圖片載入前的最後準備工作

而在下一層的

[CUICatalog _resolvedRenditionKeyFromThemeRef: withBaseKey: scaleFactor: deviceIdiom: deviceSubtype: displayGamut: layoutDirection: sizeClassHorizontal: sizeClassVertical: memoryClass: graphicsClass: graphicsFallBackOrder: deviceSubtypeFallBackOrder: iconSizeIndex: appearanceIdentifier:]
複製程式碼

這一個方法是負責完成載入圖片前最後的準備工作,包括對應影象的解析度、放大倍數、方向、水平尺寸、垂直尺寸等引數的設定

iOS拾遺—— Assets Catalogs 與 I/O 優化

同時在此方法內,我們會注意到有很多地方呼叫canGetRenditionWithKey:這個方法

iOS拾遺—— Assets Catalogs 與 I/O 優化

而在開始呼叫canGetRenditionWithKey:之前,會呼叫renditionInfoForIdentifier:去獲取rendition,如果能夠成功獲取,則不會再進入到多次呼叫canGetRenditionWithKey:的流程中,這一點十分重要,因為只有在 Folder 中載入圖片才不能在這步成功獲取rendition,所以可以假設rendition是 Assets Catalogs 中附帶的一些屬性,在 Assets Catalogs 中能夠直接獲取,而在 Folder 中則是需要重複呼叫canGetRenditionWithKey:來手動獲取。

3. canGetRendition 的判斷

canGetRenditionWithKey:方法內部可以看到它本質上是呼叫了renditionWithKey:的方法,再判斷該方法返回值是否為空:

iOS拾遺—— Assets Catalogs 與 I/O 優化

而在renditionWithKey:方法內,它主要做了兩件事

  1. 根據上一層傳入的[CUIRenditionKey keyList]獲取keySignature
  2. 根據[CUIRenditionKey keyList]keySignature獲取rendition

iOS拾遺—— Assets Catalogs 與 I/O 優化

先看一下這個keyList

iOS拾遺—— Assets Catalogs 與 I/O 優化

它其實是獲取自身的的屬性,是一個 getter 方法,拿到的值其實不是一個 List ,而是一個結構體

iOS拾遺—— Assets Catalogs 與 I/O 優化

裡面包含了identifiervalue

所以利用這個keyListCUIMutableStructuredThemeStore獲取到了keySignature,並根據它獲取到了對應的rendition

iOS拾遺—— Assets Catalogs 與 I/O 優化

可以看到這個方法被加了一個執行緒同步鎖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。所以我們來關注一下這個函式:

iOS拾遺—— Assets Catalogs 與 I/O 優化

略去快取的情況不談,這個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就是用於在樹中獲取對應的rendtionrenditionKey

2. CUIStructuredThemeStore

在接下來的流程中,能夠看到生成的ThemeStoreCUIStructuredThemeStore,不同於 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 在載入圖片中負責了什麼工作?
  • CUIMutalbeStructuredThemeStoreCUIStructuredThemeStore是什麼東西?
  • rendition又是什麼東西?
  • 為什麼 Assets Catalogs 能夠提高這麼多載入速度呢?
  • imageWithContentOfFile:不對影象進行快取,是否這個原因導致其載入速度要比imageWithName:要快呢?

在現在我們可以一一解答了:

  • CoreUI.framework 在載入圖片中負責了什麼工作?

CoreUI.framework 負責進行圖片載入的準備工作,UIImage其實是對 CoreUI 的上層封裝。

  • CUIMutalbeStructuredThemeStoreCUIStructuredThemeStore是什麼東西?

我們可以將它們理解成 imageSet ,其中包含了不同的影象資源。

  • rendition又是什麼東西?

rendition是 CoreUI.framework 對某一影象資源的不同樣式的統稱,如@1x,@2x,每一個rendition有一個renditionKey與之對應,renditionKey包含了不同的attribute,用於記錄圖片資源的引數。

  • 為什麼 Assets Catalogs 能夠提高這麼多載入速度呢?

因為在編譯過程中其會生成一個.car檔案,其中包含了 BOM 檔案,BOM檔案能夠在載入圖片時直接獲取renditionrenditionKey以及attribute,不同於 Folder 中載入需要先讀取影象獲取其引數,再生成renditionrenditionKey,並進行需要大量耗時的canGetRenditionWithKey操作。

  • imageWithContentOfFile:不對影象進行快取,是否這個原因導致其載入速度要比imageNamed:要快呢?

不是,只不過是imageWithContentOfFile:不需要轉換成rendition與生成renditionKey等耗時操作。

總結

如果你的專案裡面還沒有使用 Assets Catalogs ,你應該馬上使用,因為它不只是能夠更方便的管理影象,還可以提供包括切圖等一系列方便的功能,更不用說它在 I/O 上效能的顯著提升了。

那將圖片儲存在 Folder 上是否就永遠不可取呢?其實也不一定,因為儲存在 Assets Catalogs 中的影象無法通過imageWithContentOfFile:獲取,所以一些不常用、佔用記憶體多的圖片,可以放在 Folder 中,並通過imageWithContentOfFile:獲取,另一方面,如果你的應用是**“記憶體緊張”**的,或者是想應用更長時間存活在後臺,那麼可以將圖片都存放在 Folder,以減少imageNamed:對圖片的快取,換取更低的記憶體佔用。不過我還是建議使用 Assets Catalogs 進行影象的管理。

參考資料

更多內容可以關注我的部落格

相關文章