记一次苦逼的资源逆向分析

上一篇文章和大家讨论了Instant Run的机制,并且总结了它和现有的hot patch框架的优缺点,现在回过头去看那篇文章结尾处的“结合二者的优点,打造一个官方版的hot patch”这句话,脑子里只有一个念头,还是太年轻了啊!!

为什么这么说呢?其实现有的hot patch框架对代码patch这一块做的已经是非常不错了,重点要改进的就是资源patch,但是在后来的研究中发现,Instant Run的资源patch是全量下发的,也就是说每次patch的时候,整个app的资源都会被打进补丁中,这对于Instant Run来说是无所谓的,但是对于我们的hot patch来说就没办法接受了,因为这样会使我们的patch包变得异常的大,从kb级别变成了mb级别。

那怎么办呢,海口已经夸下了,总不能就这么放弃吧,于是我开始了苦逼的资源逆向,阅读并改造aapt源码的过程。

友情提示,本篇文章可能会引起大家的胃部不适,因为所有的代码都是C++的。

参考文章

Android应用程序资源的编译和打包过程分析

Android逆向之旅—解析编译之后的Resource.arsc文件格式

Gradle AaptOptions

何为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文件结构,还是比较清晰的。

资源打包过程总结

好了,最后让我们来总结一下整个资源打包过程吧。

flow

可能的改造点

最后,让我们来讨论一下可能的改造点。大家应该没有忘记我们想要达到什么效果吧?[如何通过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)是腾讯超级补丁的方案。由于下个星期我要回学校完成毕业论文和答辩的相关事宜,希望等我回去的时候公司里的大牛前辈已经把设想变成了现实~