Android中的资源也是一块比较重要的知识,平时工作中除了简单的使用
context.getResouce().getColor(R.id.xxx)
之外,我们也更想了解背后的原理。接下来的系列文章从资源编译、资源访问和具体实践几个部分来一窥Android中资源框架的基本原理。
后续更新计划,欢迎持续关注😝:
- Android资源初探(二)运行时资源的访问
- Android资源初探(三)换肤框架原理解析
- Android资源初探(四)资源的插件化和热修复
apk文件
先来看一个简单的apk
文件,解压后包含的文件如图所示:
可以看到一个apk
文件解压后,主要分为3部分:
- 资源:包含res目录,assets目录,以及
AndroidManifest.xml
、resources.arsc
都可以算作资源文件; - 代码:包括
classes.dex
、lib/xxx.so
; - 签名信息:
META-INFO
文件;
可以认为应用 = 可执行代码 + 资源
,今天我们主要研究资源部分,后续有时间再来再探讨dex
相关的话题。
apk中的资源类型
根据上面的分析,一个打包后的apk文件中属于资源的主要包含以下几部分:
- res/layout/…、res/drawable…
- assets/…
- resources.arsc
- AndroidManifest.xml
资源打包前后变化
我们可以写一个简单的hello world
工程,然后打包后将apk文件进行解压,对比下上述这些资源文件在打包前后有何变化,如图所示:
assets/..
打包前后无变化res/layout/xxx.xml
打包后xml文件变成了二进制res/values/…
打包后不存在了res/drawable-xxx/xxx
打包后drawable-xx
后缀有变化,具体图片文件似乎没变化,AndroidManifest.xml
打包后,变成了二进制文件- 无中生有了
resources.arsc
文件
可以看出apk打包过程中,类似布局文件之类的xml文件会被进行编译,而values下的文件会消失不见,同时也生成resources.arsc。这是我们的直观感受,接下来分析具体流程。
apk资源编译
上图是apk打包流程图,从流程图也可以看出资源的编译是通过aapt
工具完成的,输入原始工程,输出为编译后的资源,同时生成R.java。先来看aapt
。[AAPT]全名Android Asset Package Tool, 是官方SDK自带的资源打包工具,是一个可执行文件,具体在sdk/build-tools/&sdkversion/
, 可以看到aapt是一个可执行程序,我们也可以通过将其加到环境变量中,然后直接在命令行中通过调用aapt命令来执行一些操作,具体命令参数可参考[https://developer.android.com/studio/command-line/aapt2];
总之就是我们完全可以通过手动输入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:)。[https://github.com/msandroid/androidsourceCode/blob/master/share/aapt/Main.cpp]
(img)
我们还是从简,主要是梳理下编译流程:
- 解析AndroidManifest.xml 得到包名,构造对应的ResourceTable对象,可以重点看下这个ResourceTable对象,可以这样理解,打包流程基本上就是构造ResourceTable过程,并利用这个数据结构来完成R.java和resources.arsc的生成。
- 引入外部资源,我们工程中会引入一些系统资源,比如R.color.white,他们都不在本工程中,而是在/system/framework/framework-res.apk中,所以在编译过程中需要将其引进来
- 收集需要编译的资源文件,构造aaptFile对象
- 给每个资源分配id
- 编译aaptfile资源
- 编译values资源
- 生成R.java
- 生成Resources.arsc文件
- 生成最终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对应源码细节较多,感兴趣的同学,可以参考[https://github.com/msandroid/androidsourceCode/tree/master/share/aapt]。
转载:https://blog.csdn.net/qwm8777411/article/details/100921576