Android插件化中,要解决资源的问题,有些插件化框架会选择不合并资源,这样就得维护多套mResources变量,这样的话难免开发上没有那么的灵活和方便。所以一般地都是选择合并资源,也就是我们上一遍文章《Android插件化原理和实践 (四) 之 合并插件中的资源》介绍的办法。但是合并后资源id会冲突。为什么会有这种冲突的问题?在Android项目打包后,res目录下的每一个资源都有一个对应的资源id值对应在R.java类中,比如0x7f4001b,都是默认0x7f开头的。因为宿主App和插件App都是各自打包,所以宿主App中的某个资源id值肯定会存在跟插件中的App中某个资源id值是相同的,这就是合并资源方案的后遗症,此问题可导致我们加载不到正确的资源。像small插件化框架的做法是在合并资源再打包生成resources.arsc文件之后,使用Gradle第三方插件gradle-small来对这个resources.arsc文件进行修改,这是一种办法,但我们在本文是会给出另一种更简单和一劳永逸的办法,那就是修改aapt命令了。只要我们对aapt进行扩展,在Gradle中让其能接收一个apk包的资源id值作为输入参数,就能全部解决了。
1 App打包流程
先来谈谈Android App的打包过程和aapt命令了。我们在平时开发中使用的IDE是Android Studio,它默认是使用了Gradle来对工程进行编译和打包,官方也给出了一套完整的Android App打包流程图,如下图:
来解释一下其过程步骤:
- 打包资源文件:使用aapt(Android Asset Package Tool)把res目录下的资源生成相应的R.java文件和resource.arsc文件,同时为AndroidManifest.xml生成二进制的AndroidManifest.java文件。
- 处理aidl文件:使有aidl(Android Interface Denifition Language)把项目自定义的aidl文件生成相应的Java代码文件。
- 编译Java代码文件:使用javac(Java 编译器)把项目中所有的Java代码编译成class文件。包括Java源文件、aapt生成的R.java文件 以及 aidl生成的Java接口文件。
- 代码混淆处理:若配置了启用Proguard混淆,就会对代码进行混淆处理并生成proguardMapping.txt文件。
- 把class文件转成dex文件:使用dx.bat将所有的class文件(包括第三方库中的class文件)转换成dex文件。
- 生成apk文件:使用apkbuilder(主要用到的是sdk/tools/lib/sdklib.jar文件中的ApkBuilderMain类)将所有的dex文件、resource.arsc、res文件夹、assets文件夹、AndroidManifest.xml 打包为.apk文件。
- apk文件签名:使用apksigner(Android官方针对apk签名及验证工具)或jarsigner(JDK提供针对jar包签名工具)对未签名的apk文件进行签名。
- 对齐处理:使用zipalign对签名后的apk文件进行对齐处理,以便在运行时可节省内存。
2 十六进制整数规则
我们知道了res目录下所有的资源都会生成一个R.java文件,并且每个资源都对应R.java中的一件十六进制整数变量。其实这些十六进制的整数是由三部分组成的,那就是:PackageId + TypeId + ItemValue。
PackageId | 是apk包的id,默认是0x7f,默认不可变 |
TypeId | 资源类型Id,比如像layout、string、drawable、id等等,它们对应的是:0x7f04、0x7f06、0x7f02、0x7f0b 等等,它们是按顺序从1开始递增的 |
ItemValue | 类型Id下的资源值,从0开始递增 |
正是因为宿主和插件都是apk包,所以它们默认PackageId都是0x7f,所以就会导致合并资源后资源id冲突。所以解决这个问题就要为不同的插件设置不同的PackageId,而宿主可以保留原来0x7f不变,这样就永远不会有冲突发生了。但是PackageId默认就是0x7f,而且默认就是不可修改的,那该怎么办呢?这时就得去修改aapt了!
3 如何修改aapt命令
3.1 aapt源码分析
我们先来看看aapt的源码,它的源码位于Android源码目录/tools/aapt下,它是一个使用了C++编写的工程。一般地工程的入口都是main方法,所以我们先找到Main.cpp下的main方法:
Main.cpp
int main(int argc, char* const argv[])
{
char *prog = argv[0];
Bundle bundle;
bool wantUsage = false;
int result = 1; // pessimistically assume an error.
int tolerance = 0;
/* default to compression */
bundle.setCompressionMethod(ZipEntry::kCompressDeflated);
if (argc < 2) {
wantUsage = true;
goto bail;
}
if (argv[1][0] == 'v')
bundle.setCommand(kCommandVersion);
else if (argv[1][0] == 'd')
……
/*
* We're past the flags. The rest all goes straight in.
*/
bundle.setFileSpec(argv, argc);
// 关键代码
result = handleCommand(&bundle);
bail:
if (wantUsage) {
usage();
result = 2;
}
return result;
}
这个方法比较长,贴出代码中我省略了中间部分,从代码上看主要是分析传入的参数,我们来看下面关键代码行中的handleCommand方法:
int handleCommand(Bundle* bundle)
{
//printf("--- command %d (verbose=%d force=%d):\n",
// bundle->getCommand(), bundle->getVerbose(), bundle->getForce());
//for (int i = 0; i < bundle->getFileSpecCount(); i++)
// printf(" %d: '%s'\n", i, bundle->getFileSpecEntry(i));
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中的方法,来看看它的代码:
Command.cpp
int doPackage(Bundle* bundle)
{
……
// If they asked for any fileAs that need to be compiled, do so.
if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {
// 关键代码
err = buildResources(bundle, assets, builder);
if (err != 0) {
goto bail;
}
}
……
}
这里看关键代码,buildResources方法位于Resource.cpp中,继续看代码:
Resource.cpp
status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuilder>& builder)
{
……
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);
err = table.addIncludedResources(bundle, assets);
if (err != NO_ERROR) {
return err;
}
……
}
这里能看到有一个packageType字段,它是表示包的类型,然后将这个包类型传递给ResourceTable的构造函数,所以再来看看ResourceTable的构造函数:
ResourceTable.cpp
ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type)
: mAssetsPackage(assetsPackage)
, mPackageType(type)
, mTypeIdOffset(0)
, mNumLocal(0)
, mBundle(bundle)
{
ssize_t packageId = -1;
switch (mPackageType) {
case App:
case AppFeature:
packageId = 0x7f;
break;
case System:
packageId = 0x01;
break;
case SharedLibrary:
packageId = 0x00;
break;
default:
assert(0);
break;
}
sp<Package> package = new Package(mAssetsPackage, packageId);
mPackages.add(assetsPackage, package);
mOrderedPackages.add(package);
// Every resource table always has one first entry, the bag attributes.
const SourcePos unknown(String8("????"), 0);
getType(mAssetsPackage, String16("attr"), unknown);
}
看出了吗?0x7f这就是我们包的默认资源id。这里代码意思就是:判断mPackageType,如果是App,则packageId就是0x7f,此外0x01和0x00都是系统占用了。所以我们就是从这里入手,只要通过传入一个非0x7f、0x01和0x00的参数,然后能够使packageId的值变成我们传入的参数就大功告成了。
3.2 修改aapt源码
第一步,修改Main.cpp的main方法,使其接收一个关键字和包资源id值,这里我们写的关键字是“--PLUG-resoure-id “:
Main.cpp
int main(int argc, char* const argv[])
{
……
else if(strcmp(cp, "-PLUG-resoure-id") == 0){
argc--;
argv++;
if (!argc) {
fprintf(stderr, "ERROR: No argument supplied for '--PLUG-resoure-id' option\n");
wantUsage = true;
goto bail;
}
bundle.setApkModule(argv[0]);
}
……
}
这里可以模仿其上下文代码,插入关键关解析代码,关将最后解析到了的值通过bundle的setApkModele方法设置进去。
第二步,接下来,当然就是要为bundle创建set和get方法了:
Bundle.h
public:
……
const android::String8& getApkModule() const {return mApkModule;}
void setApkModule(const char* str) { mApkModule=str;}
……
}
最后一步,就是修改ResourceTable的构造函数,使其支持通过getApkModule来获得自定义的包的id值,然后修改packageId变量:
ResourceTable.cpp
ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type)
: mAssetsPackage(assetsPackage)
, mPackageType(type)
, mTypeIdOffset(0)
, mNumLocal(0)
, mBundle(bundle)
{
ssize_t packageId = -1;
switch (mPackageType) {
case App:
case AppFeature:
packageId = 0x7f;
break;
case System:
packageId = 0x01;
break;
case SharedLibrary:
packageId = 0x00;
break;
default:
assert(0);
break;
}
// 添加的代码
if(!bundle->getApkModule().isEmpty()){
android::String8 apkmoduleVal=bundle->getApkModule();
packageId=apkStringToInt(apkmoduleVal);
}
……
}
到此,我们的修改就完成了,然后就是要执行编译。在编译完成后生成的新的aapt文件后,就可以将本地电脑中的Android SDK中build-tools\你工程中使用的编译版本\aapt目录下的aapt替换即可。
4 配置aapt命令
这里顺便一提,在ACCD插件化框架也是使类似的办法去通过修改aapt命令来解决资源冲突的问题,但是此框架在Gradle配置中并不是通过使aapt传入关键字的方式,而是通过在android-defaultConfig-versionName配置指定版本名称时在后缀传入,例如:versionName "1.00x71"。其实原理差不多就是为了让里面的packageId变成我们自定义的id。说回我们上述修改,我们还差最后一步,就是让插件工程的Gradle中的android闭包中配置以下代码,这里传入的包资源id是0x71。代码如下:
android {
……
aaptOptions {
aaptOptions.additionalParameters '--PLUG-resoure-id', '0x71'
}
}
如果你工程中使用的编译sdk版本是25或以上的,应该是默认使用了aapt2,aapt2是aapt的优化版本,如果要关闭使用aapt2的话,可以在a工程中的gradle.properties中加上一行配置代码:
android.enableAapt2=false