Android资源初探(一) 资源打包

2,108 阅读5分钟

Android资源初探(一) 资源打包

Android中的资源是一块比较重要的知识,平时工作中除了简单的使用 context.getResouce().getColor(R.id.xxx)之外,我们也更想了解背后的原理。接下来系列文章从资源编译、资源访问和相关实践几个部分来一窥Android资源框架的基本原理。

后续更新计划,欢迎持续关注:

  • Android资源初探(二)运行时资源的访问

  • Android资源初探(三)换肤框架原理解析

  • Android资源初探(四)资源的插件化和热修复

 

apk文件

先来看一个简单的apk文件,解压后包含的文件如图所示: 

可以看到一个apk文件解压后,主要分为3部分:

  1. 资源:包含res目录,assets目录,以及AndroidManifest.xml、resources.arsc都可以算作资源文件;

  2. 代码:包括classes.dex、lib/xxx.so;

  3. 签名信息:META-INFO文件;

可以认为 应用 = 可执行代码 + 资源,今天我们主要研究资源部分,后续有时间再来再探讨dex相关的话题。

apk中的资源类型

根据上面的分析,一个打包后的apk文件中属于资源的主要包含以下几部分:

  • res/layout/..、res/drawable..

  • assets/..

  • resources.arsc

  • AndroidManifest.xml

资源打包前后变化

我们可以写一个简单的 hello world工程,然后打包后将apk文件进行解压,对比下上述这些资源文件在打包前后有何变化:

  1. assets/.. 打包前后无变化

  2. res/layout/xxx.xml 打包后xml文件变成了二进制 res/values/…打包后不存在了 res/drawable-xxx/xxx 打包后drawable-xx后缀有变化,具体图片文件似乎没变化,

  3. AndroidManifest.xml 打包后,变成了二进制文件

  4. 新增了resources.arsc文件

可以看出apk打包过程中,res下的xml文件都会被进行编译,而res/values下的文件会消失不见,同时也生成resources.arsc。这是我们的直观感受,接下来分析具体流程。

apk资源编译

上图是apk打包流程图,从流程图也可以看出资源的编译是通过 aapt工具完成的,输入原始工程,输出为编译后的资源,同时生成R.java。先来看 aapt。aapt 全名Android Asset Package Tool, 是官方SDK自带的资源打包工具,是一个可执行文件,具体在 sdk/build-tools/&sdkversion/。我们也可以通过将其加到环境变量中,然后直接在命令行中通过调用aapt命令来执行一些操作,具体命令参数可参考aapt

总之我们完全可以通过手动输入aapt命令,来完成apk资源的编译,搜索 aapt手动打包相关介绍文章很多,这里不再赘述。

综上,android资源的打包就是调用aapt命令完成的,输入是当前工程,输出就是最终的apk资源文件,接下来我们来分析aapt是如何被调用起来的。

aapt调用逻辑

开发过程中执行一个 assembleDebug task就能生成一个debug apk,由此我们也可以推测最终是在执行gradle task中完成aapt的调用。因为gradle源码比较大,我们可以在工程中添加gradle依赖的方式来分析jar包

因为gradle源码分析不是我们的重点,我们只需要了解它最终如何调起aapt即可,所以分析从简:

//build.gradle

    apply com.android.application

    //对应gradle AppPlugin.apply()方法==>BasePlugin.apply()

    -> BasePlugin.java

    public final void apply(Project project) {

    ...

    -> createAndroidTasks();

    }

    ...

    ->AndroidBuilder.java

    public void processResources(...) {

    ArrayList<String> command = Lists.newArrayList();

    //TODO:= findAAPT: sdk aapt

    String aapt = buildToolInfo.getPath(BuildToolInfo.PathId.AAPT);

    ...

    //TODO:组件打包命令 sdk/build-tools/$version/aapt package xxxxxxx

    command.add(aapt);

    command.add("package");

    if (mVerboseExec) {

    command.add("-v");

    }

    command.add("-f");

    command.add("--no-crunch");

    // inputs

    command.add("-I");

    command.add(target.getPath(IAndroidTarget.ANDROID_JAR));

    command.add("-M");

    command.add(resPackageOutput);

    ...

    //TODO:执行命令

    mCmdLineRunner.runCmdLine(command, null);

    }

    ->CommandLineRunner.java

    public void runCmdLine(String[] command) {

    // launch the command line process

    //TODO: Executes the specified string command in a separate process with the specified environment.

    Process process = Runtime.getRuntime().exec(command);

    }

通过调用链可以看出,gradle task最终生成一个AndroidBuilder,在其中构造出最终的aapt编译命令:

    aapt --package -v xxxx -fxxx

到此,我们已经调用起来了aapt,接下来我们来分析aapt中完成资源编译的过程。

aapt资源编译流程

Aapt源码比较复杂,尤其ResourceTable, AaptFile, AaptAsset等C结构更是复杂,不是一篇文章能说清楚的。我已将分析后的源码上传至github,有兴趣的可以fork一下。以Main.cpp main函数为入口,相关调用链都已加注解(//TODO:)。aapt源码分析注解参考  阅读原文

我们还是从简,主要梳理下编译流程:

  1. 解析AndroidManifest.xml 得到包名,构造对应的ResourceTable对象,可以重点看下这个ResourceTable对象,可以这样理解,打包流程基本上就是构造ResourceTable过程,并利用这个数据结构来完成R.java和resources.arsc的生成。

  2. 引入外部资源,我们工程中会引入一些系统资源,比如R.color.white,他们都不在本工程中,而是在/system/framework/framework-res.apk中,所以在编译过程中需要将其引进来

  3. 收集需要编译的资源文件,构造aaptFile对象

  4. 给每个资源分配id (0xPPTTEEE)

  5. 编译aaptfile资源

  6. 编译values资源

  7. 生成R.java

  8. 生成Resources.arsc文件

  9. 生成最终AndroidManifes.xml

可以看到aapt对xml资源进行了编译和压缩,一方面可以减少存储空间,将大量的重复字符串构造在一个公共的地方(resources.arsc),另一方面,在解析时候也能提高解析效率。反之,如果我们将这些资源原原本本打在apk文件中,其实从运行角度来说是不会有问题的。

R.java & resources.arsc

我们最后再来看看资源编译过程中生成的R.java和resource.arsc,两个文件的的关系如图所示 

简单来说R.java中的id(0xPPTTEEEE)存在于resources.arsc文件中的id数组,然后id数组映射字符串常量池,即可找到id对应的值。

资源打包主要了解其关键流程,由于gradle和aapt对应源码细节较多,感兴趣的同学,可以参考aapt。

推荐阅读:Android Context解析

你离真正的Android高级开发有多远