你不了解的Assets.car

轉(zhuǎn)發(fā):http://www.itdecent.cn/p/03c001cfa954

.car文件是蘋果.xcassets文件夾中的資源編譯后生成的,會以Assets.car的名稱打包進(jìn)應(yīng)用的安裝包中。這篇文章中我們將分析car文件的文件結(jié)構(gòu),并討論如何將car文件中的顏色、圖片、pdf、文檔等資源解析出來。

原創(chuàng)文章,如需轉(zhuǎn)載請在下面留言讓我知道??。不留言不在開頭標(biāo)明出處鏈接的壞同學(xué),1字1元索賠??


背景.

方案引入

公司中現(xiàn)有換膚機(jī)制要花費(fèi)大量時間對軟件進(jìn)行改造,具體方案就不介紹了,總之大家都吐槽不好用??。為了能盡量貼近蘋果官方推薦的資源管理方式、減少開發(fā)者學(xué)習(xí)成本、使用官方的優(yōu)化方案,最開始的出發(fā)點(diǎn)是要使用Asset Catalogs管理資源。Asset Catalogs管理方式在Xcode工程中最常接觸到的就是名為“Assets.xcassets”的文件夾,創(chuàng)建工程時默認(rèn)就會創(chuàng)建這個文件夾,在Xcode中可以使用圖形界面很方便地管理資源文件,如下圖所示:

image

一般大家會在xcassets中放置應(yīng)用的圖標(biāo)、UI切圖,從iOS 11開始,xcassets中還可以放置顏色信息。其實(shí)還可以在其中放置PDF、紋理、Data等數(shù)據(jù),只是平時很少用到,而且換膚需求也不要求涉及這些數(shù)據(jù),所以我們只要關(guān)心切圖和顏色就可以了。如果能夠把顏色、UI切圖、圖標(biāo)這些資源在編譯時動態(tài)替換為新的資源,即可滿足現(xiàn)在行業(yè)的需求。幸運(yùn)的是,xcassets中的資源不僅能在編譯時替換,甚至可以在運(yùn)行時,通過從不同的bundle中讀取資源達(dá)到動態(tài)換膚的目的,這大大增加了這種方案的可擴(kuò)展性。

遇到的問題

眾所周知,xcassets中的資源會被編譯為car格式的文件,保存于App或framework的包中。編譯過程中會做圖片壓縮等優(yōu)化工作,雖然這些是我們想要的,但同時也產(chǎn)生一個問題,那就是必須通過系統(tǒng)API才能讀取出car中的資源。讀取圖片不是什么難事,但從下面UIKit中的代碼片段可以看出,讀取顏色信息的API只有在iOS11之后才能使用。

@interface UIColor (UIColorNamedColors)
+ (nullable UIColor *)colorNamed:(NSString *)name API_AVAILABLE(ios(11.0));      // load from main bundle
+ (nullable UIColor *)colorNamed:(NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection API_AVAILABLE(ios(11.0));
@end

而我們的應(yīng)用至少要兼容3個最新版本的iOS系統(tǒng),目前最新系統(tǒng)是iOS 12,也就是說從iOS 10的系統(tǒng)沒法讀取出顏色信息。為了解決這個問題,我們需要自己實(shí)現(xiàn)讀取car文件中顏色信息的邏輯,而關(guān)于car文件的格式,蘋果是沒有公開說明文檔的,這就是我們要攻克的最大的難題。

解析car文件

car究竟是什么

為了避免重復(fù)造輪子,最先想到的方案就是使用第三方的框架實(shí)現(xiàn)解析功能。但是過程不那么順利,Github上開源工具有很多,但全都是在macOS中解析car文件,而且沒有一個是真正解析car的,都是通過iOS或者macOS系統(tǒng)提供的庫實(shí)現(xiàn)的。如果能用系統(tǒng)庫,我們也就不用解析car了。
之后在公司內(nèi)部找了一些比較資深的iOS開發(fā)者,咨詢了一圈,發(fā)現(xiàn)也沒有人做過這個事情。百度也沒有找到相關(guān)的內(nèi)容,一度差點(diǎn)否決了這個方案。最終經(jīng)過大量查找,在Wikipedia中發(fā)現(xiàn)了蛛絲馬跡,car文件的結(jié)構(gòu)是BOM!馬上用"Synalyze It! Pro"應(yīng)用分析一下car的內(nèi)容,發(fā)現(xiàn)前8個字節(jié)真的是“BOMStore”,如下圖所示:

image

簡單介紹下BOM文件格式,BOM是“Bill of Materials”的縮寫。之前被用于macOS的應(yīng)用安裝器中,用于標(biāo)識哪些文件需要安裝、哪些需要移除或者升級。具體介紹可以參考這個鏈接https://en.wikipedia.org/wiki/BOM_(file_format)
幸好BOM文件已經(jīng)在macOS中用了很多年,雖然官方?jīng)]有文檔,但內(nèi)部結(jié)構(gòu)有人嘗試逆向過。BOM只定義了一種存儲信息的樹狀結(jié)構(gòu),并沒有規(guī)定樹中存儲的數(shù)據(jù)是什么樣的。分析出BOM數(shù)據(jù)之后,還要分析出顏色信息是以什么格式儲存在BOM結(jié)構(gòu)中的,這篇文章Reverse engineering the .car file format (compiled Asset Catalogs)介紹了car中的信息,但使用了macOS中的BOM.framework解析BOM中的數(shù)據(jù),iOS中并沒有這個框架。我們要結(jié)合上面的文章和之前開發(fā)者分析出的BOM大致結(jié)構(gòu),解析出car中指定名稱的顏色數(shù)據(jù)。

BOM結(jié)構(gòu)

感謝PureDarwin在Github上的開源項目osxbom。雖然這個項目是為了解析macOS的應(yīng)用安裝器中的數(shù)據(jù),但是BOM的頭、樹中的索引和節(jié)點(diǎn)等數(shù)據(jù)結(jié)構(gòu)和解析方法都很有幫助。下面大致介紹一下BOM結(jié)構(gòu),給大家一些啟發(fā)。
BOM文件的最開頭,是頭數(shù)據(jù),相信大家看一下osxbom中的結(jié)構(gòu)體就明白了:

struct BOMHeader {
  char magic[8]; // = BOMStore
  uint32_t unknown0; // = 1?
  uint32_t unknown1; // = 73 = 0x49?
  uint32_t indexOffset; // Length of first part
  uint32_t indexLength; // Length of second part
  uint32_t varsOffset;
  uint32_t trailerLen; // FIXME: What does this data at the end mean?
} __attribute__((packed));

  • indexOffset
    它的含義是索引表在BOMHeader后面多少個字節(jié)地址偏移處。這里有一個新概念就是索引表,索引表可以根據(jù)一個(索引)數(shù)字找到BOM文件中對應(yīng)的地址偏移。有了索引表,只要給出一個很小的(索引)數(shù)字,就可以跳轉(zhuǎn)到BOM中的任意位置讀取數(shù)據(jù)。索引表的結(jié)構(gòu)比較簡單,看下面的結(jié)構(gòu)體就可以理解了:
struct BOMIndex {
  uint32_t address;
  uint32_t length;
} __attribute__((packed));

struct BOMIndexHeader {
  uint32_t unknown0; // FIXME: What is this? It is not the length of the array...
  struct BOMIndex index[FLEXIBLE_ARRAY_MEMBER];
} __attribute__((packed));

索引數(shù)字如果是3,就找到index[3]結(jié)構(gòu)體,其中就存儲著地址偏移和數(shù)據(jù)塊的長度。

  • varsOffset
    BOMHeader中還有個重要的信息是varsOffset,它表示BOM中的每一棵樹的名稱、數(shù)據(jù)位置的索引數(shù)字的表,在BOMHeader后面多少個字節(jié)偏移處,結(jié)構(gòu)如下所示:
struct BOMVar {
  uint32_t index;
  uint8_t length;
  char name[FLEXIBLE_ARRAY_MEMBER]; // length
} __attribute__((packed));

struct BOMVars {
  uint32_t count; // Number of entries that follow
  struct BOMVar first[FLEXIBLE_ARRAY_MEMBER];
} __attribute__((packed));

如果我們要找某一棵名為RENDITIONS的樹,只要遍歷BOMVars中的所有BOMVar,判斷名稱是否和我們要的一致,如果一致則根據(jù)BOMVar中的index索引表中查找到對應(yīng)的地址偏移即可取出這棵樹的數(shù)據(jù)。

  • 其他
    上面已經(jīng)介紹了BOM中的主要內(nèi)容,關(guān)系有點(diǎn)繞,但是也很精妙,可以體會一下設(shè)計BOM結(jié)構(gòu)的人思維方式??傊?,有了上面的基礎(chǔ)知識,就可以根據(jù)樹的名稱拿到具體的數(shù)據(jù)塊了。其實(shí)每棵樹的數(shù)據(jù)塊的結(jié)構(gòu)設(shè)計,也是相當(dāng)巧妙的,有興趣的同事可以看下osxbom源碼自行分析,這里由于篇幅原因不再贅述。

car信息

上面已經(jīng)提到,car數(shù)據(jù)的結(jié)構(gòu)在Reverse engineering the .car file format (compiled Asset Catalogs)博客中已經(jīng)有比較詳細(xì)的描述。
幾乎所有圖片、顏色等信息都存儲在名為RENDITIONS的樹中,樹中每個節(jié)點(diǎn)的數(shù)據(jù)都是下面這個結(jié)構(gòu)所示的結(jié)構(gòu):

struct csiheader {
    uint32_t tag;                               // 'CTSI'
    uint32_t version;
    struct renditionFlags renditionFlags;
    uint32_t width;
    uint32_t height;
    uint32_t scaleFactor;
    uint32_t pixelFormat;
    struct {
        uint32_t colorSpaceID:4;
        uint32_t reserved:28;
    } colorSpace;
    struct csimetadata csimetadata;
    struct csibitmaplist csibitmaplist;
} __attribute__((packed));

看結(jié)構(gòu)體已經(jīng)非常明確了,csimetadata中存儲著當(dāng)前節(jié)點(diǎn)數(shù)據(jù)的類型(比如圖片、顏色、PDF),還有數(shù)據(jù)的名稱(比如圖片名、顏色名、PDF文件名)。遍歷樹中每個節(jié)點(diǎn),找到希望獲取的顏色類型節(jié)點(diǎn),并且顏色名和希望獲取的一致,剩下的就是去除顏色數(shù)據(jù)即可。其他類型的數(shù)據(jù)在博客中也都提到,有興趣的同事可以自己研究一下,下面我就以解析顏色數(shù)據(jù)為例。
通過csiheader里面csibitmaplist中的數(shù)據(jù)偏移等信息,可以找到顏色信息存儲的具體位置(是的,這個名字看起來很像圖片,因為早期car中只能存儲圖片)。
到這里我們就獲取到了顏色信息的數(shù)據(jù),它的結(jié)構(gòu)如下所示:

struct csicolor {
    uint32_t tag;                   // COLR
    uint32_t version;
    struct {
        uint32_t colorSpaceID:8;
        uint32_t unknown0:3;
        uint32_t reserved:21;
    } colorSpace;
    uint32_t numberOfComponents;
    double components[];
} __attribute__((packed));

上面的結(jié)構(gòu)體名稱是csicolor,因為這是顏色信息的結(jié)構(gòu)體,其他類型數(shù)據(jù)有對應(yīng)的結(jié)構(gòu)體。我們可以看到,其中有顏色空間colorSpaceID,它表示顏色使用的SRGB還是灰度等等。組件數(shù)量numberOfComponents和組件components,表示的是某種顏色空間中的不同組件,比如SRGB中的紅、綠、藍(lán)、透明通道的亮度值,或者灰度顏色中的亮度、透明度值。

總結(jié)

至此,我們就把car文件中的顏色信息全部讀取出來了。篇幅有限,有很多細(xì)節(jié)沒有羅列。這其中也確實(shí)有大量的工作要做,我們要分析、驗證BOM結(jié)構(gòu)解析是否正確,驗證car信息的正確性和兼容性。這可能需要用到"Synalyze It! Pro",還需要查閱大量資料、做大量實(shí)驗、使用不同版本的Xcode編譯出car驗證我們的解析正確性。
最終,我們創(chuàng)造性地實(shí)現(xiàn)了在iOS平臺上對car文件中所有的圖片、顏色、文檔等資源和其他附加信息的讀取,此前國內(nèi)外都沒有公開資料顯示有哪個團(tuán)隊實(shí)現(xiàn)過這個完整的過程(當(dāng)然除了蘋果??)。新的換膚方案配合框架的讀取資源API,節(jié)省了大量開發(fā)成本。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容