ROM UI 多主题打包流程研究

简述:

   一直希望有个机会可以好好研究一下android手机的多主题功能,借此机会将自己所能分析到的内容记录一下,防止以后遗忘。
   目前大多数厂商的手机都具备的切换主题的功能,以一个apk的形式将所有资源打包,切换主题时会动态提醒每个应用的资源管理器,将需要使用的资源信息添加进去并重新刷新界面,使其能够使用已经替换过的资源,从而达到整个ROM UI风格的更新。
   目前所有的学习都是基于魔趣OS的多主题功能框架代码,先看下一个主题 apk中主要目录结构:

  • Samsung_theme.apk


    图1 主题包目录结构

   从上图可以看出所有资源都在assets目录下,该APK安装后会同时修改壁纸、铃声、app皮肤、锁屏壁纸和应用图标等,接下是围绕着多主题中' Icon '资源的分析。

学习目的:

   通过研究多主题框架代码,进一步了解AAPT工具打包apk流程。

整体架构:

   分析完整个编译流程和查找资源的流程,画了张图:

图2 资源框架图

上图为本人对整个多主题-Icon资源的理解,暂时先这样,可能画的不够详细,有不对的地方还望大侠们指出。

   通过上图可知,Icon资源包的创建可分为两步:

   第一步:安装主题APK,路径: ' /data/data/包名/base.apk ';
   第二步:主题APK安装完毕时PMS会接受到广播,紧接着通过AAPT工具为刚才安装的' 主题apk '打包一个"resource.apk",我把它简称为“索引apk”。

   资源的查找这边不花时间赘述,可以通过上图了解到下大体的查找流程。

打包流程:

   由于对 c/c++ 代码不熟,百度了一点知识(呵呵哒)。在走到 c++ 流程时记得尽量详细一些,错误的地方还请大拿们指出。先放出整个资源打包的流程图,流程对我而言是相当复杂繁琐,而打包的流程是有两个入口:

  1. 当主题包安装完成时,PMS会对主题包进行二次打包处理,为资源创建新的资源索引包;
  2. 当开机完成,系统服务准备就绪,PMS会立即检查当前应用中的主题,如有必要会重新通过aapt工具打包。

下面的流程图是从安装主题包完成时开始分析(图太大,需要单独查看)。

图3 资源打包流程

一、主题包安装完成

step 1 - 3 :

先列出这部分所有类的位置 :

  • source\frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java (简称 PMS)
  • source\packages\appsThemeManagerService\src\com\gome\themeservice\ThemeManagerService.java (简称 TMS)
  • source\frameworks\base\services\core\java\org\cm\platform\internal\ThemeManagerServiceBroker.java (简称 TMSBroker)

   当主题包安装完成,PMS会接受到一个' POST_INSTALL '的 Handle 消息,在接下的方法中先判断当前 apk 包安装成功与否,再接着对当前的包信息进行判断,如果当前 apk 是主题包就进入特殊处理。PMS会将主题包的包名传递给TMS 服务,从而开始进行下一步处理。
TMS 服务并非是系统服务,坐落于app层。TMSBroker 为系统服务,是 TMS 的在 framework 层的服务代理,所有与app层的交互都是通过 TMSBroker 来代理)

private void handlePackagePostInstall(PackageInstalledInfo res, boolean grantPermissions,
            boolean killApp, String[] grantedPermissions,
            boolean launchedForRestore, String installerPackage,
            IPackageInstallObserver2 installObserver) {
           
        if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
                ...
            // if this was a theme, send it off to the theme service for processing
            if(res.pkg.mIsThemeApk || res.pkg.mIsLegacyIconPackApk) {
                processThemeResourcesInThemeService(res.pkg.packageName);
            } 
                ...
        }

private void processThemeResourcesInThemeService(String pkgName) {
        IThemeService ts = IThemeService.Stub.asInterface(ServiceManager.getService(
                MKContextConstants.MK_THEME_SERVICE));
        if (ts == null) {
            Slog.e(TAG, "Theme service not available");
            return;
        }
        try {
            ts.processThemeResources(pkgName);
        } catch (RemoteException e) {
            /* ignore */
        }
    }
step 4 - 7 :

   TMS 拿到包名后会重新调用PMSprocessThemeResources 方法继续走打包流程,如果编译成功TMS会将这些信息收集起来并做其他处理。此时上层主题的操作交由TMS的来处理,而具体打包流程由PMS来进行。TMS 这块本章不做任何介绍,接来下看看PMS如何工作。

private class ResourceProcessingHandler extends Handler {
        ...
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                 ...
                case MESSAGE_DEQUEUE_AND_PROCESS_THEME:
                    ...
                   if (pkgName != null) {
                        String name;
                        try {
                            PackageInfo pi = mPM.getPackageInfo(pkgName, 0);
                            name = getThemeName(pi);
                        } catch (PackageManager.NameNotFoundException e) {
                            name = null;
                        }
                        //走打包流程
                        int result = mPM.processThemeResources(pkgName);
                        if (result < 0) {
                            postFailedThemeInstallNotification(name != null ? name : pkgName);
                        }
                        //根据打包结果处理上层逻辑
                        sendThemeResourcesCachedBroadcast(pkgName, result);

                        synchronized (mThemesToProcessQueue) {
                            mThemesToProcessQueue.remove(0);
                            if (mThemesToProcessQueue.size() > 0 &&
                                    !hasMessages(MESSAGE_DEQUEUE_AND_PROCESS_THEME)) {
                                this.sendEmptyMessage(MESSAGE_DEQUEUE_AND_PROCESS_THEME);
                            }
                        }
                        //提醒打包完成
                        postFinishedProcessing(pkgName);
                    }
                    break;
            }
        }
    }

二、为索引 apk 的打包作准备

step 8 - 15 :

   这部分主要根据当前主题包名来判断是否需要进一步的打包处理,判断依据就是读取对应目录下' hash '值,并与当前主题包 ' versionCode '作对比,一致则不需要再次编译。当一个主题包首次安装时,对应目录下是不存在这些缓存文件的。而在每次开机时会动态检查缓存,如果之前不慎删除了缓存,此时才会重新打包。

   缓存目录(以 Samsung_theme.apk 为例) : " /data/resource-cache/com.wsdeveloper.galaxys7/icons/ "
该目录下就存在两个文件,' hash '文件用来检验主题包的合法性,而' resources.apk '才是这次需要重点研究的对象,可以参考上面的资源架构图来理解。


图4 索引包缓存目录

从下面的代码可知,如果需要编译索引包,就先根据包名创建对应的缓存目录。

    @Override
    public int processThemeResources(String themePkgName) {
        ...
        // Process icons
        if (isIconCompileNeeded(pkg)) {
            try {
                ThemeUtils.createCacheDirIfNotExists();
                ThemeUtils.createIconDirIfNotExists(pkg.packageName);
                //开始准备创建“icon”目录下的两个文件
                compileIconPack(pkg);
            } catch (Exception e) {
                //出现异常就立即将主题包卸载
                uninstallThemeForAllApps(pkg);
                deletePackageX(themePkgName, getCallingUid(), PackageManager.DELETE_ALL_USERS);
                return PackageManager.INSTALL_FAILED_THEME_AAPT_ERROR;
            }
        }

        // 以下是关于 overley 资源的编译流程,暂不记录
        ....
        return 0;
    }

这个方法主要为了创建零时的' Manifest '文件和 ' hash '文件,' resources.apk '的打包流程接着往下分析。

private void compileIconPack(Package pkg) throws Exception {
        if (DEBUG_PACKAGE_SCANNING) Log.d(TAG, "  Compile resource table for " + pkg.packageName);
        OutputStream out = null;
        DataOutputStream dataOut = null;
        try {
            //创建临时 AndroidManifest.xml 文件
            createTempManifest(pkg.packageName);
            //创建"icon/hash"文件
            int code = pkg.mVersionCode;
            String hashFile = ThemeUtils.getIconHashFile(pkg.packageName);
            out = new FileOutputStream(hashFile);
            dataOut = new DataOutputStream(out);
            dataOut.writeInt(code);
            //准备编译 resources.apk
            compileIconsWithAapt(pkg);
        } finally {
            IoUtils.closeQuietly(out);
            IoUtils.closeQuietly(dataOut);
            cleanupTempManifest();
        }
    }

   准备使用aapt命令打包资源,为了能够适配多主题,CM修改了aapt工具的部分流程,使之适应主题索引包的编译。工具打包时,上层传入的参数主要有四个:

  • 主题资源包路径(pkg.baseCodePath): /data/app/com.wsdeveloper.galaxys7-1/base.apk
  • 索引包路径(resPath):/data/resource-cache/com.wsdeveloper.galaxys7/icons
  • 资源前缀(APK_PATH_TO_ICONS): assets/icons/
  • 图标资源包 package id(Resources.THEME_ICON_PKG_ID) : 98

   描述一下,在查找主题资源时,' package id ' 用于定位索引包位置;索引包路径用于获取单一资源的' 相对路径 '(这里只讨论图片资源);此时资源管理器会通过 ' 主题资源包路径 ' 来解压' base.apk ',并通过' 资源前缀+相对路径 '得到资源在' base.apk '中的绝对位置,得到包中的所有文件资源,从而获取到资源返回给上层的Resources

private void compileIconsWithAapt(Package pkg) throws Exception {
        String resPath = ThemeUtils.getIconPackDir(pkg.packageName);
        final int sharedGid = UserHandle.getSharedAppGid(pkg.applicationInfo.uid);
        try {
            if (mInstaller.aapt(pkg.baseCodePath, APK_PATH_TO_ICONS, resPath, sharedGid,
                    Resources.THEME_ICON_PKG_ID,
                    pkg.applicationInfo.targetSdkVersion,
                    "", "") != 0) {
                throw new AaptException("Failed to run aapt");
            }
        } catch (InstallerException ignored) {
        }
    }
step 16 - 20:

先列出这部分所有类的位置 :

  • source\frameworks\base\services\core\java\com\android\server\pm\Installer.java
  • source\frameworks\base\core\java\com\android\internal\os\InstallerConnection.java
  • source\frameworks\native\cmds\installd\installd.cpp

   具体代码不全贴了,都是逻辑上的处理,简要概括下:Installer 重组 aapt 参数接着传给 InstallerConnection 来继续工作,而InstallerConnection负责与 installd服务进程 进行通讯,将aapt命令跨进程传送 installd 服务,由native层来执行命令。可以通过下面大神的博客详细了解一下native层的installd服务进程:
   http://blog.csdn.net/yangwen123/article/details/11104397

   installd服务进程的启动时在Android启动脚本' init.rc '中通过服务配置的,而PMS是通过套接字的方式访问 installd服务,在以下代码中可以了解大概流程,如果没有连接则先尝试连接socket,再将aapt指令通过socket发送给installd服务。

  public synchronized String transact(String cmd) {
        ...
        //尝试连接 installd 服务的socket,
        if (!connect()) {
            return "-1";
        }
        //发送cmd指令
        if (!writeCommand(cmd)) {
            if (!connect() || !writeCommand(cmd)) {
                return "-1";
            }
        }
        ...
  }

此时接受到来自PMS传来的指令,接下来开始执行打包指令。

static int installd_main(const int argc ATTRIBUTE_UNUSED, char *argv[]) {
    ...
    //自installd服务启动后,会一直等待来自PMS的 socket 数据流
    for (;;) {
        alen = sizeof(addr);
        s = accept(lsocket, &addr, &alen);
        for (;;) {
            ...
            //buf 将装载 aapt 指令每个参数
            if (execute(s, buf)) break;
        }
    }
}
step 21 - 25:

先列出这部分所有类的位置 :

  • source\frameworks\base\services\core\java\com\android\server\pm\Installer.java
  • source\frameworks\native\cmds\installd\commands.cpp

这边通过打印可以查看指令字串:
aapt /data/app/com.wsdeveloper.galaxys7-1/base.apk assets/icons/ /data/resource-cache/com.wsdeveloper.galaxys7/icons 50089 98 0

   下面代码中将执行 cmdsinfo 中对应的函数,arg 数组保存在所有参数的地址,接下来看下cmdsinfo 数组中的各个位对应的函数。

/* Tokenize the command buffer, locate a matching command,
 * ensure that the required number of arguments are provided,
 * call the function(), return the result.
 */
static int execute(int s, char cmd[BUFFER_MAX])
{
    ...
    for (i = 0; i < sizeof(cmds) / sizeof(cmds[0]); i++) {
        if (!strcmp(cmds[i].name,arg[0])) {
            if (n != cmds[i].numargs) {
                ALOGE("%s requires %d arguments (%d given)\n",
                     cmds[i].name, cmds[i].numargs, n);
            } else {
                //ALOGE("wangjian func arg + 1 :%d reply :%s "(arg + 1),reply.string());
                ret = cmds[i].func(arg + 1, reply);
            }
            goto done;
        }
}

   多主题功能在 cmds 中添加了两个对应参数 aaptaapt_with_common,也就是说刚才输入的参数指令,会调用 cmds[19] 中对应的 do_aapt 函数。在do_aapt函数也是做了很多判断和过滤,主要功能代码在run_aapt函数中。

struct cmdinfo cmds[] = {
   
    ...
    { "aapt",                 7, do_aapt },
    { "aapt_with_common",     8, do_aapt_with_common },
    ...
};

static int do_aapt(char **arg, char reply[REPLY_MAX] __unused)
{
    return aapt(arg[0], arg[1], arg[2], atoi(arg[3]), atoi(arg[4]), atoi(arg[5]), arg[6], "");
}

函数中考虑情况太多,我只贴出用到的主干部分代码,这边打印出各个参数的值:

  • source_apk : /data/app/com.wsdeveloper.galaxys7-1/base.apk
  • internal_path : assets/icons/
  • out_restable : /data/resource-cache/com.wsdeveloper.galaxys7/icons
  • uid : 50089
  • pkgId : 98
  • min_sdk_version : 0
  • app_res_path : null
  • common_res_path : null

   此时 app_res_pathcommon_res_path 值是为空的,因此下面函数中不走的代码都注释掉了。最后执行了 execl 函数,定义在 unistd.h 头文件。从代码中得知,最终会通过 execl 函数来启动 aapt tools 工具,具体 aapt tools 功能入口的代码在源码中的位置 : ' /frameworks/base/tools/aapt/Main.cpp '。

static void run_aapt(const char *source_apk, const char *internal_path,
                     int resapk_fd, int pkgId, int min_sdk_version,
                     const char *app_res_path, const char *common_res_path)
{
    static const char *AAPT_BIN = "/system/bin/aapt";
    ...
    static const size_t MAX_INT_LEN = 32;
    char resapk_str[MAX_INT_LEN];
    char pkgId_str[MAX_INT_LEN];
    char minSdkVersion_str[MAX_INT_LEN];

    bool hasCommonResources = (common_res_path != NULL && common_res_path[0] != '\0');
    bool hasAppResources = (app_res_path != NULL && app_res_path[0] != '\0');

    if (hasCommonResources) {
        ...
    } else {
        // 执行"/system/bin/"目录下的aapt工具, 其功能代码在framework/base/tools/aapt/下
        execl(AAPT_BIN, AAPT_BIN, "package",
                          "--min-sdk-version", minSdkVersion_str,
                          "-M", MANIFEST,
                          "-S", source_apk,
                          "-X", internal_path,
                          "-I", FRAMEWORK_RES,
                          "-r", resapk_str,
                          "-x", pkgId_str,
                          "-f",
                          hasAppResources ? "-I" : (char*)NULL,
                          hasAppResources ? app_res_path : (char*) NULL,
                          (char*)NULL);
    }
    ALOGE("execl(%s) failed: %s\n", AAPT_BIN, strerror(errno));
}
step 26 - 28:

先列出这部分所有类的位置 :

  • source\frameworks\base\tools\aapt\Main.cpp

   从这部分开始,主题的打包工作正式交由 aapt tools工具来执行。首先由入口函数 Main() 将打包的一系列参数全都封装进Bundle中,结合 (step 21 - 25 )代码可以知道每个命令对应参数代表的意思。最后由 handleCommand()函数来分配任务。

/*
 * Parse args.
 */
int main(int argc, char* const argv[])
{
    char *prog = argv[0];
    Bundle bundle;
    ...
    /* 设置默认的压缩方式 */
    bundle.setCompressionMethod(ZipEntry::kCompressDeflated);
    ...
    if (argv[1][0] == 'v')
        ...
    // package 对应功能是打包资源的操作,kCommandPackage 对应 doPackage 函数
    else if (argv[1][0] == 'p') 
        bundle.setCommand(kCommandPackage);
    else {
        goto bail;
    }
    /*
     * Pull out flags.  We support "-fv" and "-f -v".
     */
    while (argc && argv[0][0] == '-') {
        /* flag(s) found */
        const char* cp = argv[0] +1;

        while (*cp != '\0') {
            switch (*cp) {
            case '-':
                if (strcmp(cp, "-debug-mode") == 0) {
                    ...
                } else if (strcmp(cp, "-min-sdk-version") == 0) {
                    ...
                    bundle.setMinSdkVersion(argv[0]); //设置最小sdk版本
                }
                break;
            case 'M':
                ...
                bundle.setAndroidManifestFile(argv[0]); 
                break;
            case 'S':
                ...
                bundle.addResourceSourceDir(argv[0]); // 设置主题包资源 apk 的绝对路径
                break;
            case 'X':
                ...
                bundle.setInternalZipPath(argv[0]); // 设置主题查找路径前缀
                break;
            case 'I':
                bundle.addPackageInclude(argv[0]);// 设置原生资源包“framework-res.apk”的绝对路径
                break;
            case 'r':
                ...
                bundle.setOutputResApk(argv[0]);// 设置主题“索引包”的绝对路径
                break;
            case 'x':
                ...
                bundle.setExtendedPackageId(atoi(argv[0]));//设置主题包“索引包”的 package ID
                break;
          default:
                goto bail;
       }
    ...
    bundle.setFileSpec(argv, argc);
    result = handleCommand(&bundle);
    return result;
}

/*
 * Dispatch the command.
 */
int handleCommand(Bundle* bundle)
{
    switch (bundle->getCommand()) {
    ...
    case kCommandPackage:      return doPackage(bundle);
    ...
    default:
        return 1;
    }
}

三、aapt 工具打包

step 29 - 72:

先列出这部分所有类的位置 :

  • source\frameworks\base\tools\aapt\Command.cpp
  • source\frameworks\base\tools\aapt\AaptAssets.cpp
  • source\frameworks\base\tools\aapt\AaptConfig.cpp
  • source\frameworks\base\tools\aapt\Resource.cpp
  • source\frameworks\base\tools\aapt\ResourceTable.cpp
  • source\frameworks\base\tools\aapt\ApkBuilder.cpp
  • source\frameworks\base\tools\aapt\Package.cpp
  • source\frameworks\base\tools\aapt\ZipEntry.cpp
  • source\frameworks\base\tools\aapt\OutputSet.cpp
  • source\frameworks\base\tools\aapt\ZipEntry.cpp
  • source\frameworks\base\tools\aapt\StringPool.cpp
  • source\frameworks\base\libs\androidfw\AssetManager.cpp

   前面所有的步骤都是在为这部分服务,用一句话概括下面要分析函数就是:打包索引apk。但是过程相当复杂,关键代码都需要分将近40个步骤来记录。
   结合上部分代码可以了解到 Bundle 中保存的变量有哪些,也可以查看下图(红框框起来的参数不需要考虑)。即将要分析的是这些变量在打包过程中的作用,整个打包的流程全部都在函数 doPackage 中完成的。

图5 Bundle 储存的变量

   在分析 doPackage 详细工作之前需要了解一下 索引apk 中有那些文件需要打包。从' 图6 ' 可以看出 apk 中存在三个资源文件,' AndroidManifest.xml ' 和 ' appfilter.xml ' 文件最终会以二进制方式打包进apk中,现在只分析 ' resources.arsc '的生成过程,' resources.arsc '可以理解为资源索引表,其表结构的描述可以提前看下下面大神的博客,具体生成过程还得看流程:
   https://www.jianshu.com/p/3cc131db2002

图 6

先整体看下这个函数的主干部分:

/*
 * Package up an asset directory and associated application files.
 */
int doPackage(Bundle* bundle)
{
    ...
    sp<AaptAssets> assets;
    ...
    sp<ApkBuilder> builder;
    ...
    //实例化出 AaptAssets 对象,用来收集“资源apk”中所有的资源
    assets = new AaptAssets();

    //通过给定的参数,开始收集资源
    err = assets->slurpFromArgs(bundle);

    //在解析过程中发生问题导致收集有误,就立即返回
    if (err < 0) {
        goto bail;
    }

    ...

    //创建Apkbuilder 对象,用来将已经收集完毕的资源信息打包,编译出最终apk文件。
    builder = new ApkBuilder(configFilter);

    //分包处理,这边不需要分析
    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)) {
                goto bail;
            }

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

    // 编译收集到的资源,此时 assets 中保存了所有资源的基本信息
    if (bundle->getResourceSourceDirs().size() || bundle->getAndroidManifestFile()) {
        err = buildResources(bundle, assets, builder);
        if (err != 0) {
            goto bail;
        }
    }

    // Write the apk
    if (outputAPKFile || bundle->getOutputResApk()) {
        // 将所有收集到的资源添加到Builder中
        err = addResourcesToBuilder(assets, builder);
        if (err != NO_ERROR) {
            goto bail;
        }
    }

    ALOGW("aapt, write apk ------");

    //Write the res apk
    if (bundle->getOutputResApk()) {
        const char* resPath = bundle->getOutputResApk();
        char *endptr;
        int resApk_fd = strtol(resPath, &endptr, 10);

        if (*endptr == '\0') {
            //生成最终apk
            err = writeAPK(bundle, resApk_fd, builder->getBaseSplit(), true);
        } else {

        }

        if (err != NO_ERROR) {
            goto bail;
        }
        ALOGW("aapt, write Res apk OK ");
    }
}

   先大体概括一下doPackage 为主题包做了那些工作:
   1. 收集资源信息,将主题资源包中的所有资源信息全部打包进 AaptAssets 中;
   2. 编译资源,将所有收集到的资源进行编译,并将资源信息赋给 APKBuilder;
   3. 由 APKBuilder 来进行最终的打包APK工作。
(以上待分析完在更正)

收集资源信息

   在编译资源之前需要先将主题包中所有的资源信息全部收集起来,再统一处理。等这段流程走完再回过头来分析 AaptAsset 对象的结构。图 7 是 icon 目录下所有的图标资源,接下来就以' aospMusic.png '为例来记录整个收集流程。

图 7 主题包 icon 目录

   这个函数的主要工作:先收集' AndroidManifest.xml '文件信息,里面包含了主题包的包名,在编译资源时会用到。接着从Bundle中取出主题包资源apk路径并解压,最终调用本类的 slurpResourceZip()收集压缩包中的资源信息。

ssize_t AaptAssets::slurpFromArgs(Bundle* bundle)
        {
    int count;
    int totalCount = 0;
    FileType type;
    // 主题包资源 apk 的绝对路径
    const Vector<const char *>& resDirs = bundle->getResourceSourceDirs();
    // 集合中只有一个资源路径
    const size_t dirCount =resDirs.size();
    sp<AaptAssets> current = this;
    ...
    /*
     * AndroidManifest.xml 存在的话就先将这个文件加入到 AaptAsset 中
     */
    if (bundle->getAndroidManifestFile() != NULL) {
        // place at root of zip.
        String8 srcFile(bundle->getAndroidManifestFile());
        addFile(srcFile.getPathLeaf(), AaptGroupEntry(), srcFile.getPathDir(),
            NULL, String8());
        //记录收集的资源总数
        totalCount++;
    }

    /*
     * 由于集合中只存在主题包资源,所有只需要对主题包进行资源信息收集即可
     */
    for (size_t i=0; i<dirCount; i++) {
        const char *res = resDirs[i];
        if (res) {
            //定义在 misc.cpp 的函数,判断当前文件类型
            type = getFileType(res);
            ...
            if (type == kFileTypeDirectory) {
                ...
            } else if (type == kFileTypeRegular) { //.apk文件为资源文件类型
                ZipFile* zip = new ZipFile;
                //创建ZipFile对象来解压缩主题包文件
                status_t err = zip->open(String8(res), ZipFile::kOpenReadOnly);
                if (err != NO_ERROR) {
                    delete zip;
                    totalCount = -1;
                    goto bail;
                }
                //核心函数,准备收集主题包内的资源信息
                count = current->slurpResourceZip(bundle, zip, res);
                delete zip;
                if (count < 0) {
                    totalCount = count;
                    goto bail;
                }
            } else {
                ...
                return UNKNOWN_ERROR;
            }
        }
    }

   解压出' /data/app/com.wsdeveloper.galaxys7-1/base.apk '下所有的文件,每个文件对应一个 ZipEntry对象,此时 current->slurpResourceZip开始遍历出主题包中所有的资源文件。从' 图1 '可以知道,一个主题包中包含了ROM中需要的所有资源,但是对于' Icon资源 '的编译打包来说,是不需要把其他资源也一并打包进来的。所以在收集Icon资源时,需要先过滤掉同包中的其他类型资源,单独打包' Icon资源 ',看下 addEntry具体如何收集信息。

AaptAssets::slurpResourceZip(Bundle* bundle, ZipFile* zip, const char* fullZipPath)
{
    status_t err = NO_ERROR;
    int count = 0;
    SortedVector<AaptGroupEntry> entries;

    //ZipFile 解压出主题包中的总文件数
    const int N = zip->getNumEntries();
    for (int i=0; i<N; i++) {
        ZipEntry* entry = zip->getEntryByIndex(i);
        //过滤掉internalPath以外的文件路径,对于打包 Icon 资源,这里只解析assets/icon下的资源文件
        if (!isEntryValid(bundle, entry)) {
            continue;
        }
        //entryName 对应具体资源文件路径: "assets/icons/res/drawable-xxxhdpi/aospMusic.png"
        String8 entryName(entry->getFileName());
        //entryLeaf 对应具体资源文件名: "aospMusic.png"
        String8 entryLeaf = entryName.getPathLeaf();
        //entryDirFull 对应具体资源文件夹路径: "assets/icons/res/drawable-xxxhdpi"
        String8 entryDirFull = entryName.getPathDir();
        //entryDir 对应具体资源文件类型: "drawable-xxxhdpi"
        String8 entryDir = entryDirFull.getPathLeaf();
        //收录资源
        err = addEntry(entryName, entryLeaf, entryDirFull, entryDir, String8(fullZipPath), 0);
        if (err) continue;

        count++;
    }

    return count;
}

   全部的收集过程都在下面的代码中,该函数的逻辑可以分几个阶段来分析:

AaptAssets::addEntry(const String8& entryName, const String8& entryLeaf,
                         const String8& /* entryDirFull */, const String8& entryDir,
                         const String8& zipFile, int compressionMethod)
{
    AaptGroupEntry group;
    String8 resType;
    //通过initFromDirName函数确定该资源的类型,同时作为AaptFile的成员变量
    bool b = group.initFromDirName(entryDir, &resType);
    if (!b) {
        return -1;
    }
    //先从缓存中查找是否已经创建了属于该资源类型的AaptDir对象,没有就创建并保存在mDir中
    sp<AaptDir> dir = makeDir(resType); //Does lookup as well on mdirs
    //为 aospMusic.png 创建对应的 AaptFile 对象
    sp<AaptFile> file = new AaptFile(entryName, group, resType, zipFile);
    file->setCompressionMethod(compressionMethod);
    //先从缓存mFiles中查找是否已经创建了属于该资源的AaptFile对象,存在即通过对应的AaptGroup将AaptFile保存
    dir->addLeafFile(entryLeaf, file);
    //将AaptDir添加进mResDirs缓存
    sp<AaptDir> rdir = resDir(resType);
    if (rdir == NULL) {
        mResDirs.add(dir);
    }

    return NO_ERROR;
}

第1步,确定资源类型

   initFromDirName的工作是为了解析出资源的类型,确定 resType ,并为AaptGroupEntryConfigDescription属性确定配置信息。
   AaptConfig::parse函数不贴出来了,具体逻辑就是通过对' -xxxhdpi '的解析来确定ConfigDescription的配置信息 ' density '和' sdkVersion '的值。

bool AaptGroupEntry::initFromDirName(const char* dir, String8* resType)
{
    //查找字符串dir中首次出现字符'-'的地址,如'drawable-xxxhdpi'返回的为‘-’的地址
    const char* q = strchr(dir, '-');
    size_t typeLen;
    //获取常规资源类型,如drawable-xxxhdpi,常规资源类型为 drawable
    if (q != NULL) {
        typeLen = q - dir;//获取"drawable"长度
    } else {
        typeLen = strlen(dir);
    }

    String8 type(dir, typeLen);
    //如果不是正常的资源类型,直接pass, resType 为空。
    if (!isValidResourceType(type)) {
        return false;
    }
    if (q != NULL) {
        //通过对"-xxxhdpi"的解析来对 mParams 的属性赋值 (out->density = ResTable_config::DENSITY_XXXHIGH)
        if (!AaptConfig::parse(String8(q + 1), &mParams)) {
            return false;
        }
    }

    *resType = type;
    return true;
}

bool isValidResourceType(const String8& type)
{
    return type == "anim" || type == "animator" || type == "interpolator"
        || type == "transition"
        || type == "drawable" || type == "layout"
        || type == "values" || type == "xml" || type == "raw"
        || type == "color" || type == "menu" || type == "mipmap";
}

   此时* resType指向' drawable ',而 AaptGroupEntryConfigDescription属性信息如下(ConfigDescription 继承自 ResTable_config):

density(像素密度) sdkVersion mcc(移动国家码 ) mnc(移动网络码) ...
ResTable_config::DENSITY_XXXHIGH 4 null null null

第2步,通过第一步确定的资源类型,创建AaptDir对象,并将 drawable 资源信息添加进去

sp<AaptDir> AaptDir::makeDir(const String8& path)
{
    String8 name;
    String8 remain = path;

    sp<AaptDir> subdir = this;
    // 获取remain所描述文件的根目录
    // remain = “drawable”,name=“drawable”,remain = “”
    while (name = remain.walkPath(&remain), remain != "") {
        subdir = subdir->makeDir(name);
    }

    ssize_t i = subdir->mDirs.indexOfKey(name);
    if (i >= 0) {
        //如果该path已经存在于mDirs中,就直接返回
        return subdir->mDirs.valueAt(i);
    }
    sp<AaptDir> dir = new AaptDir(name, subdir->mPath.appendPathCopy(name));
    //将该path添加到mDirs中,并返回
    subdir->mDirs.add(name, dir);
    return dir;
}

   此时AaptAsset::AaptDir 中保存了的属性状态:

mLeaf (资源目录名称) mPath (资源目录路径) mFiles(AaptGroup集合) mDirs(AaptDir集合)
drawable drawable size: 0 size: 1

第3步,为 aospMusic.png 创建一个AaptFile对象

attribute value
mSourceFile (资源全路径) assets/icons/res/drawable-xxxhdpi/aospMusic.png
mGroupEntry (资源配置信息) density:ResTable_config::DENSITY_XXXHIGH,sdkVersion: 4
mResourceType (资源类型) drawable
mZipFile(资源包路径) /data/app/com.wsdeveloper.galaxys7-1/base.apk
mData (数据源) null
... ...

以上的AaptFile对象目前储存的属性状态。

第4步,为' asopMusic.png '资源创建一个AaptGroup对象,并将AaptFile 加入到AaptGroup.mFile中去,最终将 AaptGroup与其对应的资源名称' leafName '一同打包进AaptDir.mFiles

status_t AaptDir::addLeafFile(const String8& leafName, const sp<AaptFile>& file,
        const bool overwrite, const bool isAssetsDir)
{
    sp<AaptGroup> group;

#ifdef MTK_GMO_ROM_OPTIMIZE
    ...
#else
    UNUSED(isAssetsDir);
#endif
    //此时 mFiles 中是不包含"asopMusic.png"这个文件名的
    if (mFiles.indexOfKey(leafName) >= 0) {
        group = mFiles.valueFor(leafName);
    } else {
        //为"asopMusic.png" 创建一个 AaptGroup
        group = new AaptGroup(leafName, mPath.appendPathCopy(leafName));
        //将"asopMusic.png"文件名和对应的 AaptGroup 添加进 mFiles
        mFiles.add(leafName, group);
    }
    //将第3步初始化完成的AaptFile 添加到 group 中
    return group->addFile(file, overwrite);
}

   AaptGroup::addFile做了两件事,将自己的属性mPath值替换成AaptFile.mPath,将AaptFile 与对应的AaptGroupEntry添加至mFiles中。这几部分析完在回头分析一下,这几个重要类的结构关系。

status_t AaptGroup::addFile(const sp<AaptFile>& file, const bool overwriteDuplicate)
{
    //先从 mFiles 查找是否有同配置信息的 AaptGroupEntry
    ssize_t index = mFiles.indexOfKey(file->getGroupEntry());
    if (index >= 0 && overwriteDuplicate) {
        removeFile(index);
        index = -1;
    }

    if (index < 0) {
        //赋值
        file->mPath = mPath;
        //以AaptGroupEntry为key 将 AaptFile 加入到AaptGroup:mFiles中
        mFiles.add(file->getGroupEntry(), file);
        return NO_ERROR;
    }
}

   最终,经过以上几步的信息收集,关于' aospMusic.png '的资源信息全部都收集到各个类中,最终打包到AaptAsset.mResDirs集合中。通过一个表格来看下各个类的状态:

*AaptGroupEntry

density(像素密度) sdkVersion mcc(移动国家码 ) mnc(移动网络码) ...
ResTable_config::DENSITY_XXXHIGH 4 null null null

*AaptFile

attribute value
mPath(资源路径) drawable/aospMusic.png
mSourceFile (资源全路径) assets/icons/res/drawable-xxxhdpi/aospMusic.png
mGroupEntry (资源配置信息) *AaptGroupEntry
mResourceType (资源类型) drawable
mZipFile(资源包路径) /data/app/com.wsdeveloper.galaxys7-1/base.apk
mData (数据源) null
... ...

*AaptGroup

attribute value
mLeaf(资源名称) aospMusic.png
mPath(资源路径) drawable/aospMusic.png
mFiles (*AaptFile集合) key:*AaptGroupEntry,value: *AaptFile,目前size : 1

*AaptDir

attribute value
mLeaf (资源目录名称) drawable
mPath (资源目录路径) drawable
mFiles(*AaptGroup集合) key: "aospMusic.png",value: *AaptGroup,目前size : 1
mDirs(AaptDir集合) *AaptDir集合,目前size : 1

通过上面几个表格,理一下这几个类的关系:

  • AaptAsset 对象表示正在编译的整个资源,;
    他的几个重要属性:
    String8 mPackage :表示正在编译的主题包的包名;
    Vector<sp<AaptDir> > mResDirs :表示正在编译的目录集合,保存所有收集到的 AaptDir
    KeyedVector<String8, sp<ResourceTypeSet> >* mRes:表示正在编译的资源包的资源类型集合,以资源类型分类来保存;
  • AaptDir 对象表示正在编译的资源目录
    他的几个重要属性:
    String8 mLeaf:表示正在编译的资源目录名称,主题包的话就只有"drawable"这个目录;
    String8 mPath:表示正在编译的资源目录路径;
    DefaultKeyedVector<String8, sp<AaptGroup> > mFiles:表示正在编译的资源集合,这里需要强调一下,这个集合以资源名为key保存的。以"aospMusic.png"为例,一个"aospMusic.png"对应着一个AaptGroup。AaptGroup 在下面解释。;
  • AaptGroup 表示一个同名资源在不同配置文件夹下的组合;
    他的几个重要属性:
    String8 mLeaf:表示一个单一的资源名,如"aospMusic.png";
    String8 mPath:表示"aospMusic.png"资源的公共路径;
    DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> > mFiles:表示"aospMusic.png"资源在不同配置下对应的AaptFile集合,比如手机为Nexus 6,那么它的屏幕像素密度为 640dpi,此时匹配到的AaptGroupEntry.densityResTable_config::DENSITY_XXXHIGH,对应的资源为drawable-xxxhdpi目录下的"aospMusic.png"。
  • 一个 AaptFile 对应着一个资源;
    他的几个重要属性:
    String8 mPath:表示资源路径;
    AaptGroupEntry mGroupEntry:表示当前的资源对应的配置信息,上面有说到;
    String8 mResourceType:资源类型,"aospMusic.png"为drawable类型;
    String8 mSourceFile:表示目前需要编译的资源在 apk 中的绝对路径;
    void* mData:二进制数据;
    size_t mDataSize:数据大小;
    int mCompression:压缩方式;
    String8 mZipFile:需要解压的主题包资源路径。

它们的之间的关系就是:
   AaptAsset中保存着所有的AaptDir,AaptDir中以资源目录来分类(比如layout、values、还有本次重点分析的drawable),保存着每个AaptGroup,而AaptGroup中又是以资源名来分,分别保存着各个配置对应的AaptFile,此时AaptFile已经为最小单位了,这样解释有点直白了。

   收集流程算是分析完毕,回到slurpResourceZip这个函数可以知道,这样的流程需要循环执行将ZipFile中的所有资源全部收集到AaptAsset中,由于目前只分析了' aospMusic.png '这个资源文件。图标资源文件加上' appfilter.xml '文件和' AndroidManifest.xml '文件,如果全部收集完成,AaptFile对象的数量应该是 38 个。

编译资源

   此时AaptAsset基本文件算是收集完成了,之前有简要概括过这一部分的工作,接下好好看看buildResources函数是如何编译这些资源的,先分析流程在总结。
   此函数需要的三个参数分别是:Bundle*AaptAssetsApkBuilder。可以从' 图 5 ' 知道Bundle*中目前储存着哪些属性,AaptAssets才分析过,而ApkBuilder为空。下面代码经过删除,只留下主干部分,先大体看一下吧:

status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuilder>& builder)
{
    // slurpFromArgs 函数开始时已经将"AndroidManifest.xml"收集到 AaptAssets 中。
    sp<AaptGroup> androidManifestFile =
            assets->getFiles().valueFor(String8("AndroidManifest.xml"));
    if (androidManifestFile == NULL) {
        return UNKNOWN_ERROR;
    }
    //解析androidManifest.xml,需要从中解析出 package name
    status_t err = parsePackage(bundle, assets, androidManifestFile);
    if (err != NO_ERROR) {
        return err;
    }
   //为 ResourceTable::PackageType 确定类型
   ResourceTable::PackageType packageType = ResourceTable::App;
    if (bundle->getBuildSharedLibrary()) {
        ...
    } else if (bundle->getExtending()) { //标注主题包类型为System类型
        packageType = ResourceTable::System;
    } 
    //可以从step 21 - 25中查到,主题包的 packageId 等于 98
    int extendedPackageId = bundle->getExtendedPackageId();
    // 根据package信息创建 ResourceTable
    ResourceTable table(bundle, String16(assets->getPackage()), packageType, extendedPackageId);
    //将 framework_res.apk 路径添加进 path
    err = table.addIncludedResources(bundle, assets);
    if (err != NO_ERROR) {
        return err;
    }
    // resType -> leafName -> group
    KeyedVector<String8, sp<ResourceTypeSet> > *resources = 
            new KeyedVector<String8, sp<ResourceTypeSet> >;
    //将之前收集的所有信息打包进 resources 集合
    collect_files(assets, resources);
    //只看 drawables 类型
    sp<ResourceTypeSet> drawables;
    sp<ResourceTypeSet> layouts;
    ...
    ...
    sp<ResourceTypeSet> mipmaps;

    /*将resources中保存的各资源类型的Set拆分,并保存到drawable 类型的Set中去 */
    ASSIGN_IT(drawable);
    ...
    ASSIGN_IT(mipmap);
    //将已经收集的resources赋值给AaptAssets的 mRes 集合变量
    assets->setResources(resources);
    ...
    // 将drawable类型的 ResourceTypeSet 资源整合进 ResourceType 资源表
    if (drawables != NULL) {
        ...
        if (err == NO_ERROR) {
            err = makeFileResources(bundle, assets, &table, drawables, "drawable");
            if (err != NO_ERROR) {
                hasErrors = true;
            }
        }
    }
    // 将 xml 类型的 ResourceTypeSet 资源整合进 ResourceType 资源表
    if (xmls != NULL) {
        err = makeFileResources(bundle, assets, &table, xmls, "xml");
        if (err != NO_ERROR) {
            hasErrors = true;
        }
    }
    ...
   // compile resources
   // 编译values下面的资源信息,这里不讨论
    ...

    //为已经收集到 resource table 的 Bags 类型资源 分配资源ID,这里只分析 TYPE_ITEM类型资源
    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;
    
    if (table.hasResources()) {
        ...
        //不分包处理, numSplits 为1
        Vector<sp<ApkSplit> >& splits = builder->getSplits();
        const size_t numSplits = splits.size();

        for (size_t i = 0; i < numSplits; i++) {
            // ApkBuilder 内部类 ApkSplit
            sp<ApkSplit>& split = splits.editItemAt(i);
            // 创建一个名为"resources.arsc"的 AaptFile
            sp<AaptFile> flattenedTable = new AaptFile(String8("resources.arsc"),
                    AaptGroupEntry(), String8());
            //组织一下"resources.arsc"的数据表
            err = table.flatten(bundle, split->getResourceFilter(),
                    flattenedTable, split->isBase());
            if (err != NO_ERROR) {
                return err;
            }
            //将"resources.arsc"于对应的aaptFile 插入到split :mFiles中去
            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;
                }
            } 
        }
    }
    if (resFile != NULL) {
        // 将这个资源表加入到AssetManager 中供其他模块应用
        err = assets->addIncludedResources(resFile);
        if (err < NO_ERROR) {
            fprintf(stderr, "ERROR: Unable to parse generated resources, aborting.\n");
            return err;
        }
    }
    return err;
 }

   这个函数真的长,差点绕晕了,整理完剩下的就是现在需要分析的部分了。接下来逻辑代码分几个步骤来分析:

第1步,获取包名

   从' AndroidManifest,xml '文件中获取到 package信息,赋值给AaptAsset.mPackage
这下AaptAsset对应的包名也确定了。接着往下面分析吧,到后面将AaptAsset表整理出来看看保存了什么。

static status_t parsePackage(Bundle* bundle, const sp<AaptAssets>& assets,
    const sp<AaptGroup>& grp)
{
    ...
    //AndroidManifest.xml 文件对应的 AaptFile
    sp<AaptFile> file = grp->getFiles().valueAt(0);

    ResXMLTree block;
    //初始化 ResXMLTree 来解析 xml 文件内容
    status_t err = parseXMLResource(file, &block);
    if (err != NO_ERROR) {
        return err;
    }

    ResXMLTree::event_code_t code;
    while ((code=block.next()) != ResXMLTree::START_TAG
           && code != ResXMLTree::END_DOCUMENT
           && code != ResXMLTree::BAD_DOCUMENT) {
    }

    size_t len;
    if (code != ResXMLTree::START_TAG) {
        return UNKNOWN_ERROR;
    }
    ...
    //check 解析出的 Attribute 是否有包含 "package" 属性
    ssize_t nameIndex = block.indexOfAttribute(NULL, "package");
    if (nameIndex < 0) {
        return UNKNOWN_ERROR;
    }
    //将 "package"属性对应的vaule赋值给AaptAsset的 mPackage 属性
    assets->setPackage(String8(block.getAttributeStringValue(nameIndex, &len)));
}
第2步,初始化资源表。

先看一下罗老师博客上画的图:


ResourceTable 结构

   接下来的代码逻辑可以结合罗老师的架构图来看,通过上面核心代码buildResources()可以知道,此时创建 ResourceTable的四个参数已经确认:

  • Bundle* bundle :结合 图5 和上面代码可以知道
  • String16& assetsPackage :com.wsdeveloper.galaxys7
  • ResourceTable::PackageType type :System
  • ssize_t pkgIdOverride :98 ,16进制 0x62
/* Add for theme start */
ResourceTable::ResourceTable(Bundle* bundle, const String16& assetsPackage, ResourceTable::PackageType type, ssize_t pkgIdOverride)
    : mAssetsPackage(assetsPackage)
    , mPackageType(type)
    , mTypeIdOffset(0)
    , mIsmtkCur(bundle->getmtkCur()), mIsmtkCurNum(bundle->getmtkCurNum()) ///M: add mediatek-res.apk and 3rd-party resource package.
    , mNumLocal(0)
    , mBundle(bundle)
{
    ssize_t packageId = -1;
    //通过 mPackageType 来确定 packageId,但是在编译主题包的这个流程上,pkgIdOverride会覆盖原有的packageId。
    switch (mPackageType) {
          ...
    }
    //覆盖原来的packageId
    if (pkgIdOverride != 0) {
        packageId = pkgIdOverride;
    }
    /* add for theme end */
    //初始化对应的 Package 对象,并添加进 mPackages 中。
    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);
    //确定 ResourceTable 的各属性值
    getType(mAssetsPackage, String16("attr"), unknown);
}

   多主题结构有改造ResourceTable的构造函数,添加了pkgIdOverride这个参数,默认情况下 pkgIdOverride为0,当编译的包为主题包时,会覆盖原先由PackageType确定的packageId。紧接着由主题包名和packageId实例化对应的Package对象并保存到mPackagesmOrderedPackages中。

sp<ResourceTable::Type> ResourceTable::getType(const String16& package,
                                               const String16& type,
                                               const SourcePos& sourcePos,
                                               bool doSetIndex)
{
    //取出刚保存的Package对象
    sp<Package> p = getPackage(package);
    if (p == NULL) {
        return NULL;
    }
    //在确定Package对象的各属性值
    return p->getType(type, sourcePos, doSetIndex);
}

   getType()的工作就是通过包名取出刚在保存在mPackage集合中的Package对象,并接着向Package对象中mTypes集合添加新Type

sp<ResourceTable::Type> ResourceTable::Package::getType(const String16& type,
                                                        const SourcePos& sourcePos,
                                                        bool doSetIndex)
{
    //此时mTypes是不包含 "attr "这个type的
    sp<Type> t = mTypes.valueFor(type);
    if (t == NULL) {
        //sourcePos == unknown
        t = new Type(type, sourcePos);
        //保存名为"attr"的 Type
        mTypes.add(type, t);
        mOrderedTypes.add(t);
        ...
    }
    return t;
}

此时ResourceTable的初始化已经完成了,名为' com.wsdeveloper.galaxys7 '的Package对象已经建立,并添加到 ResourceTable.mPackage 集合中。

第3步,为资源表收集AaptAsset中保存的所有资源信息

   初始化一个resources集合来收集主题包资源信息,逐个取出保存在AaptAsset.AaptDir的中资源信息,以资源类型来划分,将所有信息全部添加到resources集合中,最终添加到AaptAssetsmRes 集合中,并同步从AaptAsset.mDirs删除对应的AaptDir

static void collect_files(const sp<AaptAssets>& ass,
        KeyedVector<String8, sp<ResourceTypeSet> >* resources)
{
    //将前面收集到assets中的各类资源文件重新收集到resources中来
    const Vector<sp<AaptDir> >& dirs = ass->resDirs();
    int N = dirs.size();

    for (int i=0; i<N; i++) {
        sp<AaptDir> d = dirs.itemAt(i);
        collect_files(d, resources);
        ...
        //收集完资源信息后将AaptAsset 中保存的AaptDir对象清除
        ass->removeDir(d->getLeaf());
    }
}

static void collect_files(const sp<AaptDir>& dir,
        KeyedVector<String8, sp<ResourceTypeSet> >* resources)
{
    const DefaultKeyedVector<String8, sp<AaptGroup> >& groups = dir->getFiles();
    int N = groups.size();
    for (int i=0; i<N; i++) {
        String8 leafName = groups.keyAt(i);
        //groups 是以文件名为key,AaptGroup为values的集合,
        const sp<AaptGroup>& group = groups.valueAt(i);
        //files 以AaptGroupEntry(ldpi、mdpi和hdpi等)为key AaptFile为values的集合
        const DefaultKeyedVector<AaptGroupEntry, sp<AaptFile> >& files
                = group->getFiles();

        if (files.size() == 0) {
            continue;
        }
        //收集资源信息时分析过,此时AaptFile只有两种类型 drawable 和 xml
        String8 resType = files.valueAt(0)->getResourceType();
        //检查是否已经添加此资源类型
        ssize_t index = resources->indexOfKey(resType);

        if (index < 0) {
            //初始化 ResourceTypeSet
            sp<ResourceTypeSet> set = new ResourceTypeSet();
            //以文件名为key,AaptGroup为values的形式添加
            set->add(leafName, group);
            //resources 以文件类型为key,ResourceTypeSet为values的集合
            resources->add(resType, set);
        } else {
            //取出已经保存了 drawable 类型的集合
            sp<ResourceTypeSet> set = resources->valueAt(index);
            index = set->indexOfKey(leafName);
            //如果 drawable 类型的集合中不包括"aospMusic.png",那就添加新的文件信息
            if (index < 0) {
                set->add(leafName, group);
            } else {
                //如果 drawable 类型的集合中包括"aospMusic.png"的AaptGroup,但是不包括分辨率低的"aospMusic.png"得文件信息,那就添加不同分辨率文件夹下的资源信息。
                sp<AaptGroup> existingGroup = set->valueAt(index);
                for (size_t j=0; j<files.size(); j++) {
                    existingGroup->addFile(files.valueAt(j));
                }
            }
        }
    }
}

   收集完 AaptAsset 保存的资源信息时, KeyedVector<String8, sp<ResourceTypeSet> > *resources中应该保存着两对资源信息,分别以' xml '和' drawable '类型来保存,最终还是得赋值给AaptAsset.mRes

    //将已经收集的resources赋值给AaptAssets的 mRes 集合变量
    assets->setResources(resources);

可以通过表格看下目前mRes中保存着的数据:

ResourceTypeSet drawable 类型:

key value
aospMusic.png *AaptGroup集合
browser.png *AaptGroup集合
... *AaptGroup集合
weather.png *AaptGroup集合

drawable 类型ResourceTypeSet集合中保存了 36 个键值对。

ResourceTypeSet xml 类型:

key value
appFillter.xml *AaptGroup集合

xml 类型ResourceTypeSet集合中保存了 1 个键值对。

此时 mRes 为啥没有保存 AndroidManifest.xml 的信息?因为 AndroidManifest.xml 不属于任何类型的资源文件。

第4步,往资源表中收集 drawable 和 xml 类型的资源信息。
static status_t makeFileResources(Bundle* bundle, const sp<AaptAssets>& assets,
                                  ResourceTable* table,
                                  const sp<ResourceTypeSet>& set,
                                  const char* resType)
{
    String8 type8(resType);
    String16 type16(resType);

    bool hasErrors = false;

    ResourceDirIterator it(set, String8(resType));
    ssize_t res;
    //循环取出各个类型的资源信息
    while ((res=it.next()) == NO_ERROR) {
        String16 baseName(it.getBaseName());
        const char16_t* str = baseName.string();
        const char16_t* const end = str + baseName.size();
        while (str < end) {
            if (!((*str >= 'a' && *str <= 'z')
                    || (*str >= '0' && *str <= '9')
                    || *str == '_' || *str == '.')) {
                hasErrors = true;
            }
            str++;
        }
        String8 resPath = it.getPath();
        resPath.convertToResPath();
        /* 将一个资源文件封装为一个Entry添加到ResourceTable中去 */
        table->addEntry(SourcePos(it.getPath(), 0), String16(assets->getPackage()),
                        type16,
                        baseName,
                        String16(resPath),
                        NULL,
                        &it.getParams());
        //将重新组织的资源信息添加到 assets 中去
        assets->addResource(it.getLeafName(), resPath, it.getFile(), type8);
    }

    return hasErrors ? STATUST(UNKNOWN_ERROR) : NO_ERROR;
}

makeFileResources()的工作可以继续分几步来分析:

1. 创建资源迭代器;
2. 遍历ResourceTypeSet资源,将资源信息添加到资源迭代器的属性中;

之前有介绍过ResourceTypeSet对象的结构,next()函数主要是将每个资源信息都解压出来,根据当前配置和其他因素来确定当前资源的名称 mLeafName、配置信息系 mParams、资源路径 mPath等等。

ssize_t next()
    {
        while (true) {
            sp<AaptGroup> group;
            sp<AaptFile> file;

            // Try to get next file in this current group.
            if (mGroup != NULL && mGroupPos < mGroup->getFiles().size()) {
                group = mGroup;
                file = group->getFiles().valueAt(mGroupPos++);

            // Try to get the next group/file in this directory
            } else if (mSetPos < mSet->size()) {
                mGroup = group = mSet->valueAt(mSetPos++);
                if (group->getFiles().size() < 1) {
                    continue;
                }
                file = group->getFiles().valueAt(0);
                mGroupPos = 1;

            // All done!
            } else {
                return EOD;
            }

            mFile = file;

            String8 leaf(group->getLeaf());
            mLeafName = String8(leaf);
            mParams = file->getGroupEntry().toParams();

            mPath = "res";
            //这一步比较关键,toDirName函数很有可能会改变最终的路径信息
            mPath.appendPath(file->getGroupEntry().toDirName(mResType));
            mPath.appendPath(leaf);
            // parseResourceName 函数作用就是将.去掉,取出资源名称
            mBaseName = parseResourceName(leaf);
            if (mBaseName == "") {
                return UNKNOWN_ERROR;
            }
            return NO_ERROR;
        }
    }

以' aospMusic.png '为例,看看解析完出的各变量的值:

   还记得上面流程由通过解析' drawable-xxxhdpi '来确定 mParams的代码,mParamsResTable_config*对象的子类。目前有两个属性' density '和' sdkVersion',可以查看*AaptGroupEntry 属性表格。file->getGroupEntry().toDirName(mResType)最终返回的值是' drawable-xxxhdpi-v4 ',因此 :

  • mPath = res/drawable-xxxhdpi-v4/aospMusic.png
  • mBaseName = aospMusic
String8
AaptGroupEntry::toDirName(const String8& resType) const
{
    String8 s = resType;
    //根据配置信息确定目录后缀,如 drawable 目录最后变成 drawable-xxxhdpi-v4
    String8 params = mParams.toString();
    if (params.length() > 0) {
        if (s.length() > 0) {
            s += "-";
        }
        s += params;
    }
    return s;
}

String8 ResTable_config::toString() const {
    String8 res;
    ...
    if (density != DENSITY_DEFAULT) {
        if (res.size() > 0) res.append("-");
        switch (density) {
            ...
            case ResTable_config::DENSITY_XXXHIGH:
                res.append("xxxhdpi");
                break;
            ...
            default:
                res.appendFormat("%ddpi", dtohs(density));
                break;
        }
    }

   AaptGroupEntry::toDirName是个需要注意的函数,在制作主题包时,如果在' assets '目录下创建的图标目录为' drawable-xxxhdpi '的话,会出现查找不到资源的问题。所以想要解决这个问题要么直接将图标目录设成' drawable-xxxhdpi-v4 ',要么修改AaptGroupEntry::toDirName函数的逻辑,不让加' v4 '后缀。

3.取出资源迭代器中保存的属性,封装为一个个Entry最终添加到ResourceTable中去;
step 53 - 61

可以结合step 53 - 61图和罗老师的ResourceType 结构图来看下面的代码,

status_t ResourceTable::addEntry(const SourcePos& sourcePos,
                                 const String16& package,
                                 const String16& type,
                                 const String16& name,
                                 const String16& value,
                                 const Vector<StringPool::entry_style_span>* style,
                                 const ResTable_config* params,
                                 const bool doSetIndex,
                                 const int32_t format,
                                 const bool overwrite)
{
    ...

    sp<Entry> e = getEntry(package, type, name, sourcePos, overwrite,
                           params, doSetIndex);
    if (e == NULL) {
        return UNKNOWN_ERROR;
    }
    // 获取了一个描述名称为name资源文件的Entry对象之后,把其相关信息组织成一个Item对象然后添加到Entry中。
    // 此时 Entry 的 mType 属性会被设置为 TYPE_ITEM
    status_t err = e->setItem(sourcePos, value, style, format, overwrite);
    if (err == NO_ERROR) {
        mNumLocal++;
    }
    return err;
}

   整个addEntry函数就是为了将一个资源信息打包成Entry对象,类似及AaptFile的作用。与之不同的是,AaptFileAaptAsset中最小单位,Entry对象并不是ResourceTable中最小的单位,Item对象包含在Entry对象内部,表示更具体的资源信息。
   而getEntry函数的工作就是实现资源具体信息结构的重新组合,创建与资源对应的Entry,打包到ConfigList对象中,再将ConfigList对象添加到mConfigs 集合。最终回到addEntry函数中通过当前资源信息组织一个Item,设置到Entry对象中。

sp<ResourceTable::Entry> ResourceTable::Type::getEntry(const String16& entry,
                                                       const SourcePos& sourcePos,
                                                       const ResTable_config* config,
                                                       bool doSetIndex,
                                                       bool overlay,
                                                       bool autoAddOverlay)
{
    //entry :gome_icon_launcher_appstore
    //sourcePos :res/drawable-xxxhdpi-v4/gome_icon_launcher_appstore.png
    int pos = -1;
    sp<ConfigList> c = mConfigs.valueFor(entry);
    if (c == NULL) {
        c = new ConfigList(entry, sourcePos);
        mConfigs.add(entry, c);
        pos = (int)mOrderedConfigs.size();
        mOrderedConfigs.add(c);
        if (doSetIndex) {
            c->setEntryIndex(pos);
        }
    }
    
    ConfigDescription cdesc;
    if (config) cdesc = *config;
    sp<Entry> e = c->getEntries().valueFor(cdesc);
    if (e == NULL) {
        e = new Entry(entry, sourcePos);
        c->addEntry(cdesc, e);
    }
    
    return e;
}

   当资源迭代器属性全部遍历完,可以看下各个ResourceTable对象的成员对象中保存着的数据:
(表示单个资源信息的数据都以"aospMusic.png"为例)

*Item

attribute value
sourcePos(资源最终位置) res/drawable-xxxhdpi-v4/aospMusic.png
value(资源项的原始值) res/drawable-xxxhdpi-v4/aospMusic.png
isId false
parsedValue null
... ...

*Entry

attribute value
mName(资源名) aospMusic
mType(资源项类型) TYPE_ITEM
mPos(资源最终位置) res/drawable-xxxhdpi-v4/aospMusic.png
mItem(资源项数据) *Item
... ...

*ConfigList

attribute value
mName(资源名) aospMusic
mPos(资源最终位置) res/drawable-xxxhdpi-v4/aospMusic.png
mEntries(Entry 集合) 以ConfigDescription 为key*Entry为value的集合
... ...

*Type

attribute value
mName(资源类型名) drawable
mPos(资源类型目录位置) UNKNOWN
mConfigs(ConfigList集合) 以资源名为key*ConfigList为value的集合
mOrderedConfigs(ConfigList集合) 有序*ConfigList集合
mPublic(Public集合) size : 0
... ...

*Package

attribute value
mName(正在编译的包名) com.wsdeveloper.galaxys7
mPackageId 0x62
mTypes(Type 集合) 以资源类型名为key*Type为value的集合
mTypeStringsData(AaptFile类型) null
mKeyStringsData(AaptFile类型) null
mTypeStrings(ResStringPool 类型) null
mKeyStrings(ResStringPool 类型) null
... ...

*ResourceTable

attribute value
mAssetsPackage(正在编译的包名) com.wsdeveloper.galaxys7
mPackageType(资源包类型) System
mAssets(AaptAsset 对象) *AaptAsset
mPackages(Package集合) 以包名为key*Package为value的集合
mOrderedPackages(Package集合) 有序*Package集合
mBundle(aapt 命令参数对象) *Bundle
... ...

   到此为止,代表主题包的ResourceTable资源表已经建成,各个成员对象的具体含义在这就不一一介绍了,可以从上面的表格了解到每个对象之间的包含关系。最终这几个成员对应的数量:

class size value
Package 1 com.wsdeveloper.galaxys7
Type 2 drawable、xml
ConfigList 37 37个资源 Entry
Entry 37 aospMusic.png、browser.png、... 、weather.png
4.将重新组合起来的资源信息添加到ResourceTableAaptAsset中去。

   上述创建ResourceTable资源数据表的逻辑分析结束,但还是需要回到makeFileResources这个函数中,在资源迭代器每遍历出一个资源数据时会向ResourceTable表中添加信息,同时也要向ResourceTableAaptAsset中添加。
   在函数collect_files中有分析过收集完一个AaptDir时会将它从AaptAsset.mDirs中删除,所以此时ResourceTableAaptAsset对象中AaptDir集合应该是空的,需要重新添加新的并且正确的资源信息。看看addResource()函数中做了什么工作:
(此时由资源迭代器重组过的数据,以‘aospMusic.png’为例)

  • leafName :aospMusic.png
  • path:res/drawable-xxxhdpi-v4/aospMusic.png
  • file:还是 aospMusic.png 对应的AaptFile
  • resType:drawable
void AaptAssets::addResource(const String8& leafName, const String8& path,
                const sp<AaptFile>& file, const String8& resType)
{
    //创建一个名为‘res ’的AaptDir对象,并添加到mDirs中去
    sp<AaptDir> res = AaptDir::makeDir(kResString);
    //为当前配置信息匹配最终的dirname
    String8 dirname = file->getGroupEntry().toDirName(resType);
    //再此调用私有的 makeDir 函数,确定mPath的值
    sp<AaptDir> subdir = res->makeDir(dirname);
    //重新将资源信息封装承 AaptGroup
    sp<AaptGroup> grr = new AaptGroup(leafName, path);
    //file 的数据信息还是不变的,将添加到新的 AaptGroup对象中
    grr->addFile(file);
    //将新的 AaptGroup对象添加到新的 AaptDir 中
    subdir->addFile(leafName, grr);
}

   记得在分析AaptAsset收集资源数据信息时,是为'drawable'创建了一个AaptDir对象,现在需要为'res'目录创建一个AaptDir对象,接着通过当前配置获得最终的目录名:drawable-xxxhdpi-v4。
   此时需要注意的是,之前所有创建的临时AaptDirpath属性都是‘drawable’,可以查看之前的AaptDir数据表格。而在addResource函数中为正在编译的资源确定了正确的AaptDir的各属性,并重新为资源封装了新的AaptGroup对象,看下更新后的数据表:

*AaptGroup

attribute value
mLeaf(资源名称) aospMusic.png
mPath(资源路径) res/drawable-xxxhdpi-v4/aospMusic.png
mFiles (*AaptFile集合) 与之前不变

*AaptDir

attribute value
mLeaf (资源目录名称) res
mPath (资源目录路径) res/drawable-xxxhdpi-v4
mFiles(*AaptGroup集合) key: "aospMusic.png",value: *AaptGroup,size : 37
mDirs(AaptDir集合) *AaptDir集合,size : 2
第5步,给保存在ResourceTable资源表中的 Bag 资源分配 ' 资源 ID '

   BAG资源的定义和ID的分配规则可以参考最后我贴出的博客链接,由于主题包中没有BAG类型的资源,可以忽略。

第6步,生成最终的资源索引表

   资源索引表"resources.arsc"是资源查找的基础,保存着所有的资源信息。回顾 buildResources函数,当所有资源都合入到资源表 ResourceTable时,这时就需要从资源表 中逐个取出资源数据压入到名为'resource.arsc'的AaptFile中,最后添加到ApkSplit对象中。

status_t buildResources(Bundle* bundle, const sp<AaptAssets>& assets, sp<ApkBuilder>& builder)
{
    ...
    / --------------------------------------------------------------
    // Generate the final resource table.
    // Re-flatten because we may have added new resource IDs
    // --------------------------------------------------------------
    ResTable finalResTable;
    sp<AaptFile> resFile;
    
    if (table.hasResources()) {
        ...
        //不分包处理, numSplits 为1
        Vector<sp<ApkSplit> >& splits = builder->getSplits();
        const size_t numSplits = splits.size();

        for (size_t i = 0; i < numSplits; i++) {
            // ApkBuilder 内部类 ApkSplit
            sp<ApkSplit>& split = splits.editItemAt(i);
            // 创建一个名为"resources.arsc"的 空的 AaptFile对象
            sp<AaptFile> flattenedTable = new AaptFile(String8("resources.arsc"),
                    AaptGroupEntry(), String8());
            //组织一下"resources.arsc"的数据表数据
            err = table.flatten(bundle, split->getResourceFilter(),
                    flattenedTable, split->isBase());
            if (err != NO_ERROR) {
                return err;
            }
            //将"resources.arsc"对应的 aaptFile 插入到split :mFiles中去
            split->addEntry(String8("resources.arsc"), flattenedTable);

            if (split->isBase()) {
                resFile = flattenedTable;
                err = finalResTable.add(flattenedTable->getData(), flattenedTable->getSize());
                if (err != NO_ERROR) {
                    return err;
                }
            } 
        }
    }

   6.1 组织数据表数据

   flatten函数会遍历资源表 ResourceTable中所有的数据,遍历是以从大到小的方式:*Package-->*Type-->ConfigList-->Entry,每个正在编译资源包中以资源类型划分,比如 ‘icon 索引包’中只有 'xml' 和 'drawable' 两种类型的资源,会先将 'drawable' 类型的所有资源取出并计算将资源信息依次插入到指定内存中。

   6.1.1 创建三个StringPool字符串资源池用来分别保存资源类型(比如:drawable)、资源key(比如:aospMusic)和资源的值(比如:res/drawable-xxxhdpi-v4/aospMusic.png)

    status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>& filter,
        const sp<AaptFile>& dest,
        const bool isBase)
    {
        const ConfigDescription nullConfig;
        //只有一个主题包 package,所以 N = 1
        const size_t N = mOrderedPackages.size();
        size_t pi;

        //创建用于保存资源项值的字符串资源池
        StringPool valueStrings(useUTF8);
        //用于保存所有完整数据的 Entry
        Vector<sp<Entry> > allEntries;
        for (pi=0; pi<N; pi++) {
            sp<Package> p = mOrderedPackages.itemAt(pi);
            //创建用于保存资源类型的字符串资源池
            StringPool typeStrings(useUTF8);
            //创建用于保存资源名称的字符串资源池
            StringPool keyStrings(useUTF8);
            ssize_t stringsAdded = 0;
            const size_t N = p->getOrderedTypes().size();
            for (size_t ti=0; ti<N; ti++) {
                sp<Type> t = p->getOrderedTypes().itemAt(ti);

                const String16 typeName(t->getName());
                //获取 drawable、xml类型的名称并添加到 typeStrings
                typeStrings.add(typeName, false);
                stringsAdded++;

                String8 configTypeName(typeName);
                if (configTypeName == "drawable" || configTypeName == "layout"
                        || configTypeName == "color" || configTypeName == "anim"
                        || configTypeName == "interpolator" || configTypeName == "animator"
                        || configTypeName == "xml" || configTypeName == "menu"
                        || configTypeName == "mipmap" || configTypeName == "raw") {
                    configTypeName = "1complex";
                } else {
                    configTypeName = "2value";
                }

                const size_t N = t->getOrderedConfigs().size();
                for (size_t ci=0; ci<N; ci++) {
                    sp<ConfigList> c = t->getOrderedConfigs().itemAt(ci);
                    const size_t N = c->getEntries().size();
                    for (size_t ei=0; ei<N; ei++) {
                        ConfigDescription config = c->getEntries().keyAt(ei);
                        sp<Entry> e = c->getEntries().valueAt(ei);
                        //收集 资源项名称值 在字符串资源池中的索引位置
                        e->setNameIndex(keyStrings.add(e->getName(), true));
                        ConfigDescription* valueConfig = NULL;
                        if (N != 1 || config == nullConfig) {
                            // 将资源对应的 配置信息地址 赋值给 valueConfig
                            valueConfig = &config;
                        }
                        // 将每个资源的值添加到 valueStrings 中保存
                        status_t err = e->prepareFlatten(&valueStrings, this,
                            &configTypeName, &config);
                        allEntries.add(e);
                    }
                }
            }
            //最终把这些字符串资源池的数据合成字符串块 放入对应的package中
            p->setTypeStrings(typeStrings.createStringBlock());
            p->setKeyStrings(keyStrings.createStringBlock());
        }

  //创建package 数据块
  ...
}

   代码流程比较清晰,先创建一个保存所有资源项值的字符串资源池valueStringsvalueStrings保存并不是按照package来分类,而是将所有资源统一保存。接着每当遍历一个package时就会创建一个typeStringskeyStrings,专门来保存每个package中所有的 '资源类型字符串' 和 'key 字符串',并将字符串保存在对应的package中。

  • StringPool 字符串资源池几个重要属性:
    Vector<entry> mEntries; :为最终保存的字符串的集合;
    Vector<size_t> mEntryArray; :保存着字符串在mEntries中位置的集合;
    DefaultKeyedVector<String16, ssize_t> mValues;:字符串值与字符串值在 mEntryArray 中的位置的键值对;

  • entry 几个重要属性:
    String16 value :字符串值
    String8 configTypeName; 配置名称
    Vector<ResTable_config> configs; 具体配置对象

从上面的几个属性可以判断出资源字符串的添加流程和最终的查找流程,看下字符串资源池的添加流程:

ssize_t StringPool::add(const String16& value,
        bool mergeDuplicates, const String8* configTypeName, const ResTable_config* config)
{
    // mValues保存着一个字符串的值'value'以及其在 mEntryArray 中的位置值
    ssize_t vidx = mValues.indexOfKey(value);
    ssize_t pos = vidx >= 0 ? mValues.valueAt(vidx) : -1;
    // mEntryArray中保存着某一个字符串在mEntries中的位置
    ssize_t eidx = pos >= 0 ? mEntryArray.itemAt(pos) : -1;
    if (eidx < 0) {
        // 将 'aospMusic'封装到 entry 对象中并添加到 mEntries 集合
        eidx = mEntries.add(entry(value));
    }

    if (configTypeName != NULL) {
        entry& ent = mEntries.editItemAt(eidx);
        if (ent.configTypeName.size() <= 0) {
            ent.configTypeName = *configTypeName;
        } else if (ent.configTypeName != *configTypeName) {
            ent.configTypeName = " ";
        }
    }

    if (config != NULL) {
        // Add this to the set of configs associated with the string.
        entry& ent = mEntries.editItemAt(eidx);
        size_t addPos;
        for (addPos=0; addPos<ent.configs.size(); addPos++) {
            int cmp = ent.configs.itemAt(addPos).compareLogical(*config);
            if (cmp >= 0) {
                if (cmp > 0) {
                    ent.configs.insertAt(*config, addPos);
                }
                break;
            }
        }
        if (addPos >= ent.configs.size()) {
            ent.configs.add(*config);
        }
    }

    const bool first = vidx < 0;
    
    const bool styled = (pos >= 0 && (size_t)pos < mEntryStyleArray.size()) ?
        mEntryStyleArray[pos].spans.size() : 0;
    if (first || styled || !mergeDuplicates) {
        // 将 value 对应在 mEntries 集合中的位置 添加到 mEntryArray 中
        pos = mEntryArray.add(eidx);
        if (first) {
            // 将 value 与 eidx 在 mEntryArray 中的位置以键值对的形式保存在 mValues 集合中
            vidx = mValues.add(value, pos);
        }
        // 
        entry& ent = mEntries.editItemAt(eidx);
        //最后将 entry 对应在 mEntryArray的位置pos保存到 entry 的 indices集合中
        ent.indices.add(pos);
    }
    // 返回这个位置
    return pos;
}

   资源类型和资源名称 字符串资源的添加比较简洁,在结构体entry保存了这个字符串的基本名称,并没有其他的各种配置属性。而资源项值字符串资源池中需要添加对应的配置信息字串和对应的ResTable_config*对象。flatten 函数中具体的添加逻辑就不仔细记录了,代码比较直观。由于packageResourceTable中的数量只有一个,最后flatten 函数执行完,三个字符串资源池的保存状态大概如下:

资源类型 StringPool

attribute value
mEntries(entry集合) size :2 ,values:xml 、drawable

资源名称 StringPool

attribute value
mEntries(entry集合) size :37 ,entry:value:appfiller 、aospMusic、...

资源项值 StringPool

attribute value
mEntries(entry集合) size :37 ,entry:value: res/xml/appfiller .xml 、res/drawable-xxxhdpi-v4/aospMusic.png 、... entry:configTypeName:null、xxxhdpi-v4、...

   createStringBlock函数中会计算出每个字符串资源池中的属性需要分配的内存缓冲区大小,再通过AaptFile:editData方法为其分配空间,将这些字串通过二进制的格式写入到指定空间中。最终将这些代表着字符串信息的AaptFile保存到对应的package:mTypeStringsDatapackage:mKeyStringsData中,将AaptFile.mDataAaptFile.mDataSize设置给package:mTypeStringspackage:mKeyStrings

此时的*Package状态如下:

attribute value
mName(正在编译的包名) com.wsdeveloper.galaxys7
mPackageId 0x62
mTypes(Type 集合) 以资源类型名为key*Type为value的集合
mTypeStringsData(AaptFile类型) 资源类型 AaptFile
mKeyStringsData(AaptFile类型) 资源名称 AaptFile
mTypeStrings(ResStringPool 类型) 资源类型字符串池数据
mKeyStrings(ResStringPool 类型) 资源名称字符串池数据
... ...

   6.1.2 创建 package 数据块,并将所有资源数据填充进去

   首先为这些package初始化一个集合,用来保存这些装载 package 数据块的 AaptFile 对象强指针的集合,从上面的ResourceTable表格可以知道这里的ResourceTable.mOrderedPackages的size为1,所以这里的flatPackages最终只保存一个。
   开始为主题包的package 数据块创建一个AaptFile对象,AaptFile会为每个资源块来分配内存缓冲区空间,每个资源块都会往每段内存中填充对应的资源数据,组成最终的package 数据块。通过分析下面的代码来确定一个package数据块包含着那些数据。

status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>& filter,
        const sp<AaptFile>& dest,
        const bool isBase)
    {
        // 开始编译package块数组
        Vector<sp<AaptFile>> flatPackages;
        for (pi=0; pi<N; pi++) {
            //取出主题包 package
            sp<Package> p = mOrderedPackages.itemAt(pi);
            //取出刚才组织好的资源类型字符串池数据
            const size_t N = p->getTypeStrings().size();
            //初始化 ResTable_package 空间大小
            const size_t baseSize = sizeof(ResTable_package);

            //创建一个代表着  ResTable_package 的 AaptFile
            sp<AaptFile> data = new AaptFile(String8(), AaptGroupEntry(), String8());
            //为 ResTable_package 分配缓冲区空间
            ResTable_package* header = (ResTable_package*)data->editData(baseSize);
            // 初始化这个头部的空间
            memset(header, 0, sizeof(*header));
            header->header.type = htods(RES_TABLE_PACKAGE_TYPE);
            header->header.headerSize = htods(sizeof(*header));
            header->id = htodl(static_cast<uint32_t>(p->getAssignedId()));
            strcpy16_htod(header->name, p->getName().string());

            // 写入资源类型字符串池
            const size_t typeStringsStart = data->getSize();
            sp<AaptFile> strFile = p->getTypeStringsData();
            // 将package中对应的资源类型资源池中的数据 copy 到当前 AaptFile 的内存中
            ssize_t amt = data->writeData(strFile->getData(), strFile->getSize());
            // 写入资源名称字符串池
            const size_t keyStringsStart = data->getSize();
            strFile = p->getKeyStringsData();
            // 将package中对应的资源名称资源池中的数据 copy 到当前 AaptFile 的内存中
            amt = data->writeData(strFile->getData(), strFile->getSize());

            strAmt += amt;

            // 在package 内部创建一个 类型规范数据块
            for (size_t ti=0; ti<N; ti++) {
                size_t len;
                // 获取类型名称
                String16 typeName(p->getTypeStrings().stringAt(ti, &len));
                //通过 类型名称 从 pacakge 类型集合中获取对应的 Type 对象
                sp<Type> t = p->getTypes().valueFor(typeName);
                // N = 37
                const size_t N = t != NULL ? t->getOrderedConfigs().size() : 0;
                //先写入 typeSpec 块(类型规范数据块)头部,typeSpec 块头部中包含着 Type 对象中每个资源的entry 信息
                {
                // 先为 typeSpec 块计算需要的内存大小
                const size_t typeSpecSize = sizeof(ResTable_typeSpec) + sizeof(uint32_t)*N;
                const size_t typeSpecStart = data->getSize();
                    // 由 data 为 typeSpec 块分配内存缓冲区空间
                    ResTable_typeSpec* tsHeader = (ResTable_typeSpec*)
                    (((uint8_t*)data->editData(typeSpecStart+typeSpecSize)) + typeSpecStart);
                    //初始化 typeSpec 块内存空间
                    memset(tsHeader, 0, sizeof(*tsHeader));
                    tsHeader->header.type = htods(RES_TABLE_TYPE_SPEC_TYPE);
                    tsHeader->header.headerSize = htods(sizeof(*tsHeader));
                    tsHeader->header.size = htodl(typeSpecSize);
                    tsHeader->id = ti+1;
                    tsHeader->entryCount = htodl(N);
                    //初始化一个为 typeSpecFlags 大小空间的内存
                    uint32_t* typeSpecFlags = (uint32_t*)
                    (((uint8_t*)data->editData())
                    + typeSpecStart + sizeof(ResTable_typeSpec));
                    memset(typeSpecFlags, 0, sizeof(uint32_t)*N);

                    for (size_t ei=0; ei<N; ei++) {
                        sp<ConfigList> cl = t->getOrderedConfigs().itemAt(ei);

                        const size_t CN = cl->getEntries().size();
                        for (size_t ci=0; ci<CN; ci++) {
                            for (size_t cj=ci+1; cj<CN; cj++) {
                                // 用来描述配置差异性
                                typeSpecFlags[ei] |= htodl(
                                        cl->getEntries().keyAt(ci).diff(cl->getEntries().keyAt(cj)));
                            }
                        }
                    }
                }

                SortedVector<ConfigDescription> uniqueConfigs;
                if (t != NULL) {
                    // 将每个type 中每个 entry 对应的 ConfigDescription 按照顺序保存在 uniqueConfigs 集合中
                    uniqueConfigs = t->getUniqueConfigs();
                }
                //计算 所有资源合起来的 类型规范数据块 总大小
                const size_t typeSize = sizeof(ResTable_type) + sizeof(uint32_t)*N;

                const size_t NC = uniqueConfigs.size();
                for (size_t ci=0; ci<NC; ci++) {
                    const ConfigDescription& config = uniqueConfigs[ci];
                    //资源类型数据块紧跟着上面的 类型规范数据块结尾
                    const size_t typeStart = data->getSize();
                    //先为 类型资源项数据块 分配一个内存缓冲区
                    ResTable_type* tHeader = (ResTable_type*)
                    (((uint8_t*)data->editData(typeStart+typeSize)) + typeStart);
                    // 为类型资源项数据块初始化内存空间
                    memset(tHeader, 0, sizeof(*tHeader));
                    tHeader->header.type = htods(RES_TABLE_TYPE_TYPE);
                    tHeader->header.headerSize = htods(sizeof(*tHeader));
                    tHeader->id = ti+1;
                    tHeader->entryCount = htodl(N);
                    tHeader->entriesStart = htodl(typeSize);
                    tHeader->config = config;
                    tHeader->config.swapHtoD();

                    // 在这个类型资源项数据块 内部创建一个 entries 数据块
                    for (size_t ei=0; ei<N; ei++) {
                        sp<ConfigList> cl = t->getOrderedConfigs().itemAt(ei);
                        sp<Entry> e = NULL;
                        if (cl != NULL) {
                            e = cl->getEntries().valueFor(config);
                        }

                        // 为这个类型中的每个 entry 设置一个偏移位置
                        uint32_t* index = (uint32_t*)
                        (((uint8_t*)data->editData())
                        + typeStart + sizeof(ResTable_type));
                        if (e != NULL) {
                            index[ei] = htodl(data->getSize()-typeStart-typeSize);

                            // 将每个资源的具体信息(item )写入到 Res_value 中,并将Res_value 的内存空间copy 到 data 的结尾处
                            ssize_t amt = e->flatten(bundle, data, cl->getPublic());

                            validResources.editItemAt(ei) = true;
                        }
                    }

                    // 为 类型资源项数据块 填充下自己占用的内存大小信息
                    tHeader = (ResTable_type*)
                    (((uint8_t*)data->editData()) + typeStart);
                    tHeader->header.size = htodl(data->getSize()-typeStart);
                }
                
            }

            // 为 Package资源项元信息数据块头部 填充资源信息.
            header = (ResTable_package*)data->editData();
            header->header.size = htodl(data->getSize());
            header->typeStrings = htodl(typeStringsStart);
            header->lastPublicType = htodl(p->getTypeStrings().size());
            header->keyStrings = htodl(keyStringsStart);
            header->lastPublicKey = htodl(p->getKeyStrings().size());
            
            flatPackages.add(data);
        }

   这个匿名的AaptFile需要依次为 Package资源项元信息数据块头部、资源类型字符串池、资源名称字符串池、类型规范数据块以及类型资源项数据块分配内存空间缓冲区,并依次初始化每个chunk 的内存空间,将各个数据信息填充进去。每个chunk 在内存中的位置是首尾相连。组成了最终的package数据块。介绍一下每个chunk的内容:

  • Package资源项元信息数据块头部(ResTable_package*): 描述整个package数据的基本信息,比如package数据块的大小、字符串资源池的在内存中的位置等。为此分配了缓冲区大小: baseSize
  • 资源类型字符串池(AaptFile): 6.1.1 步中 pacakge 所收集的保存着所有资源类型(ps:drawable)的字符串数据。为此分配了缓冲区大小为 资源类型字符串数据的大小
  • 资源名称字符串池(AaptFile): 6.1.1 步中 pacakge 所收集的保存着所有资源名称(ps:aospMusic)的字符串数据。为此分配了缓冲区大小为 资源名称字符串数据的大小
  • 类型规范数据块: 描述配置差异性,为此分配了缓冲区大小为 ResTable_typeSpec*的大小 + 37个数组,37个数组 对应着37个资源配置的差异性,代码中写的很详细。
  • 类型资源项数据块: 记录每个资源的具体信息(item ),e->flatten函数就是为了将这些资源信息写入到 Res_value 中,并将Res_value 的内存空间copy 到 data的结尾处。为此分配了缓冲区大小为 资源名称字符串数据的大小:ResTable_type*数据 + 37 个Res_value 数据的小大

   综合上面创建package数据块的代码和下面的资源表结构图中的package部分,就更容易理解每个 chunk 的在内存中的分布:

图 8 资源表结构图

至此,package数据块的创建已经完成。

   6.1.2 完成最终的资源索引表

   参考图 8 ,整个资源索引表分为三大部分:资源索引表头部、资源项值字符串资源池和 package数据块。从下面这段代码就是整个所有的部分到资源索引表中,先写入资源索引表头部,接着将 6.1.1 创建的用于保存资源项值的字符串资源池 写入到资源索引表中的仅次于资源索引表头部的位置。最后再将上部分写好的package数据块 写入到资源索引表的最后部分。
    所以分析到这里,终于知道一个资源索引表中的 package数据块是可以有多个的,写完这个笔记最后再发表一下我遇到的问题和想法。

status_t ResourceTable::flatten(Bundle* bundle, const sp<const ResourceFilter>& filter,
        const sp<AaptFile>& dest,
        const bool isBase)
    {
    // dest 是名为'resource.arsc'的AaptFile对象,为最终的资源索引表,这里先计算数据开始点
    const size_t dataStart = dest->getSize();

    {
        // 初始化资源索引表头部
        ResTable_header header;
        // 为 资源索引表头部 初始化内存
        memset(&header, 0, sizeof(header));
        header.header.type = htods(RES_TABLE_TYPE);
        header.header.headerSize = htods(sizeof(header));
        header.packageCount = htodl(flatPackages.size());
        // 将头部信息写入到内存中
        status_t err = dest->writeData(&header, sizeof(header));
    }
    //紧接着 资源索引表头部 开始
    ssize_t strStart = dest->getSize();
    // 将资源项值写入到 dest 内存中
    status_t err = valueStrings.writeStringBlock(dest);
    if (err != NO_ERROR) {
        return err;
    }
    //循环所有的 package资源块,将每个chunk写入到 dest 内存中,虽然这里只有一个package 数据块
    for (pi=0; pi<flatPackages.size(); pi++) {
        err = dest->writeData(flatPackages[pi]->getData(),
                              flatPackages[pi]->getSize());
    }
    //填充 资源索引表头部 整个资源索引表占用内存总大小
    ResTable_header* header = (ResTable_header*)
        (((uint8_t*)dest->getData()) + dataStart);
    header->header.size = htodl(dest->getSize() - dataStart);

    return NO_ERROR;
}
第7步,将代表资源索引表的 AaptFile对象插入到APK编译器容器中
status_t ApkSplit::addEntry(const String8& path, const sp<AaptFile>& file) {
    //将"resources.arsc"与对应的 AaptFile 打包进OutputEntry对象中,最后插入到 mFiles 容器
    if (!mFiles.insert(OutputEntry(path, file)).second) {
        // Duplicate file.
        return ALREADY_EXISTS;
    }
    return NO_ERROR;
}
生成资源索引包

    回到最初的doPackage函数,当所有资源都已经编译完成,接下来就要将这些资源打包成 ' resource.apk' 也就是最终的 ' 资源索引包 '

/*
 * Package up an asset directory and associated application files.
 */
int doPackage(Bundle* bundle)
{
    ...
    ...
    // Write the apk
    if (outputAPKFile || bundle->getOutputResApk()) {
        // 将所有收集到的资源添加到Builder中
        err = addResourcesToBuilder(assets, builder);
    }
    //Write the res apk
    if (bundle->getOutputResApk()) {
        //取出 索引apk的绝对路径 '/data/resource-cache/com.wsdeveloper.galaxys7/icons'
        const char* resPath = bundle->getOutputResApk();
        char *endptr;
        //将路径转换成10进制的长整型数
        int resApk_fd = strtol(resPath, &endptr, 10);

        if (*endptr == '\0') {
            //压缩资源
            err = writeAPK(bundle, resApk_fd, builder->getBaseSplit(), true);
        }
        ALOGW("aapt, write Res apk OK ");
    }
}

    第一步,将所有收集到的资源添加到ApkBuilder中。
    第二步,生成最终apk。
    ApkBuilder::ApkSplit中的容器已经添加了资源索引表AaptFileAaptAsset中所有的资源AaptFile了,通过资源打包流程图中的 step 69 - 72,看下writeAPK函数的打包逻辑:



ssize_t processAssets(Bundle* bundle, ZipFile* zip, const sp<const OutputSet>& outputSet,
                        bool isOverlay)
{
    ssize_t count = 0;
    // 取出保存在 ApkBuilder 的 OutputEntry 容器,按之前计算 OutputEntry 的数量应该是 39 个
    const std::set<OutputEntry>& entries = outputSet->getEntries();
    std::set<OutputEntry>::const_iterator iter = entries.begin();
    for (; iter != entries.end(); iter++) {
        const OutputEntry& entry = *iter;
        if (entry.getFile() == NULL) {
              ...
        } else {
            // ps :res/drawable-xxxhdpi-v4/aospmusic.png
            String8 storagePath(entry.getPath());
            storagePath.convertToResPath();
            bool ret = isOverlay ? processOverlayFile(bundle, zip, storagePath, entry.getFile())
                                    : processFile(bundle, zip, storagePath, entry.getFile());
            if (!ret) {
                return UNKNOWN_ERROR;
            }
            count++;
        }
    }
    return count;
}

bool processOverlayFile(Bundle* bundle, ZipFile* zip,
                            String8 storageName, const sp<const AaptFile>& file)
{
    const bool hasData = file->hasData();

    storageName.convertToResPath();
    ZipEntry* entry;
    status_t result;
    ...
    if (hasData) {
        const char* name = storageName.string();
        if (endsWith(name, ".9.png") || endsWith(name, ".xml") || endsWith(name, ".arsc")) {
            result = zip->add(file->getData(), file->getSize(), storageName.string(),
                               file->getCompressionMethod(), &entry);
            if (result == NO_ERROR) {
                ...
                entry->setMarked(true);
            } else {
                ...
                return false;
            }
        }
    }

    return true;
}

   ZipFile*对象指向的是resApk_fd所代表的索引apk的绝对路径,最终会通过ZipFile* :add方法来压缩保存在AaptFile中的内存数据。 ApkSplit 继承自OutputSetprocessAssets函数中会循环取出保存在OutputSet中所有的OutputEntry对象,而OutputEntry对象中封装着资源的 path 和对应的AaptFile
   回顾之前代码,通过ApkSplit::addEntry方法添加进去的OutputEntry对象总共应该是39个:

  • AndroidManifest.xml
  • res/drawable-xxxhdpi-v4/aospmusic.png、...、res/drawable-xxxhdpi-v4/weather.png
  • res/xml/appfilter.xml
  • resources.arsc

   processOverlayFile函数中会过滤掉类似于‘res/drawable-xxxhdpi-v4/aospmusic.png’所有的OutputEntry对象,最终需要添加到' resources.apk '压缩包的文件有' AndroidManifest.xml ' 、' res/xml/appfilter.xml ' 和分析到头疼的 资源索引表' resources.arsc ',最终索引包中的文件见' 图6 '。

总结:

   从安装主题包到将最终的 主题资源索引apk 输出到指定目录下的流程大体上算是分析完了,还是很多知识点没有掌握,下次在分析 。
   在分析最终生成' resources.arsc '时了解到,是可以同时将多个资源包的资源数据打包进' resources.arsc
'的。之前在做独立资源包时,需要编译出' sdk '中的' android.jar '给上层开发工具使用。但是编译出的' android.jar '中的' resources.arsc '资源索引表中不包含独立资源包中的资源信息,在类似于AS这样的开发工具中使用这些资源无法编译通过,是否可以专门为 sdk 改下 aapt 的打包流程,让独立资源包和原生包同时编译进资源索引表呢?如果有哪位大神尝试过私信我一下,这个问题卡了我很久。

一些大神的博客和学习网站的链接如下(罗老师的文章看了不下五遍才看懂,看懂了才知道写的太好了,哈哈):

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容