.
.car
文件是苹果.xcassets
文件夹中的资源编译后生成的,会以Assets.car
的名称打包进应用的安装包中。这篇文章中我们将分析car文件的文件结构,并讨论如何将car文件中的颜色、图片、pdf、文档等
资源解析出来。
原创文章,如需转载请在下面留言让我知道😊。不留言不在开头标明出处链接的坏同学,1字1元索赔😡
背景.
方案引入
公司中现有换肤机制要花费大量时间对软件进行改造,具体方案就不介绍了,总之大家都吐槽不好用😤。为了能尽量贴近苹果官方推荐的资源管理方式、减少开发者学习成本、使用官方的优化方案,最开始的出发点是要使用Asset Catalogs管理资源。Asset Catalogs管理方式在Xcode工程中最常接触到的就是名为“Assets.xcassets”的文件夹,创建工程时默认就会创建这个文件夹,在Xcode中可以使用图形界面很方便地管理资源文件,如下图所示:
一般大家会在xcassets中放置应用的图标、UI切图,从iOS 11开始,xcassets中还可以放置颜色信息。其实还可以在其中放置PDF、纹理、Data等数据,只是平时很少用到,而且换肤需求也不要求涉及这些数据,所以我们只要关心切图和颜色就可以了。如果能够把颜色、UI切图、图标这些资源在编译时动态替换为新的资源,即可满足现在行业的需求。幸运的是,xcassets中的资源不仅能在编译时替换,甚至可以在运行时,通过从不同的bundle中读取资源达到动态换肤的目的,这大大增加了这种方案的可扩展性。
遇到的问题
众所周知,xcassets中的资源会被编译为car格式的文件,保存于App或framework的包中。编译过程中会做图片压缩等优化工作,虽然这些是我们想要的,但同时也产生一个问题,那就是必须通过系统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
而我们的应用至少要兼容3个最新版本的iOS系统,目前最新系统是iOS 12,也就是说从iOS 10的系统没法读取出颜色信息。为了解决这个问题,我们需要自己实现读取car文件中颜色信息的逻辑,而关于car文件的格式,苹果是没有公开说明文档的,这就是我们要攻克的最大的难题。
解析car文件
car究竟是什么
为了避免重复造轮子,最先想到的方案就是使用第三方的框架实现解析功能。但是过程不那么顺利,Github上开源工具有很多,但全都是在macOS中解析car文件,而且没有一个是真正解析car的,都是通过iOS或者macOS系统提供的库实现的。如果能用系统库,我们也就不用解析car了。
之后在公司内部找了一些比较资深的iOS开发者,咨询了一圈,发现也没有人做过这个事情。百度也没有找到相关的内容,一度差点否决了这个方案。最终经过大量查找,在Wikipedia中发现了蛛丝马迹,car文件的结构是BOM!马上用"Synalyze It! Pro"应用分析一下car的内容,发现前8个字节真的是“BOMStore”,如下图所示:
简单介绍下BOM文件格式,BOM是“Bill of Materials”的缩写。之前被用于macOS的应用安装器中,用于标识哪些文件需要安装、哪些需要移除或者升级。具体介绍可以参考这个链接https://en.wikipedia.org/wiki/BOM_(file_format)
幸好BOM文件已经在macOS中用了很多年,虽然官方没有文档,但内部结构有人尝试逆向过。BOM只定义了一种存储信息的
树状结构
,并没有规定树中存储的数据是什么样的。分析出BOM数据之后,还要分析出颜色信息是以什么格式储存在BOM结构中的,这篇文章Reverse engineering the .car file format (compiled Asset Catalogs)介绍了car中的信息,但使用了macOS中的BOM.framework解析BOM中的数据,iOS中并没有这个框架。我们要结合上面的文章和之前开发者分析出的BOM大致结构,解析出car中指定名称的颜色数据。
BOM结构
感谢PureDarwin在Github上的开源项目osxbom。虽然这个项目是为了解析macOS的应用安装器中的数据,但是BOM的头、树中的索引和节点等数据结构和解析方法都很有帮助。下面大致介绍一下BOM结构,给大家一些启发。
BOM文件的最开头,是头数据,相信大家看一下osxbom中的结构体就明白了:
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
后面多少个字节地址偏移处。这里有一个新概念就是索引表,索引表可以根据一个(索引)数字找到BOM文件中对应的地址偏移。有了索引表,只要给出一个很小的(索引)数字,就可以跳转到BOM中的任意位置读取数据。索引表的结构比较简单,看下面的结构体就可以理解了:
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));
索引数字如果是3,就找到index[3]
结构体,其中就存储着地址偏移和数据块的长度。
- varsOffset
BOMHeader
中还有个重要的信息是varsOffset
,它表示BOM中的每一棵树的名称、数据位置的索引数字的表,在BOMHeader
后面多少个字节偏移处,结构如下所示:
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
,判断名称是否和我们要的一致,如果一致则根据BOMVar
中的index
到索引表
中查找到对应的地址偏移即可取出这棵树的数据。
- 其他
上面已经介绍了BOM中的主要内容,关系有点绕,但是也很精妙,可以体会一下设计BOM结构的人思维方式。总之,有了上面的基础知识,就可以根据树的名称拿到具体的数据块了。其实每棵树的数据块的结构设计,也是相当巧妙的,有兴趣的同事可以看下osxbom源码自行分析,这里由于篇幅原因不再赘述。
car信息
上面已经提到,car数据的结构在Reverse engineering the .car file format (compiled Asset Catalogs)博客中已经有比较详细的描述。
几乎所有图片、颜色等信息都存储在名为RENDITIONS
的树中,树中每个节点的数据都是下面这个结构所示的结构:
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));
看结构体已经非常明确了,csimetadata
中存储着当前节点数据的类型(比如图片、颜色、PDF),还有数据的名称(比如图片名、颜色名、PDF文件名)。遍历树中每个节点,找到希望获取的颜色类型节点,并且颜色名和希望获取的一致,剩下的就是去除颜色数据即可。其他类型的数据在博客中也都提到,有兴趣的同事可以自己研究一下,下面我就以解析颜色数据为例。
通过csiheader
里面csibitmaplist
中的数据偏移等信息,可以找到颜色信息存储的具体位置(是的,这个名字看起来很像图片,因为早期car中只能存储图片)。
到这里我们就获取到了颜色信息的数据,它的结构如下所示:
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));
上面的结构体名称是csicolor
,因为这是颜色信息的结构体,其他类型数据有对应的结构体。我们可以看到,其中有颜色空间colorSpaceID
,它表示颜色使用的SRGB还是灰度等等。组件数量numberOfComponents
和组件components
,表示的是某种颜色空间中的不同组件,比如SRGB中的红、绿、蓝、透明通道的亮度值,或者灰度颜色中的亮度、透明度值。
总结
至此,我们就把car文件中的颜色信息全部读取出来了。篇幅有限,有很多细节没有罗列。这其中也确实有大量的工作要做,我们要分析、验证BOM结构解析是否正确,验证car信息的正确性和兼容性。这可能需要用到"Synalyze It! Pro",还需要查阅大量资料、做大量实验、使用不同版本的Xcode编译出car验证我们的解析正确性。
最终,我们创造性地实现了在iOS平台上对car文件中所有的图片、颜色、文档等资源和其他附加信息的读取,此前国内外都没有公开资料显示有哪个团队实现过这个完整的过程(当然除了苹果😊)。新的换肤方案配合框架的读取资源API,节省了大量开发成本。