ROM UI 多主题打包流程研究

Posted 王_健

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ROM UI 多主题打包流程研究相关的知识,希望对你有一定的参考价值。

简述:

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

  • Samsung_theme.apk

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

学习目的:

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

整体架构:

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

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

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

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

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

打包流程:

   由于对 c/c++ 代码不熟,百度了一点知识(呵呵哒)。在走到 c++ 流程时记得尽量详细一些,错误的地方还请大拿们指出。先放出整个资源打包的流程图,流程对我而言是相当复杂繁琐,而打包的流程是有两个入口:
1. 当主题包安装完成时,PMS会对主题包进行二次打包处理,为资源创建新的资源索引包;
2. 当开机完成,系统服务准备就绪,PMS会立即检查当前应用中的主题,如有必要会重新通过aapt工具打包。

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

一、主题包安装完成

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 ‘才是这次需要重点研究的对象,可以参考上面的资源架构图来理解。

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

    @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 中完成的。

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

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

/*
 * 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 ‘为例来记录整个收集流程。

   这个函数的主要工作:先收集’ 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(像素密度)sdkVersionmcc(移动国家码 )mnc(移动网络码)
ResTable_config::DENSITY_XXXHIGH4nullnullnull

第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集合)
drawabledrawablesize: 0size: 1

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

attributevalue
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(像素密度)sdkVersionmcc(移动国家码 )mnc(移动网络码)
ResTable_config::DENSITY_XXXHIGH4nullnullnull

*AaptFile

attributevalue
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

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

*AaptDir

attributevalue
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中以资源目录来分类

以上是关于ROM UI 多主题打包流程研究的主要内容,如果未能解决你的问题,请参考以下文章

Android ROM包定制(解包,增删模块,打包)

如何打包Activiti的流程资源文件

element-ui使用babel-plugin-component按需打包组件及官方自定义生成css主题

iOS打包ipa给客户测试流程

彻底学会element-ui按需引入和纯净主题定制

ROM制作工具小白如何进行ROM解包,精简,修改,授权,打包详细图文教程