AAPT源码分析(一)

前言

通过 AAPT命令行使用实践,我们已经知道了如何去使用 aapt 这个工具。为了更详细地了解 aapt 的工作流程以及原理,我们接下来从源码的角度来分析一下,它是如何编译和打包项目资源文件的。aapt 相关源码的位置如下所示:

AOSP/frameworks/base/tools/aapt/

主函数 main()

在终端执行 appt 命令,首先会进入到 Main.cpp 的 main()函数:

/*
 * 解析参数
 */
int main(int argc, char* const argv[])
{
    Bundle bundle;
    ......
    if (argv[1][0] == 'v')
        bundle.setCommand(kCommandVersion);
    else if (argv[1][0] == 'd')
        bundle.setCommand(kCommandDump);
    else if (argv[1][0] == 'l')
        bundle.setCommand(kCommandList);
    else if (argv[1][0] == 'a')
        bundle.setCommand(kCommandAdd);
    else if (argv[1][0] == 'r')
        bundle.setCommand(kCommandRemove);
    else if (argv[1][0] == 'p')
        bundle.setCommand(kCommandPackage);
    else if (argv[1][0] == 'c')
        bundle.setCommand(kCommandCrunch);
    else if (argv[1][0] == 's')
        bundle.setCommand(kCommandSingleCrunch);
    else if (argv[1][0] == 'm')
        bundle.setCommand(kCommandDaemon);
    else {
        fprintf(stderr, "ERROR: Unknown command '%s'\n", argv[1]);
        wantUsage = true;
        goto bail;
    }
    ......
}    

在 main() 函数里面,通过命令行传过来的参数列表(字符指针数组),首先判断出执行的命令,由代码可知,只要命令参数的第一个字符成功匹配即可,所以 “aapt v” 跟 “aapt vOJBK”是一样的。

Bundle.h 头文件

这里有个比较重要的类Bundle,这个东西会贯穿在 aapt 的源码之中,它负责全局保存命令行参数列表解析的结果,如上,Bundle 通过 setCommand() 函数记录 aapt 的命令类型,kCommandVersion、kCommandPackage 等都是定义在 Bundle.h 头文件里面的枚举类型:

/*
 * 命令枚举类型
 */
typedef enum Command {
    kCommandUnknown = 0,
    //对应 v[ersion]命令
    kCommandVersion,
    kCommandList,
    kCommandDump,
    kCommandAdd,
    kCommandRemove,
    //对应 p[ackage]命令
    kCommandPackage,
    kCommandCrunch,
    //对应 s[ingleCrunch]命令
    kCommandSingleCrunch,
    kCommandDaemon
} Command;

查看 Bundle 类的构造函数,每个变量都对应一个 aapt 的命令行参数:

public:
    Bundle(void)
        : mCmd(kCommandUnknown), mVerbose(false), mAndroidList(false),
          mForce(false), mGrayscaleTolerance(0), mMakePackageDirs(false),
          mUpdate(false), mExtending(false),
          mRequireLocalization(false), mPseudolocalize(NO_PSEUDOLOCALIZATION),
          mWantUTF16(false), mValues(false), mIncludeMetaData(false),
          mCompressionMethod(0), mJunkPath(false), mOutputAPKFile(NULL),
          mManifestPackageNameOverride(NULL), mInstrumentationPackageNameOverride(NULL),
          mAutoAddOverlay(false), mGenDependencies(false), mNoVersionVectors(false),
          mCrunchedOutputDir(NULL), mProguardFile(NULL), mMainDexProguardFile(NULL),
          mAndroidManifestFile(NULL), mPublicOutputFile(NULL),
          mRClassDir(NULL), mResourceIntermediatesDir(NULL), mManifestMinSdkVersion(NULL),
          mMinSdkVersion(NULL), mTargetSdkVersion(NULL), mMaxSdkVersion(NULL),
          mVersionCode(NULL), mVersionName(NULL), mReplaceVersion(false), mCustomPackage(NULL),
          mExtraPackages(NULL), mMaxResVersion(NULL), mDebugMode(false), mNonConstantId(false),
          mSkipSymbolsWithoutDefaultLocalization(false),
          mProduct(NULL), mUseCrunchCache(false), mErrorOnFailedInsert(false),
          mErrorOnMissingConfigEntry(false), mOutputTextSymbols(NULL),
          mSingleCrunchInputFile(NULL), mSingleCrunchOutputFile(NULL),
          mBuildSharedLibrary(false),
          mBuildAppAsSharedLibrary(false),
          mArgc(0), mArgv(NULL)
        {}
    ~Bundle(void) {}

判断完命令类型之后,main.cpp 的 main() 函数继续遍历解析参数列表,因为 aapt 的命令行参数众多,所以只截取部分代码片段如下:

int main(int argc, char* const argv[])
{
    Bundle bundle;
    ......
    argc -= 2;
    argv += 2;

    /*
     * 遍历参数列表
     */
    while (argc && argv[0][0] == '-') {
        /* 参数标记 */
        const char* cp = argv[0] +1;
        while (*cp != '\0') {
            switch (*cp) {
            case 'v':
                // -v 打印详细信息
                bundle.setVerbose(true);
                break;
            ......
            case 'F':
                argc--;
                argv++;
                ......
                // -F 保存资源打包文件的输出路径
                bundle.setOutputAPKFile(argv[0]);
                break;
            ......
            case 'M':
                argc--;
                argv++;
                ......
                // -M 保存 AndroidManifest 文件的路径
                bundle.setAndroidManifestFile(argv[0]);
                break;
            case 'S':
                argc--;
                argv++;
                ......
                // -S 添加资源文件夹
                bundle.addResourceSourceDir(argv[0]);
                break;
            ......
            //解析两条杠 -- 的参数
            case '-':
                if (strcmp(cp, "-debug-mode") == 0) {
                    // --debug-mode 开启 debug 模式
                    bundle.setDebugMode(true);
                }
                ......
                else if (strcmp(cp, "-max-res-version") == 0) {
                    argc--;
                    argv++;
                    ......
                    // --max-res-version 如果指定为19,则类似drawable-v21的资源文件夹会被忽略
                    bundle.setMaxResVersion(argv[0]);                                              
                } 
                ......
                else if (strcmp(cp, "-auto-add-overlay") == 0) {
                    //添加 overlay 独有的资源
                    bundle.setAutoAddOverlay(true);
                }
                ......
                else if (strcmp(cp, "-no-crunch") == 0) {
                    //不对 PNG 进行预处理
                    bundle.setUseCrunchCache(true);
                }
            } 
            cp++;
        }
        argc--;
        argv++;
    }
    ......
    result = handleCommand(&bundle);
    ......
}

需要注意一下 -S 这个参数,-S 可以指定多个资源文件夹,Bundle 用一个 类型为 Vector 的 mResourceSourceDirs 变量来保存,通过 addResourceSourceDir() 函数倒序添加这些指定的资源文件夹:

class Bundle {
public:
    ......
    const android::Vector<const char*>& getResourceSourceDirs() const { 
        return mResourceSourceDirs; 
    }

    void addResourceSourceDir(const char* dir) {
        //倒序添加,假设命令行依次指定了三个 -S 路径参数,分别为 A、B、C  
        //则 A 为 base 资源包,B 是 C 的 overlay 资源包,A 是 B 的 overlay 资源包
        mResourceSourceDirs.insertAt(dir,0); 
    }
   ......
private:
    ......
    android::Vector<const char*> mResourceSourceDirs;
    ...... 
}

handleCommand()

参数列表解析完成后,都全局保存在了 bundle 变量中,接着进入 Main.cpp 的 handleCommand() 函数,分发不同的命令类型:

/*
 * 分发命令.
 */
int handleCommand(Bundle* bundle)
{
    switch (bundle->getCommand()) {
    case kCommandVersion:      return doVersion(bundle);
    case kCommandList:         return doList(bundle);
    case kCommandDump:         return doDump(bundle);
    case kCommandAdd:          return doAdd(bundle);
    case kCommandRemove:       return doRemove(bundle);
    case kCommandPackage:      return doPackage(bundle);
    case kCommandCrunch:       return doCrunch(bundle);
    case kCommandSingleCrunch: return doSingleCrunch(bundle);
    case kCommandDaemon:       return runInDaemonMode(bundle);
    default:
        fprintf(stderr, "%s: requested command not yet supported\n", gProgName);
        return 1;
    }
}

根据不同的命令类型,调用不同的函数流程,比如 p 命令则执行 doPackage() 函数。

小结

时序图整理如下:

这篇主要分析了 aapt 命令行参数是如何进行解析的,涉及到的东西也比较简单。因为我们主要是想弄清楚 aapt 编译和打包项目资源文件的流程,所以,下一篇将从 doPackage() 函数开始,继续分析后续的流程。


  转载请注明: ThisMJ AAPT源码分析(一)

 上一篇
RxJava-理解响应式编程 RxJava-理解响应式编程
前言如何系统地学习一个优秀的开源框架?我认为比较正确的方式应该是,先了解它的设计思想,然后学会如何去使用它,最后通过阅读源码来弄清楚它的实现原理。所以在学习 RxJava 之前,我们需要先对响应式编程有个大概的理解。 编程范式编程范式是如何
2019-03-26
下一篇 
AAPT命令行使用实践 AAPT命令行使用实践
环境 操作系统:Mac OS High Sierra 10.13.2 (17C88) Android SDK Build Tool版本:28.0.3 AAPT是什么AAPT 即 Android Asset Packaging Tool,它
2019-03-06
  目录