Android AAPT 详解(下篇)

阿凡达2018-07-04 10:34

AAPT源码解析

首先下载Android源码

Android Source

我这边下载的是Android 6.0的源码

AAPT代码地址:*/frameworks/tools/aapt/目录下。

我们这里以一个命令来跟踪源码的流程,即用aapt是如何构建一个R.java的,命令格式如下:

aapt package –m –J <R.java目录> -S <res目录> -I <android.jar目录> -M <AndroidManifest.xml目录>

实践:

aapt package -m -J  /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/ -S /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/src/main/res/ -I /Users/zfxu/Library/Android/sdk/platforms/android-25/android.jar -M /Users/zfxu/work/androidstudio_workspace/AAPTDemo/app/src/main/AndroidManifest.xml

运行该命令后,在配置的R.java目录下 会生成一个自己app包名的目录,里面会生成R.java文件,如下:

R.java里会生成这样的索引ID类,都是以0x7f开头

public final class R {
    public static final class attr {
    }
    public static final class color {
        public static final int colorAccent=0x7f040002;
        public static final int colorPrimary=0x7f040000;
        public static final int colorPrimaryDark=0x7f040001;
    }
    public static final class layout {
        public static final int activity_main=0x7f030000;
    }
    public static final class mipmap {
        public static final int ic_launcher=0x7f020000;
        public static final int ic_launcher_round=0x7f020001;
    }
    public static final class string {
        public static final int app_name=0x7f050000;
    }
}

好,既然我们知道了输入以及输出,那让我们来分析这块的代码。

PS:由于某些函数较长,不会贴出所有的源码

  1. 入口 /frameworks/base/tools/aapt/Main.cpp

    int main(int argc, char* const argv[]) {
     ***
     else if (argv[1][0] == 'p')
         bundle.setCommand(kCommandPackage);
     ***
     while (argc && argv[0][0] == '-') {
         //通过case比较,去除命令中所有的参数,并且放进bundle中
         /* flag(s) found */
         const char* cp = argv[0] +1;
         while (*cp != '\0') {
             ***
             switch (*cp) {
             case 'M':
                 argc--;
                 argv++;
                 if (!argc) {
                     fprintf(stderr, "ERROR: No argument supplied for '-M' option\n");
                     wantUsage = true;
                     goto bail;
                 }
                 //这个仅仅是把传进来的地址坐下系统路径分割线的转换
                 convertPath(argv[0]);
                 bundle.setAndroidManifestFile(argv[0]);
                 break;
             }
             ***
         }
     }
     ***
     result = handleCommand(&bundle);
     ***
    }
    

    当通过所有的匹配规则后,该函数实际调用是 handleCommand(&bundle)。 至于执行什么命令说白了也是命令指定的,-p 设置的command参数是kCommandPackage。

  2. 分发指令 /frameworks/base/tools/aapt/Main.cpp

    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;
     }
    }
    
  3. 处理package指令 /frameworks/base/tools/aapt/Command.cpp

int doPackage(Bundle* bundle) {
    const char* outputAPKFile;
    int retVal = 1;
    status_t err;
    sp<AaptAssets> assets;
    int N;
    FILE* fp;
    String8 dependencyFile;
    sp<ApkBuilder> builder;

    sp<WeakResourceFilter> configFilter = new WeakResourceFilter();
    //见注释3-1
    err = configFilter->parse(bundle->getConfigurations());
    if (err != NO_ERROR) {
        goto bail;
    }

    //资源本地化相关的配置,具体什么含义也没有理解清楚
    if (configFilter->containsPseudo()) {
        bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_ACCENTED);
    }
    if (configFilter->containsPseudoBidi()) {
        bundle->setPseudolocalize(bundle->getPseudolocalize() | PSEUDO_BIDI);
    }

    //校验命令中是否传入正确的参数
    N = bundle->getFileSpecCount();
    if (N < 1 && bundle->getResourceSourceDirs().size() == 0 && bundle->getJarFiles().size() == 0
            && bundle->getAndroidManifestFile() == NULL && bundle->getAssetSourceDirs().size() == 0) {
        fprintf(stderr, "ERROR: no input files\n");
        goto bail;
    }

    outputAPKFile = bundle->getOutputAPKFile();

    // 如果输出文件存在,但是是不合格的,则直接报错结束,如果不存在,则新建空文件
    if (outputAPKFile) {
        FileType type;
        type = getFileType(outputAPKFile);
        if (type != kFileTypeNonexistent && type != kFileTypeRegular) {
            fprintf(stderr,
                "ERROR: output file '%s' exists but is not regular file\n",
                outputAPKFile);
            goto bail;
        }
    }

    // Load the assets.
    assets = new AaptAssets();

    // 设置res和asset的成员,仅仅是外层new一个对象赋值给AaptAssets
    if (bundle->getGenDependencies()) {
        sp<FilePathStore> resPathStore = new FilePathStore;
        assets->setFullResPaths(resPathStore);
        sp<FilePathStore> assetPathStore = new FilePathStore;
        assets->setFullAssetPaths(assetPathStore);
    }
    //调用AaptAssets类的成员函数slurpFromArgs将AndroidManifest.xml文件,目录assets和res下的资源目录和资源文件收录起来保存到AaptAssets中的
    //成员变量中
    err = assets->slurpFromArgs(bundle);
    if (err < 0) {
        goto bail;
    }
    //如果命令中指定需要详细日志输出,这里会打印所有的资源信息
    if (bundle->getVerbose()) {
        assets->print(String8());
    }

    // Create the ApkBuilder, which will collect the compiled files
    // to write to the final APK (or sets of APKs if we are building
    // a Split APK.
    //new一个ApkBuilder对象,如果需要生成多个apk,则需要将上层的配置写入改对象中
    builder = new ApkBuilder(configFilter);
    // If we are generating a Split APK, find out which configurations to split on.
    if (bundle->getSplitConfigurations().size() > 0) {
        const Vector<String8>& splitStrs = bundle->getSplitConfigurations();
        const size_t numSplits = splitStrs.size();
        for (size_t i = 0; i < numSplits; i++) {
            std::set<ConfigDescription> configs;
            if (!AaptConfig::parseCommaSeparatedList(splitStrs[i], &configs)) {
                fprintf(stderr, "ERROR: failed to parse split configuration '%s'\n", splitStrs[i].string());
                goto bail;
            }

            err = builder->createSplitForConfigs(configs);
            if (err != NO_ERROR) {
                goto bail;
            }
        }
    }

    // If they asked for any fileAs that need to be compiled, do so.
    //这是最核心的一步,编译资源(res和asset)。这个成功后,下面的步骤就仅仅是写输出文件了
    if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {
        err = buildResources(bundle, assets, builder);
        if (err != 0) {
            goto bail;
        }
    }
    ......省略代码
}

注释:

  1. 编译res和xml资源 /frameworks/base/tools/aapt/Resource.cpp

ps:改函数较长,截取部分代码分步解析

status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuilder>& builder)
{
    // First, look for a package file to parse.  This is required to
    // be able to generate the resource information.
    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;
    }

    ......
}

首先解析manifest文件,调用的是parsePackage函数,解析之前,manifest被封装成一个AaptGroup对象。

static status_t parsePackage(Bundle* bundle, const sp<AaptAssets>& assets,
    const sp<AaptGroup>& grp)
{
    if (grp->getFiles().size() != 1) {
        fprintf(stderr, "warning: Multiple AndroidManifest.xml files found, using %s\n",
                grp->getFiles().valueAt(0)->getPrintableSource().string());
    }

    sp<AaptFile> file = grp->getFiles().valueAt(0);

    ResXMLTree block;
    status_t err = parseXMLResource(file, &block);
    if (err != NO_ERROR) {
        return err;
    }
    ......省略代码
    return NO_ERROR;
}

没有具体细看里面的代码,说下具体思路,通过传进来的形参AaptGroup拿到具体的AaptFile对象。在调用公共类的parseXmlResource解析xml文件得到具体数据后,存放在对象ResXmlTree中。parseXMLResource函数在类 frameworks/base/tools/aapt/XMLNode.cpp 中。有兴趣的可以自己去读下,这里就不贴了。解析玩manifest.xml后,我们继续buildResources的分析。

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;
    }
    ...省略的代码

这段代码的目的主要是收集当前编译的资源需要依赖的的资源并且存放在ResourceTable这个数据结构中。这边简单介绍一下ResourceTable这个数据结构,首先我们得知道R.java里面的资源标识id的构成,比方说 0x7f040002 其中0x7f表示是packageID,也就是上面的packageType,它是一个命名空间,限定资源的来源,7f表明是当前应用程序的资源,系统的资源是以0x01开头。04 表示TypeID。资源的类型animator、anim、color、drawable、layout、menu、raw、string和xml等等若干种,每一种都会被赋予一个ID。最后四位是EntryID,指的是每一个资源在起对应的TypID中出现的顺序。

而ResouceTable里面存储的最核心的元素就是这个id的区分。

收集完成当前应用依赖的资源以后,就要编译当前应用自己的资源。这里由于代码太过于复杂,本人也没有完全看懂,就不贴了,逻辑基本上就是通过命令的输出,一个个的编译资源文件和png。然后存储在一个xml文件中,为后面生成R.java文件中做准备。实际上前面也有提到,所有的资源都会存在ResouceTable这个数据结构中,做完编译工作以后,只需要去遍历这个向量表,然后对里面的packageID,typeID,EvtryID进行拼接,就可以得到我们所熟悉的 0x7f040002这种资源ID。ResouceTable的构造函数也可以看出来里面的过程:

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);
}

  1. 完成上述的编译资源的工作以后,细心的读者就会发现,对于manifest.xml一直都是读取里面的配置信息,并没有编译,所以最后一步就是把manifest.xml编译成二进制文件。这个就不贴出源码了。

  2. 最后一步,将上述的编译结果输出到R.java和Apk中。其中还会输出混淆文件,java符号表等。

...省略代码
// 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());
        }
        if (err < 0) {
            goto bail;
        }
        // If we have library files, we're going to write our R.java file into
        // the appropriate class directory for those libraries as well.
        // e.g. gen/com/foo/app/lib/R.java
        if (bundle->getExtraPackages() != NULL) {
            // Split on colon
            String8 libs(bundle->getExtraPackages());
            char* packageString = strtok(libs.lockBuffer(libs.length()), ":");
            while (packageString != NULL) {
                // Write the R.java file out with the correct package name
                err = writeResourceSymbols(bundle, assets, String8(packageString), true,
                        bundle->getBuildSharedLibrary());
                if (err < 0) {
                    goto bail;
                }
                packageString = strtok(NULL, ":");
            }
            libs.unlockBuffer();
        }
    } else {
        err = writeResourceSymbols(bundle, assets, assets->getPackage(), false, false);
        if (err < 0) {
            goto bail;
        }
        err = writeResourceSymbols(bundle, assets, assets->getSymbolsPrivatePackage(), true, false);
        if (err < 0) {
            goto bail;
        }
    }
...省略代码

综上所述,分析完成了了apk资源编译的过程,由于本人c++功底不佳,有的东西也只是靠猜测来完成,基本上能够理清楚大体的逻辑。如果想更详细的内容,可以自己参考源码。网上有篇博客,有点乱,但是很细,可以看下 http://www.cnblogs.com/dyllove98/p/3144950.html

AAPT命令修改,完成修改资源ID

在第三节我们讲AAPT是如何编译资源并且生成R.java文件的,也提到R.java中资源ID的含义,在组件化框架中,由于组件和宿主分开编译,为了防止组件的资源ID和宿主的资源ID冲突,所以就需要修改AAPT源码。基本思路就是每个组件分配一个不一样的ID,宿主的ID是以0x7f开头,组件的ID是0x**开头,这样就避免冲突。

可以看出如果不修改AAPT源码重新构建,就会导致组件之间或者组件与宿主之间的ID冲突。所以就会有如下模型:

既然分析AAPT的编译过程,那思路就很清晰了,在命令中添加一个自定义的ID,然后在代码中拿到这个ID,拼接的时候替换上即可。当然这只是最简单的,而实际情况呢,比方说宿主里有一部分资源是其他组件公用的,如何保证这部分资源和id和组件本身的id不会发生冲突呢?又如何在我们工程里自动化这套自定义的aapt从而代替系统标准的aapt呢?

相关阅读:Android AAPT 详解(上篇)

本文来自网易实践者社区,经作者徐正峰授权发布。