赞
踩
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值作为输入参数,就能全部解决了。
先来谈谈Android App的打包过程和aapt命令了。我们在平时开发中使用的IDE是Android Studio,它默认是使用了Gradle来对工程进行编译和打包,官方也给出了一套完整的Android App打包流程图,如下图:
来解释一下其过程步骤:
我们知道了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了!
我们先来看看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的值变成我们传入的参数就大功告成了。
第一步,修改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替换即可。
这里顺便一提,在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
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。