前面我们讲过了资源共享库的概念和应用,现在我们来看看它是怎么实现的吧,顺便也能了解一下Android的资源管理中的一些机制。
这里面包括了两部分:资源共享库的编译和使用这个库的App的编译。我们仍旧以上一篇文章中framework里的那个项目为例来分别讨论。那个资源共享库的包名为:com.google.android.test.shared_library,项目路径为frameworks/base/tests/SharedLibrary/lib;引用这个资源共享库的App包名为:com.google.android.test.lib_client, 项目路径为:frameworks/base/tests/SharedLibrary/client。
我们看到,在其Android.mk中,有这么一行:LOCAL_AAPT_FLAGS := --shared-lib。这行的作用就是告诉aapt,在编译的时候,把当前项目当做一个资源共享库来处理,我们可以认为aapt执行了如下命令:aapt package -f --shared-lib -J ~ -S frameworks/base/tests/SharedLibrary/lib/res/ -I ~/android-sdk-linux/platforms/android-22/android.jar -M frameworks/base/tests/SharedLibrary/lib/AndroidManifest.xml -F ~/lib-out.apk
对这个命令稍作说明:
aapt package 表示打包,也就是要编译我们的资源共享库
-f 表示强制覆盖已经存在的文件
--shared-lib 表示我们要编译资源共享库,而不是普通的APK
-J指定生成的R.java的路径,我们为了方便就把它放在home下面了
-S指定我们要编译的资源路径,当然就是res目录了
-I指定编译的依赖库,当然就是SDK里面的android.jar了
-M指定我们的AndroidManifest.xml文件
-F编译后的out文件路径
需要说明的是,-I指定编译时的依赖库为android.jar。可能大家会觉得奇怪,我们不是编译资源吗,怎么会依赖一个jar包?其实如果我们解压android.jar,就会发现里面不仅有class文件,还有很多资源。也就是说,android.jar不仅是一个java库,也是一个资源库。这里我们完全可以猜想,在aapt编译并生成android.jar里的资源的时候,肯定不用 -I参数,因为android.jar是不会依赖其它资源了的。
比较有意思的是,我们可以把android.jar里的东西删得只剩下AndroidManifest.xml和resources.arsc,然后重新执行上面的那条命令,仍然可以编译成功,为什么呢,我们不是依赖android.jar里的资源吗,为啥都删除了还能编译成功呢?其实android.jar作为我们依赖的资源库,编译的时候aapt和AssetManger只会检查其AndroidManifest.xml文件,然后加载resources.arsc这个资源索引表,至于其它文件一律忽略。因为resources.arsc已经包含了android.jar里的所有资源的信息了。这个我们在后面介绍aapt生成resources.arsc的过程和AssetManager的时候会详细介绍。
关于android.jar就说到这里,aapt在编译资源共享库和一般APK时,到底会有什么不同的处理呢?这个我们就得看aapt的代码了,其路径为:framework/base/tools/aapt/。 GO!
打开Main.cpp,直奔其main函数:
我们看到,它会把相关信息存进Bundle类的一个实例里,Bundle主要用来存储我们执行aapt命令时的参数的。它有两个动作:设置BuildSharedLibrary为true很好理解,我们要编译资源共享库嘛。但这个setNonConstantId是要干啥?我们知道一般应用程序的R.java里面的资源id都是public static final的,难不成它要玩出什么花活儿?打开我们刚才执行aapt 命令生成的那个R.java瞧瞧:
好家伙,果然资源的id都不是final的了,为什么去掉了呢?final的不能改,难不成,这些资源的id还会动态改变?这个疑问我们暂且按下不表。我们继续往下看,发现R.java还多了一个方法:
果然,这个方法会改变我们资源的包id!为什么要改变呢?既然我们不清楚,那就继续看代码吧。
Command.cpp负责aapt具体命令的逻辑,我们看它的doPackage方法:
/*
* Package up an asset directory and associated application files.
*/
int doPackage(Bundle* bundle)
{
//do something
// If they asked for any fileAs that need to be compiled, do so.
if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {
err = buildResources(bundle, assets, builder); //1 build resources
if (err != 0) {
goto bail;
}
}
//do some thing
// Write out R.java constants
if (!assets->havePrivateSymbols()) {
if (bundle->getCustomPackage() == NULL) {
// Write the R.java file into the appropriate class directory
// e.g. gen/com/foo/app/R.java
err = writeResourceSymbols(bundle, assets, assets->getPackage(), true,
bundle->getBuildSharedLibrary());
} else {
const String8 customPkg(bundle->getCustomPackage());
err = writeResourceSymbols(bundle, assets, customPkg, true,
bundle->getBuildSharedLibrary());//2,生成R.java文件,最后一个参数为 true
}
if (err < 0) {
goto bail;
}
}
//do something
}
标红的两处代码比较关键,1是负责资源的编译,2是R.java文件的生成。我们先看buildResources()的实现,在Resource.cpp中:
status_t buildResources(Bundle* bundle, const sp
{
// First, look for a package file to parse. This is required to
// be able to generate the resource information.
sp
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;
}
NOISY(printf("Creating resources for package %s\n",
assets->getPackage().string()));
ResourceTable::PackageType packageType = ResourceTable::App;
if (bundle->getBuildSharedLibrary()) { //3 确定我们要编译的是资源共享库
packageType = ResourceTable::SharedLibrary;
} else if (bundle->getExtending()) {
packageType = ResourceTable::System;
} else if (!bundle->getFeatureOfPackage().isEmpty()) {
packageType = ResourceTable::AppFeature;
}
//4 创键ResourceTable,要编译的包的类型为 ResourceTable::SharedLibrary;
ResourceTable table(bundle, String16(assets->getPackage()), packageType);
//..........省略无关代码
}
我们要编译的包的Type有四种:
它们被定义在ResourceTable.h中,分别对应一般的App资源包、系统资源包、资源共享库、最后一种是APK 分片相关的。
OK,我们继续看看ResourceTable的构造函数:
一目了然,只要我们指定了--shared-lib选项,我们要编译的包Id就会被设置为0x00。另外,一般应用及其分片都会被设置成0x7f,系统资源包的id则被设置为0x01。另外多说一些,一般各个SOC厂商也会有自己的系统资源包,比如这个mtk的是0x08;手机厂商也会加入自己的系统资源包,比如我们的是0x09。这样,一个App运行起来,它内部至少会加载四个资源包:Google源生资源包、SOC厂商资源包、手机厂商资源包、以及这个App本身也是一个资源包。
以前我一直有个疑问,App的包id都是0x7f,那么系统怎么区分哪个应用对应哪个包呢?其实,包id的作用不是用来这么区分不同的应用的,不同的应用是根据包的不同路径来区分的。那么包id是用来干什么的呢?包id是用来区分同一进程,确切地说是同一个AssetManager中的不同包的。比如,Android源生资源包、SOC厂商的资源包、手机厂商的资源包、资源共享包、以及应用本身这个资源包,它们会被加载到同一个AssetManager中,包id是用来区分它们的。至此,资源共享库的包id怎么来的我们已经清楚了。
我们回过头来继续分析R.java的生成,看看加了--shared-lib选项之后,会有什么特殊的处理:
status_t writeResourceSymbols(Bundle* bundle, const sp
const String8& package, bool includePrivate, bool emitCallback)//5 emitCallback 就是我们传过来的buildSharedLibrary,true
{
{
.....
//构建R.java的路径,并创建打开这个文件为fp
.....
}
//5 emitCallback 就是我们传过来的buildSharedLibrary,true
status_t err = writeSymbolClass(fp, assets, includePrivate, symbols,
className, 0, bundle->getNonConstantId(), emitCallback);
}
继续看
static status_t writeSymbolClass(
FILE* fp, const sp
const sp
bool nonConstantId, bool emitCallback)
{
{
//写入R文件的各个类,变量,非本文重点,后面会有专门文章分析
}
if (emitCallback) {//true
//开始写入onResourcesLoaded方法了
fprintf(fp, "%spublic static void onResourcesLoaded(int packageId) {\n",
getIndentSpace(indent));
//这个方法会递归地写入每一项,这里不再展开。
writeResourceLoadedCallback(fp, assets, includePrivate, symbols, className, indent + 1);
fprintf(fp, "%s}\n", getIndentSpace(indent));
}
}
通过以上分析,我们确认了,在编译资源共享包时:
1.资源的包id会被指定成0x00;
2.R.java中的每个id不再是final的,并且还会生成一个onResourcesLoaded方法,它会修改每一个资源的包id。
但我们也应该有两个疑问:
1.Android源生资源包占了id 0x01,SOC厂商资源包占了0x08,手机厂商占了后面的一个,应用自己占了0x7f,那么剩下的呢?特别是0x02~0x07之间的id给谁用呢?
2.资源共享库的R.java中的这个onResourcesLoaded方法为啥要改变包id呢?
带着这两个疑问,我们继续分析引用这个资源共享包的App的编译过程
先看它的Android.mk文件:
我们看到它也有一行比较特殊:
LOCAL_RES_LIBRARIES := SharedLibrary
这一行有啥效果呢,我们可以把它翻译成aapt命令:
aapt package -f -J ~ -S frameworks/base/tests/SharedLibrary/client/res/ -I ~/android-sdk-linux/platforms/android-22/android.jar
-I ~/lib-out.apk -M frameworks/base/tests/SharedLibrary/client/AndroidManifest.xml -F ~/app-out.apk
也就是说,加了一个 -I 参数,它的值为我们刚才编译出来的资源共享库的路径,我们之前编译出来的资源共享库的地位现在和android.jar一样了,将会作为一个系统库,参与App的编译。这时候我们的App com.google.android.test.lib_client的编译和一般的App相比,只有一处不一样了,这处不同在生成resources.arsc的时候。我们来简单看下:
aapt在编译资源的时候会调到这个方法:
status_t ResourceTable::flatten(Bundle* bundle, const sp
const sp
const bool isBase)
{
// The libraries this table references.
Vector
//获取我们引用的资源包,这里是android源生包和我们的资源共享包,共2个
const ResTable& table = mAssets->getIncludedResources();
//basePackageCount = 2
const size_t basePackageCount = table.getBasePackageCount();
for (size_t i = 0; i < basePackageCount; i++) {
size_t packageId = table.getBasePackageId(i);
String16 packageName(table.getBasePackageName(i));
//过滤掉Android源生包,因为它的包id是固定的,
//libraryPackages里只有我们的资源共享包了
if (packageId > 0x01 && packageId != 0x7f &&
packageName != String16("android")) {
//libraryPackages size = =1;
libraryPackages.add(sp
}
}
........
//收集value字符串,并将他们写入全局字符串池中
........
//写入包信息、type string pool、key string pool
........
if (isBase) {
//这里会写入我们的App引用的资源库的信息
status_t err = flattenLibraryTable(data, libraryPackages);
if (err != NO_ERROR) {
fprintf(stderr, "ERROR: failed to write library table\n");
return err;
}
}
......
//写入所有type、entry
}
继续进入flattenLibraryTable()方法:
status_t ResourceTable::flattenLibraryTable(const sp
// Write out the library table if necessary
if (libs.size() > 0) {
NOISY(fprintf(stderr, "Writing library reference table\n"));
const size_t libStart = dest->getSize();
const size_t count = libs.size();
//如果有必要,会重新分配内存
ResTable_lib_header* libHeader = (ResTable_lib_header*) dest->editDataInRange(
libStart, sizeof(ResTable_lib_header));
//写入ResTable_lib_header,它的定义在 AssetManager部分的ResourceTypes.h中
//最主要的信息就是引用了多少个资源库
memset(libHeader, 0, sizeof(*libHeader));
libHeader->header.type = htods(RES_TABLE_LIBRARY_TYPE);
libHeader->header.headerSize = htods(sizeof(*libHeader));
libHeader->header.size = htodl(sizeof(*libHeader) + (sizeof(ResTable_lib_entry) * count));
libHeader->count = htodl(count);
// Write the library entries
for (size_t i = 0; i < count; i++) {
const size_t entryStart = dest->getSize();
sp
NOISY(fprintf(stderr, " Entry %s -> 0x%02x\n",
String8(libPackage->getName()).string(),
(uint8_t)libPackage->getAssignedId()));
ResTable_lib_entry* entry = (ResTable_lib_entry*) dest->editDataInRange(
entryStart, sizeof(ResTable_lib_entry));
memset(entry, 0, sizeof(*entry));
//6,这里需要特别说明,非常重要!!!
entry->packageId = htodl(libPackage->getAssignedId());
strcpy16_htod(entry->packageName, libPackage->getName().string());
}
}
return NO_ERROR;
}
这个函数,其实就是写入正在编译的这个资源包都引用了哪些资源包,写入他们的name,和id。But!!!
并没有这么简单,//6处已经特别标明。这里的变量libs是怎么来的,我们再复习一遍:
// The libraries this table references.
Vector
const ResTable& table = mAssets->getIncludedResources();
const size_t basePackageCount = table.getBasePackageCount();
for (size_t i = 0; i < basePackageCount; i++) {
size_t packageId = table.getBasePackageId(i);
String16 packageName(table.getBasePackageName(i));
if (packageId > 0x01 && packageId != 0x7f &&
packageName != String16("android")) {
libraryPackages.add(sp
}
}
重点看标红的这两行,packageId是从ResTable实例中获取的,此时的ResTable中,会有两个资源包,一个是Android源生的,id为0x01;另一个是我们的共享资源包,packageId 是0x00;是这样,没错吧!?
错!错!错!
我们的共享资源包,这里拿到的packageId是0x02,是0x02,是0x02!!!
为什么呢?不是说好的我们的共享包的id是0x00吗?!!!
不按套路出牌!不按套路出牌!不按套路出牌!
这里就要说ResTable这个类了,它是AssetManager中最重要的一个类,没有之一。我们编译App资源时候的依赖库,都会被加入到它里面去。假设我们要编译的App有三个依赖的共享库他们分别是 libaa.apk、lib11.apk、libαα.apk,它们的包id都是0x00,android源生资源库是 android.jar,它的包id是0x01。并且我们编译的时候aapt 命令中这样写:aapt package -f -I libαα.apk -I android.jar -I lib11.apk -I libaa.apk .......
请注意这四个-I参数的顺序。
那么当这个ResTable去加载这些资源包的时候,也会按照这个顺序来加载:
先加载libαα.apk,一看,它的包id是0x00,马上就明白了,这是个资源共享库,于是拿出小本本写下:0x00----->libαα.apk
但它转念一想,不对,如果后面还有许多个资源共享库,他们的包id也是0x00,我咋弄,到时后给我个0x00,我怎么找到是哪个包?不行,必须区分它们。既然0x01给google了,那资源共享库就从0x02开始,一个一个增长吧,于是,它的小本本最终如下:
0x02----->libαα.apk
0x03----->lib11.apk
0x04----->libaa.apk
以上只是个例子,由于我们这次编译,依赖的资源库只有android.jar和一个共享库,所以我们拿到的共享库的id一定是0x02。并且这个0x02----->com.google.android.test.shared_library会被写入到resources.arsc中。
libaa.apk、lib11.apk、libαα.apk 它们三个看着0x02----->com.google.android.test.shared_library被写入到resources.arsc,心中一定在等着看ResTable的笑话,你这样乱弹琵琶,就等着出问题吧。
aapt的处理,到此已经结束,最后再来回忆一下我们的疑问:
1.Android源生资源包占了id 0x01,SOC厂商资源包占了0x08,手机厂商占了后面的一个,应用自己占了0x7f,那么剩下的呢?特别是0x02~0x07之间的id给谁用呢?
2.资源共享库的R.java中的这个onResourcesLoaded方法为啥要改变包id呢?
同时,我们也期待着ResTable的翻车现场。