美文网首页
Android 应用资源表(resources.arsc)解析

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

作者: lbtrace | 来源:发表于2018-05-06 15:55 被阅读474次

    我们以一个包名为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/

    相关文章

      网友评论

          本文标题:Android 应用资源表(resources.arsc)解析

          本文链接:https://www.haomeiwen.com/subject/uvmerftx.html