为什么这么说呢?其实现有的hot patch框架对代码patch这一块做的已经是非常不错了,重点要改进的就是资源patch,但是在后来的研究中发现,Instant Run的资源patch是全量下发的,也就是说每次patch的时候,整个app的资源都会被打进补丁中,这对于Instant Run来说是无所谓的,但是对于我们的hot patch来说就没办法接受了,因为这样会使我们的patch包变得异常的大,从kb级别变成了mb级别。
那怎么办呢,海口已经夸下了,总不能就这么放弃吧,于是我开始了苦逼的资源逆向,阅读并改造aapt源码的过程。
友情提示,本篇文章可能会引起大家的胃部不适,因为所有的代码都是C++的。
何为aapt,ap_ 和arsc
在开始了解我的这次苦逼之旅之前,大家需要对一些基础知识有一定的了解。
大家都知道我们写的代码不是凭空就变成一个Android应用的,当你点击了Android Studio的run之后,会经过一些列的gradle task,才会生成一个可以在手机上安装并且运行的apk文件。aapt的作用就是在特定的task中,处理我们Android工程中的资源文件并且打包进apk文件的这么一个工具,它位于我们Android sdk文件夹下的build-tools目录。
而在经过aapt处理之后,我们的资源文件,当然也包括manifest文件都会被打包成一个压缩包,名字叫做resources.ap_,你可以在对应的Android工程的build/intermediates/res路径下找到它。如果你去对它进行解压缩,你会看到它里面包含了一个manifest文件,一个res文件夹和一个arsc文件,当然如果你有assets文件的也会被包含在里面。
arsc文件又是什么呢?大家可以把它看作一张资源索引表,它和我们的R文件是相关联的,也就是说我们在代码中调用了R.xxx.xxx的时候,都会去这个arsc文件中通过对应的id去索引资源。它会被我们Java层的AssetManager加载,如果有过插件化开发经验的同学应该知道,是通过addAssetPath这个方法去加载的。
说完了三个概念,我们这里要讨论的是,怎样才能完成资源的增量下发,比如我们在drawable目录下添加了一张图片patch.png,在main.layout中应用了这张图片,我们所要做的就是得到这两个改变的文件,通过aapt去生成[只包含这两个资源的ap_文件]并且打到patch包中下发。
那么这里的难点是什么?显然找出改变的资源不是难点,因为可以通过md5文件的差异去做,和代码的patch是一样的。真正的难点在于[如何通过aapt的生成正确的arsc文件]。如果你不去改造aapt的话,就算你的ap_压缩包中的res文件夹下只有改变过的资源,但是arsc索引表中还是会包含所有的资源。这里我举个例子:
原来的app中的资源为a.png,main.layou和second.layout。这个时候你新增了一个patch.png并且修改了main.layout。通过分析md5文件将patch.png和main.layout加入到了ap_的res中,但是你的arsc文件中包含的是a.png,patch.png,main.layou和second.layout四个文件。那么当你的程序去通过arsc找a.png或者second.layout的时候,就会报出resource not found的错误。
所以我们的重点应该放在[如何改造aapt]上。
不过这篇文章的主要精力会放在分析上,因为[改造的部分]我和小伙伴们还在编码当中,争取早日完成吧~
ResourceTable及其他
在整个资源打包过程中,有一个重要的类不得不提,那就是ResouceTable类。这个类的作用是用来保存我们解析得到的资源索引,并且将其最终转换成对应的arsc文件。
对应的代码在ResouceTable.cpp中。我们来看看它头文件内部的几个重要的成员变量:
1 2 3 4 5 | String16 mAssetsPackage; PackageType mPackageType; sp<AaptAssets> mAssets; DefaultKeyedVector<String16, sp<Package> > mPackages; Vector<sp<Package> > mOrderedPackages; |
mAssetsPackage:表示资源的包名,一般来说就是app的包名。
mPackageType:资源的类型,用来定义R文件中资源id的高位。类型有APP,AppFeature,对应高位是0x7f;System,对应高位是0x01;SharedLibrary,对应高位是0x00。也就是说我们在不改造aapt的app的R文件中,只会存在这三种类型的id,大家可以自行去自己应用的R文件中求证。
mAssets:一个AaptAssets对象,用来表示当前资源目录。
mPackages:表示资源包,用Package来定义。存储在一个以PackageName为key的Vector中。
mOrderedPackages:和mPackages一样,只不过这个是有序的,存储在Vector中。
而在Package中,有一个类叫Type,用来表示资源的类型,比如drawable,layout或者color等等。
在Type中,存在一个ConfigList对象,用来描述同一类型资源的同名资源。比如在drawable-xhdpi和drawable-xxhdpi中都有一张drawable.png图片,那么只会存在一个名为drawable.png的ConfigList。
在ConfigList中存在一个Entry对象,用来表示一个具体的资源。还是说上面这个例子,在drawable.png这个ConfigList中会存在两个Entry,分别叫做res/drawable-xhdpi/drawable.png和res/drawable-xxhdpi/drawable.png。
至此,我们已经知道了ResourceTable中到底含有什么,通过Type->ConfigList->Entry的形式储存了资源。
而前面提到了AaptAssets的作用就是用来收集资源,并且将其添加到ResourceTable中。所以资源索引的转换大体上是raw resource->AaptAssets->ResourceTable->arsc file。
arsc文件结构
最后的准备工作就让我们来了解一下arsc的文件结构吧。具体对应的代码在ResourceType.h中。注意这个类是不在aapt对应的源码中的,而在android /platform / frameworks / base / master / include / androidfw目录下。
首先,arsc文件是由chunk组成的,每一部分对应一个chunk。而每一个chunk中都有一个固定的结构叫做ResChunk_header。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | struct ResChunk_header { // Type identifier for this chunk. The meaning of this value depends // on the containing chunk. uint16_t type; // Size of the chunk header (in bytes). Adding this value to // the address of the chunk allows you to find its associated data // (if any). uint16_t headerSize; // Total size of this chunk (in bytes). This is the chunkSize plus // the size of any data associated with the chunk. Adding this value // to the chunk allows you to completely skip its contents (including // any child chunks). If this value is the same as chunkSize, there is // no data associated with the chunk. uint32_t size; }; |
type表示当前chunk的类型。
headerSize表示当前chunk头部的大小。
size表示当前chunk的大小。
在这儿之后,就是每一个chunk不同的部分。
首先第一个是ResTable_package,表示资源项元信息数据块头部。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | struct ResTable_package { struct ResChunk_header header; // If this is a base package, its ID. Package IDs start // at 1 (corresponding to the value of the package bits in a // resource identifier). 0 means this is not a base package. uint32_t id; // Actual name of this package, \0-terminated. uint16_t name[128]; // Offset to a ResStringPool_header defining the resource // type symbol table. If zero, this package is inheriting from // another base package (overriding specific values in it). uint32_t typeStrings; // Last index into typeStrings that is for public use by others. uint32_t lastPublicType; // Offset to a ResStringPool_header defining the resource // key symbol table. If zero, this package is inheriting from // another base package (overriding specific values in it). uint32_t keyStrings; // Last index into keyStrings that is for public use by others. uint32_t lastPublicKey; uint32_t typeIdOffset; }; |
id表示package id。
name表示包名。
typeStrings表示类型字符串资源池相对头部的偏移。
lastPublicType表示最后一个导出的Public类型字符串在类型字符串资源池中的索引,目前这个值设置为类型字符串资源池的大小。
keyStrings表示资源项名称字符串相对头部的偏移。
lastPublicKey表示最后一个导出的Public资源项名称字符串在资源项名称字符串资源池中的索引,目前这个值设置为资源项名称字符串资源池的大小。
typeIdOffset表示类型id的偏移。
接着是ResTable_typeSpec,表示类型规范数据块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | struct ResTable_typeSpec { struct ResChunk_header header; // The type identifier this chunk is holding. Type IDs start // at 1 (corresponding to the value of the type bits in a // resource identifier). 0 is invalid. uint8_t id; // Must be 0. uint8_t res0; // Must be 0. uint16_t res1; // Number of uint32_t entry configuration masks that follow. uint32_t entryCount; enum { // Additional flag indicating an entry is public. SPEC_PUBLIC = 0x40000000 }; }; |
id表示资源类型,比如drawable,layout等等。
res0和res1都为0,保留以后使用。
entryCount表示Entry的个数。
接着是ResTable_type,表示类型资源项数据块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | struct ResTable_type { struct ResChunk_header header; enum { NO_ENTRY = 0xFFFFFFFF }; // The type identifier this chunk is holding. Type IDs start // at 1 (corresponding to the value of the type bits in a // resource identifier). 0 is invalid. uint8_t id; // Must be 0. uint8_t res0; // Must be 0. uint16_t res1; // Number of uint32_t entry indices that follow. uint32_t entryCount; // Offset from header where ResTable_entry data starts. uint32_t entriesStart; // Configuration this collection of entries is designed for. ResTable_config config; }; |
id表示资源TypeId。
res0和res1都为0,保留以后使用。
entryCount表示Entry的个数。
entriesStart表示资源项数据块相对头部的偏移值。
config表示一个Configuration,比如xxhdpi等,对应前面说的ConfigList。
对于这一小节,我只分析了上面三个部分,因为这三个部分在后面的代码中是最显而易见的。其实整个arsc文件结构远远不止这些,大家可以去上面我给出的参考文章,写的非常清楚。
资源打包过程源码分析
这里就是这篇文章的重点了,资源的打包过程。
首先,aapt的打包命令是p,所以让我们看看main.cpp中对应的命令:
1 2 | else if (argv[1][0] == 'p') bundle.setCommand(kCommandPackage); |
可以看到setConmmand是kCommandPackage,那么对应的执行函数是什么呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | int handleCommand(Bundle* bundle) { switch (bundle->getCommand()) { case kCommandVersion: return doVersion(bundle); case kCommandList: return doList(bundle); case kCommandDump: return doDump(bundle); case kCommandAdd: return doAdd(bundle); case kCommandRemove: return doRemove(bundle); case kCommandPackage: return doPackage(bundle); case kCommandCrunch: return doCrunch(bundle); case kCommandSingleCrunch: return doSingleCrunch(bundle); case kCommandDaemon: return runInDaemonMode(bundle); default: fprintf(stderr, "%s: requested command not yet supported\n", gProgName); return 1; } } |
是doPackage方法,这个方法在Command.cpp中。
而在doPackage中,会调用Resouce.cpp的buildResources方法,这个方法就是我们要重点看的了。由于代码比较长,我们就挑重点地看。
(1) 解析AndroidManifest文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | sp<AaptGroup> androidManifestFile = assets->getFiles().valueFor(String8("AndroidManifest.xml")); if (androidManifestFile == NULL) { fprintf(stderr, "ERROR: No AndroidManifest.xml file found.\n"); return UNKNOWN_ERROR; } status_t err = parsePackage(bundle, assets, androidManifestFile); if (err != NO_ERROR) { return err; } ResourceTable::PackageType packageType = ResourceTable::App; if (bundle->getBuildSharedLibrary()) { packageType = ResourceTable::SharedLibrary; } else if (bundle->getExtending()) { packageType = ResourceTable::System; } else if (!bundle->getFeatureOfPackage().isEmpty()) { packageType = ResourceTable::AppFeature; } ResourceTable table(bundle, String16(assets->getPackage()), packageType); |
为什么要先解析manifest呢?因为我们前面说过,ResouceTable是通过包名来区分的,所以这里我们要先解析manfest去获取包名并且创建一个ResouceTable,并且通过parsePackage去解析一些配置,比如minSdkVersion。
1 2 3 4 5 6 7 8 9 10 11 | if (code == ResXMLTree::START_TAG) { if (strcmp16(block.getElementName(&len), uses_sdk16.string()) == 0) { ssize_t minSdkIndex = block.indexOfAttribute(RESOURCES_ANDROID_NAMESPACE, "minSdkVersion"); if (minSdkIndex >= 0) { const char16_t* minSdk16 = block.getAttributeStringValue(minSdkIndex,&len); const char* minSdk8 = strdup(String8(minSdk16).string()); bundle->setManifestMinSdkVersion(minSdk8); } } } |
(2) 添加系统的资源包。
1 2 3 4 | err = table.addIncludedResources(bundle, assets); if (err != NO_ERROR) { return err; } |
这又是为什么呢?因为我们的app中肯定会用到系统的资源,比如Framelayout,LinearLayout,orientation.Vertical这样的,所以我们要先将其加入到ResouceTable中。
(3) 收集资源文件并且添加到AaptAssets中。
1 2 | eyedVector<String8, sp<ResourceTypeSet>> *resources = new KeyedVector<String8, sp<ResourceTypeSet>>; collect_files(assets, resources); |
可以看到是通过collect_files这个函数去完成的。
我们可以看一下AaptAssets.h这个头文件:
1 2 3 4 5 6 7 8 9 10 | class AaptAssets : public AaptDir { public: AaptAssets(); virtual ~AaptAssets() { delete mRes; } ........ private: KeyedVector<String8, sp<ResourceTypeSet>>* mRes; |
可以看到它有一个成员变量mRes,key是资源类型,value是ResourceTypeSet,里面包含一系列的资源名称,比如上面提到的drawable.png。
综上所述,AaptAssets就是一个用来存储资源的类。
(4) 处理overlay资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 | if (!applyFileOverlay(bundle, assets, &drawables, "drawable") || !applyFileOverlay(bundle, assets, &layouts, "layout") || !applyFileOverlay(bundle, assets, &anims, "anim") || !applyFileOverlay(bundle, assets, &animators, "animator") || !applyFileOverlay(bundle, assets, &interpolators, "interpolator") || !applyFileOverlay(bundle, assets, &transitions, "transition") || !applyFileOverlay(bundle, assets, &xmls, "xml") || !applyFileOverlay(bundle, assets, &raws, "raw") || !applyFileOverlay(bundle, assets, &colors, "color") || !applyFileOverlay(bundle, assets, &menus, "menu") || !applyFileOverlay(bundle, assets, &mipmaps, "mipmap")) { return UNKNOWN_ERROR; } |
overlay的意思是重叠,也就是说如果两个包中有相同的资源,那么后者将会覆盖前者。这里我引用了老罗文章中的一段话:
[假设我们正在编译的是Package-1,这时候我们可以设置另外一个Package-2,用来告诉aapt,如果Package-2定义有和Package-1一样的资源,那么就用定义在Package-2的资源来替换掉定义在Package-1的资源。通过这种Overlay机制,我们就可以对资源进行定制,而又不失一般性。]
(5) 将资源添加到ResourceTable。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | sp<ResourceTypeSet> drawables; sp<ResourceTypeSet> layouts; sp<ResourceTypeSet> anims; sp<ResourceTypeSet> animators; sp<ResourceTypeSet> interpolators; sp<ResourceTypeSet> transitions; sp<ResourceTypeSet> xmls; sp<ResourceTypeSet> raws; sp<ResourceTypeSet> colors; sp<ResourceTypeSet> menus; sp<ResourceTypeSet> mipmaps; ........ if (layouts != NULL) { err = makeFileResources(bundle, assets, &table, layouts, "layout"); if (err != NO_ERROR) { hasErrors = true; } } ......... |
可以看到通过makeFileResources方法将assets(AaptAssets类型)中的资源添加到了table(ResourceTable类型)中。这里我列举了layout的处理,其他几种的处理方式都是一样的。makeFileResource的代码我们这里就不看了,大致过程就是前面提到的创建Package添加到mPackages和mOrderedPackages中,并且按照Type->ConfigList->Entry这样添加资源。
这里要注意的是处理的资源是是不包含values类型的,也就是说values目录的下资源在这里是不会进行处理的。
(6) 处理values资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | current = assets; while(current.get()) { KeyedVector<String8, sp<ResourceTypeSet> > *resources = current->getResources(); ssize_t index = resources->indexOfKey(String8("values")); if (index >= 0) { ResourceDirIterator it(resources->valueAt(index), String8("values")); ssize_t res; while ((res=it.next()) == NO_ERROR) { sp<AaptFile> file = it.getFile(); res = compileResourceFile(bundle, assets, file, it.getParams(),(current!=assets), &table); if (res != NO_ERROR) { hasErrors = true; } } } current = current->getOverlay(); } |
为什么要后处理values资源呢?因为这类型的资源比较特殊,要编译之后才能处理,具体的编译函数是compileResourceFile。
(7) 给bag资源分配id。
1 2 3 4 5 6 7 8 9 10 | // -------------------------------------------------------------------- // Assignment of resource IDs and initial generation of resource table. // -------------------------------------------------------------------- if (table.hasResources()) { err = table.assignResourceIds(); if (err < NO_ERROR) { return err; } } |
通过注释就可以看出它的作用。
至于什么是bag资源,我还是应用老罗博客中的话。
[类型为values的资源除了是string之外,还有其它很多类型的资源,其中有一些比较特殊,如bag、style、plurals和array类的资源。这些资源会给自己定义一些专用的值,这些带有专用值的资源就统称为Bag资源。例如,Android系统提供的android:orientation属性的取值范围{“vertical”、“horizontal”},就相当于是定义了vertical和horizontal两个Bag]。
(8) 编译xml资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | if (layouts != NULL) { ResourceDirIterator it(layouts, String8("layout")); while ((err=it.next()) == NO_ERROR) { String8 src = it.getFile()->getPrintableSource(); err = compileXmlFile(bundle, assets, String16(it.getBaseName()), it.getFile(), &table, xmlFlags); if (err == NO_ERROR) { ResXMLTree block; block.setTo(it.getFile()->getData(), it.getFile()->getSize(), true); checkForIds(src, block); } else { hasErrors = true; } } if (err < NO_ERROR) { hasErrors = true; } err = NO_ERROR; } |
这里我列举layout资源,类似的还有anim这样的也是一样,通过compileXmlFile方法去完成。
为什么要在最后处理xml呢?因为在xml中可能会引用了我们前面处理的资源,所以必须要保证它们已经处理过了,才能继续处理xml。
(9) 生成符号表。
1 2 3 4 5 | sp<AaptSymbols> symbols = assets->getSymbolsFor(String8("R")); err = table.addSymbols(symbols, bundle>getSkipSymbolsWithoutDefaultLocalization()); if (err < NO_ERROR) { return err; } |
这里的符号表就是那些0x01,0x7f,最终要写到R文件中。
(10) 生成arsc文件。
准备好了一切,最后就是生成arsc文件了,具体的逻辑在ResourceTable.cpp的flatten方法中。这个方法非常长,有差不多450行,这里我就不一一讲了,具体过程大家可以通过查看源码并且配合上我说的arsc文件结构,还是比较清晰的。
资源打包过程总结
好了,最后让我们来总结一下整个资源打包过程吧。
可能的改造点
最后,让我们来讨论一下可能的改造点。大家应该没有忘记我们想要达到什么效果吧?[如何通过aapt的生成正确的arsc文件]。这里我列举几个我觉得可能的改造点:
(1) 我们可以在步骤9[生成符号表]和步骤10[生成arsc文件]之间做手脚,动态的去踢掉那些没有进行改变的资源。我们可以通过传一个文件,里面保存着改变过的资源,然后在步骤9和步骤10之间对照这份文件把ResouceTable中没有改变过的资源踢掉,这样一来由于符号表已经生成,所以R文件中的id不会改变,但是arsc文件中由于ResourceTable中已经被我们踢掉了一部分资源,所以剩下的索引都是改变过的了。
(2) 在aapt中存在–split这样一个命令。在gradle中这样配置:
1 2 3 4 5 6 | android { ...... aaptOptions { additionalParameters "--split","xhdpi" } } |
这样在对应的build/intermediates/res路径下就会存在两份ap_文件,一份为主ap,另外一份ap文件中只会存在xhdpi目录下的资源。
我们可以改造aapt,去实现这样的命令:
1 2 3 4 5 6 | android { ...... aaptOptions { additionalParameters "--split","patch" } } |
然后把对应的文件放入patch文件夹中,比如drawable-patch。
(3) 我们都知道Android的系统资源是不会被加到arsc文件中的,具体的aapt命令为-I,那我们是不是也可以通过再造aapt,去把我们没有修改过的资源也像系统资源一样不添加到arsc中呢?
(4) 我们甚至可以绕开aapt,不用它转而自己去生成arsc文件,主要的逻辑都是ResourceTable.cpp的flatten方法中,可以照葫芦画瓢嘛。
以上四种方案还只是设想,其中(1)方案是公司前辈和我讨论出来的,(2)(3)方案是区长想出来的,(4)是腾讯超级补丁的方案。由于下个星期我要回学校完成毕业论文和答辩的相关事宜,希望等我回去的时候公司里的大牛前辈已经把设想变成了现实~