Android 应用资源表(resources.arsc)解析

我们以一个包名为AaptDemo的Apk为例来分析。

首先我们用sdk中的工具来生成一个简单的应用工程。

D:\android_debug>..\sdk\tools\android.bat create project --activity MainActivity --package com.jackyperf.aaptdemo --path .\AaptDemo -t android-23
  • 生成的应用工程目录
  • 应用资源目录

接下来我们以该应用为例来分析resources.arsc的生成过程。

Android 应用资源表的生成

我们知道Android应用资源是通过sdk中的aapt工具编译和打包的,,可以利用下面的命令将我们之前创建的AaptDemo应用编译、打包成AaptDemo.apk。

D:\android_debug\AaptDemo>..\..\sdk\build-tools\23.0.2\aapt.exe package -f -M AndroidManifest.xml -S .\res -I ..\..\sdk\platforms\android-23\android.jar -F AaptDemo.apk

结合上述命令,我们从aapt的入口main函数开始分析。

源文件:android\frameworks\base\tools\aapt\main.cpp

int main(int argc, char* const argv[])
{
    char *prog = argv[0];
    Bundle bundle;
    ...
    else if (argv[1][0] == 'p')
        //aapt编译、打包资源命令
        bundle.setCommand(kCommandPackage);  
    ...
    while (argc && argv[0][0] == '-') {
        /* flag(s) found */
        const char* cp = argv[0] +1;

        while (*cp != '\0') {
            switch (*cp) {
            ...
            //强制重写最终的apk文件
            case 'f':
                bundle.setForce(true);
                break;
            ...
            //添加一个额外的package
            case 'I':
                argc--;
                argv++;
                if (!argc) {
                    fprintf(stderr, "ERROR: No argument supplied for '-I' option\n");
                    wantUsage = true;
                    goto bail;
                }
                convertPath(argv[0]);
                bundle.addPackageInclude(argv[0]);
                break;
            //指定打包完成后的apk文件
            case 'F':
                argc--;
                argv++;
                if (!argc) {
                    fprintf(stderr, "ERROR: No argument supplied for '-F' option\n");
                    wantUsage = true;
                    goto bail;
                }
                convertPath(argv[0]);
                bundle.setOutputAPKFile(argv[0]);
                break;
                ...
            //指定资源路径
            case 'S':
                argc--;
                argv++;
                if (!argc) {
                    fprintf(stderr, "ERROR: No argument supplied for '-S' option\n");
                    wantUsage = true;
                    goto bail;
                }
                convertPath(argv[0]);
                bundle.addResourceSourceDir(argv[0]);
                break;
                ...
    result = handleCommand(&bundle);
    ...
}

解析我们编译、打包所使用的aapt命令,将解析得到的命令行参数保存到Bundle中,然后调用handleCommand进行处理,在handleCommand中直接,调用doPackage处理aapt package命令。

源文件:android\frameworks\base\tools\aapt\Command.cpp

int doPackage(Bundle* bundle)
{
    ...
    //检查输出的apk文件是否合法
    outputAPKFile = bundle->getOutputAPKFile();

    // Make sure the filenames provided exist and are of the appropriate type.
    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();
    ...
    //收集应用资源(AndroidManifest、asset目录、res目录等)保存在AaptAssets对象中
    err = assets->slurpFromArgs(bundle);
    ...
    // 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.
    builder = new ApkBuilder(configFilter);
    ...
    // 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;
        }
    }
    ...
    //生成Proguard文件,用于字节码混淆、压缩字节码及资源文件
    // Write out the ProGuard file
    err = writeProguardFile(bundle, assets);
    ...
    // Write the apk
    if (outputAPKFile) {
        // Gather all resources and add them to the APK Builder. The builder will then
        // figure out which Split they belong in.
        err = addResourcesToBuilder(assets, builder);
        if (err != NO_ERROR) {
            goto bail;
        }

        const Vector<sp<ApkSplit> >& splits = builder->getSplits();
        const size_t numSplits = splits.size();
        for (size_t i = 0; i < numSplits; i++) {
            const sp<ApkSplit>& split = splits[i];
            String8 outputPath = buildApkName(String8(outputAPKFile), split);
            //将编译后的资源文件写入apk包,包括resources.arsc
            err = writeAPK(bundle, outputPath, split);
            if (err != NO_ERROR) {
                fprintf(stderr, "ERROR: packaging of '%s' failed\n", outputPath.string());
                goto bail;
            }
        }
    }
    ...
}

doPackage就是应用资源编译、打包的整个过程,我们重点分析以下三个阶段:

  • 调用slurpFromArgs收集应用资源保存到AaptAssets
  • 调用buildResources编译应用资源,构建资源表
  • 资源表写入apk包的过程

调用slurpFromArgs收集应用资源保存到AaptAssets

在分析应用资源收集的过程之前,我们首先看下AaptAssets的类图,以便于了解它是如何保存应用资源的。


AaptAssets描述正在编译的资源

  • mGroupEntries:描述包含的资源配置集合
  • mRes:描述包含的资源类型集,每一种类型的资源用一个ResourceTypeSet表示

AaptDir描述单一目录(资源类型)下的资源,可以包含文件或者子目录

  • mLeaf:目录的叶子路径的名称或者资源类型名
  • mPath:目录的全路径
  • mFiles:当前目录下的同名资源集合
  • mDirs:当前目录下子目录资源

AaptGroup描述一组名字相同(配置不同)的资源文件

  • mLeaf:资源文件名
  • mPath:资源文件全路径
  • mFiles:名字相同配置不同的一组资源文件

AaptFile描述单一资源文件

  • mPath:资源文件路径
  • mGroupEntry:对应的资源配置
  • mResourceType:资源文件所属资源类型

AaptGroupEntry描述单一资源文件的配置

  • mParams:资源文件的配置信息,包括移动网络、国家、地区、语言、屏幕密度等

下面分析收集资源保存到AaptAssets的过程

源文件:android\frameworks\base\tools\aapt\AaptAssets.cpp

ssize_t AaptAssets::slurpFromArgs(Bundle* bundle)
{
    int count;
    int totalCount = 0;
    FileType type;
    const Vector<const char *>& resDirs = bundle->getResourceSourceDirs();
    //在本文的AaptDemo应用中,dirCount = 1
    const size_t dirCount =resDirs.size();
    ...
    /*
     * If a package manifest was specified, include that first.
     */
    //为AndroidManifest文件创建AaptGroup以及AaptGroupEntry,并添加到AaptAssets。
    if (bundle->getAndroidManifestFile() != NULL) {
        // place at root of zip.
        String8 srcFile(bundle->getAndroidManifestFile());
        addFile(srcFile.getPathLeaf(), AaptGroupEntry(), srcFile.getPathDir(),
                NULL, String8());
        totalCount++;
    }
    ...
    //收集应用内部的asset目录下的资源,在AaptDemo应用中,没有创建该路径
    const Vector<const char*>& assetDirs = bundle->getAssetSourceDirs();
    const int AN = assetDirs.size();
    ...
    //收集应用内部的res目录下的资源
    for (size_t i=0; i<dirCount; i++) {
        const char *res = resDirs[i];
        if (res) {
            type = getFileType(res);
            if (type == kFileTypeNonexistent) {
                fprintf(stderr, "ERROR: resource directory '%s' does not exist\n", res);
                return UNKNOWN_ERROR;
            }
            if (type == kFileTypeDirectory) {
                if (i>0) {
                    //收集当前package对应的overlay package的资源
                    sp<AaptAssets> nextOverlay = new AaptAssets();
                    current->setOverlay(nextOverlay);
                    current = nextOverlay;
                    current->setFullResPaths(mFullResPaths);
                }
                count = current->slurpResourceTree(bundle, String8(res));
                if (i > 0 && count > 0) {
                  count = current->filter(bundle);
                }

                if (count < 0) {
                    totalCount = count;
                    goto bail;
                }
                totalCount += count;
            }
            else {
                fprintf(stderr, "ERROR: '%s' is not a directory\n", res);
                return UNKNOWN_ERROR;
            }
        }
        
    }
    ...
}

收集res目录下的资源是通过调用slurpResourceTree来实现的,继续分析slurpResourceTree。

ssize_t AaptAssets::slurpResourceTree(Bundle* bundle, const String8& srcDir)
{
    ssize_t err = 0;

    //打开res目录
    DIR* dir = opendir(srcDir.string());
    ...
    while (1) {
        //收集res目录下子目录的资源
        struct dirent* entry = readdir(dir);
        ...
        String8 subdirName(srcDir);
        subdirName.appendPath(entry->d_name);

        AaptGroupEntry group;
        String8 resType;
        //解析entry目录的资源类型以及资源的配置信息
        //资源类型保存在存数resType中,资源的配置信息保存在AaptGroupEntry中
        bool b = group.initFromDirName(entry->d_name, &resType);
        ...
        //返回当前文件的类型
        FileType type = getFileType(subdirName.string());

        if (type == kFileTypeDirectory) {
            //为当前类型资源创建AaptDir对象
            sp<AaptDir> dir = makeDir(resType);
            //收集当前文件下的资源
            ssize_t res = dir->slurpFullTree(bundle, subdirName, group,
                                                resType, mFullResPaths);
            if (res < 0) {
                count = res;
                goto bail;
            }
            if (res > 0) {
                //AaptGroupEntry添加到AaptAssets
                mGroupEntries.add(group);
                count += res;
            }

            // Only add this directory if we don't already have a resource dir
            // for the current type.  This ensures that we only add the dir once
            // for all configs.
            //如果当前资源类型的AaptDir没有添加到AaptAssets,添加;
            //一种资源类型对应一个AaptDir,即使有多种配置。
            sp<AaptDir> rdir = resDir(resType);
            if (rdir == NULL) {
                mResDirs.add(dir);
            }
        } else {
            if (bundle->getVerbose()) {
                fprintf(stderr, "   (ignoring file '%s')\n", subdirName.string());
            }
        }
    }
    ...
}

slurpResourceTree通过遍历res目录的子目录收集各种类型的资源,同时创建AaptGroupEntry以及AaptDir,并添加到AaptAssets。下面分析slurpFullTree。

ssize_t AaptDir::slurpFullTree(Bundle* bundle, const String8& srcDir,
                            const AaptGroupEntry& kind, const String8& resType,
                            sp<FilePathStore>& fullResPaths, const bool overwrite)
{
    Vector<String8> fileNames;
    {
        DIR* dir = NULL;
        //收集当前文件中的子文件,并添加到fileNames
        dir = opendir(srcDir.string());
        ...
        while (1) {
            struct dirent* entry;

            entry = readdir(dir);
            ...
            String8 name(entry->d_name);
            fileNames.add(name);
            ...        
        }
    }
    ...
    const size_t N = fileNames.size();
    size_t i;
    for (i = 0; i < N; i++) {
        String8 pathName(srcDir);
        FileType type;

        pathName.appendPath(fileNames[i].string());
        type = getFileType(pathName.string());
        if (type == kFileTypeDirectory) {
            //如果当前文件是目录,与上文中处理类似
            ...
        } else if (type == kFileTypeRegular) {
            //如果是普通的资源文件,创建AaptFile对象
            sp<AaptFile> file = new AaptFile(pathName, kind, resType);
            //将AaptFile添加到文件名对应的AaptGroup对象中
            status_t err = addLeafFile(fileNames[i], file, overwrite);
            ...
        }
        ...
    }
    ...
}

最终,res目录下所有的资源都被收集到AaptAssets中,我们以AaptDemo中的res目录为例,来看下生成的AaptAssets的数据结构。


调用buildResources编译应用资源,构建资源表

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.
    ...
    // 解析AndroidManifest.xml获取包名、版本码等信息
    status_t err = parsePackage(bundle, assets, androidManifestFile);
    ...
    ResourceTable table(bundle, String16(assets->getPackage()), packageType);
    err = table.addIncludedResources(bundle, assets);
    ...
    // --------------------------------------------------------------
    // First, gather all resource information.
    // --------------------------------------------------------------

    // resType -> leafName -> group
    KeyedVector<String8, sp<ResourceTypeSet> > *resources = 
            new KeyedVector<String8, sp<ResourceTypeSet> >;
    // 将AaptAssets中资源按照资源类型以AaptGroup为单位添加到resources中
    collect_files(assets, resources);
    ...
    // 将收集到的资源类型集Vector保存到AaptAssets的mRes中
    assets->setResources(resources);
    ...
    if (layouts != NULL) {
        // 为资源集中的资源创建Type/ConfigList/Entry/Item结构存放到ResourceTable中
        err = makeFileResources(bundle, assets, &table, layouts, "layout");
        if (err != NO_ERROR) {
            hasErrors = true;
        }
    }
    ...
    while(current.get()) {
        KeyedVector<String8, sp<ResourceTypeSet> > *resources = 
                current->getResources();

        ssize_t index = resources->indexOfKey(String8("values"));
        ...
        // 编译values类型资源,创建资源Type/ConfigList/Entry/Item结构存放到ResourceTable中
        res = compileResourceFile(bundle, assets, file, it.getParams(), 
                                          (current!=assets), &table);
        ...
    }
    ...
    // --------------------------------------------------------------------
    // Assignment of resource IDs and initial generation of resource table.
    // --------------------------------------------------------------------

    if (table.hasResources()) {
        err = table.assignResourceIds();
        if (err < NO_ERROR) {
            return err;
        }
    }
    ...
    // --------------------------------------------------------------
    // Generate the final resource table.
    // Re-flatten because we may have added new resource IDs
    // --------------------------------------------------------------


    ResTable finalResTable;
    sp<AaptFile> resFile;
    ...
        Vector<sp<ApkSplit> >& splits = builder->getSplits();
        const size_t numSplits = splits.size();
        for (size_t i = 0; i < numSplits; i++) {
            sp<ApkSplit>& split = splits.editItemAt(i);
            // 创建"resources.arsc"AaptFile用于保存资源表信息
            sp<AaptFile> flattenedTable = new AaptFile(String8("resources.arsc"),
                    AaptGroupEntry(), String8());
            // 将ResourceTable中收集的资源信息按照resources.arsc文件的格式flatten到flattenedTable中
            err = table.flatten(bundle, split->getResourceFilter(),
                    flattenedTable, split->isBase());
            if (err != NO_ERROR) {
                fprintf(stderr, "Failed to generate resource table for split '%s'\n",
                        split->getPrintableName().string());
                return err;
            }
            // 将flattenedTable添加到ApkSplit中,最终作为OutputEntry写入APK中
            split->addEntry(String8("resources.arsc"), flattenedTable);

            if (split->isBase()) {
                resFile = flattenedTable;
                err = finalResTable.add(flattenedTable->getData(), flattenedTable->getSize());
                if (err != NO_ERROR) {
                    fprintf(stderr, "Generated resource table is corrupt.\n");
                    return err;
                }
            } else {
            }
    ...
}

以AaptDemo为例,最终得到的资源项如下图所示(strings.xml中仅有app_name)


其中,Entry name最终作为keyString写入resources.arsc;Item value作为ValueString写入resources.arsc。

最终资源索引表resources.arsc文件的结构如下图所示


资源表写入apk包的过程

有了前面的知识,resources.arsc文件写入apk包的过程就比较简单了,这里不再分析。

参考

  1. http://blog.csdn.net/luoshengyang/article/details/8744683
  2. http://blog.zhaiyifan.cn/2016/02/13/android-reverse-2/
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

友情链接更多精彩内容