Android插件化原理和实践 (五) 之 解决合并资源后资源Id冲突的问题

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lyz_zyx/article/details/84873004

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打包流程图,如下图:

https://images2017.cnblogs.com/blog/357738/201708/357738-20170811144825570-687368085.png

来解释一下其过程步骤:

  1. 打包资源文件:使用aapt(Android Asset Package Tool)把res目录下的资源生成相应的R.java文件和resource.arsc文件,同时为AndroidManifest.xml生成二进制的AndroidManifest.java文件。
  2. 处理aidl文件:使有aidl(Android Interface Denifition Language)把项目自定义的aidl文件生成相应的Java代码文件。
  3. 编译Java代码文件:使用javac(Java 编译器)把项目中所有的Java代码编译成class文件。包括Java源文件、aapt生成的R.java文件 以及 aidl生成的Java接口文件。
  4. 代码混淆处理:若配置了启用Proguard混淆,就会对代码进行混淆处理并生成proguardMapping.txt文件。
  5. 把class文件转成dex文件:使用dx.bat将所有的class文件(包括第三方库中的class文件)转换成dex文件。
  6. 生成apk文件:使用apkbuilder(主要用到的是sdk/tools/lib/sdklib.jar文件中的ApkBuilderMain类)将所有的dex文件、resource.arsc、res文件夹、assets文件夹、AndroidManifest.xml 打包为.apk文件。
  7. apk文件签名:使用apksigner(Android官方针对apk签名及验证工具)或jarsigner(JDK提供针对jar包签名工具)对未签名的apk文件进行签名。
  8. 对齐处理:使用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

点击下载修改后的aapt_mac文件

点击下载修改后的aapt_win文件

 

 

 

 

猜你喜欢

转载自blog.csdn.net/lyz_zyx/article/details/84873004