关于 R 的一切
date
May 6, 2020
slug
everything-about-R
status
Published
tags
gradle
summary
关于 R 的一切
type
Post
R 之初体验
R 文件可能是很多 Android 开发者既熟悉又陌生的存在。它无处不在,所有使用到资源的地方都离不开它。它又有些陌生,google 已经把它封装的很完美了,以至于很多开发者并不知道它是怎么工作的。那么我们今天就来揭开它神秘的面纱。
这是一个资源的 id,用 32 位的 int 表示。格式为 PPTTNNNN。 前 8 位 PP(Package) 表示资源所属包类型,0x7f 表示应用 Apk 包资源,0x01 表示系统包资源。 中间 8 位 TT(Type) 代表资源 id 的类型
0x02:drawable
0x03:layout
0x04:values
0x05:xml
0x06:raw
0x07:color
0x08:menu
最后 16 位表示资源在此类型里面的编号。
有了 id 之后,就可以去 resource.arsc 里面去查找到真正的资源,将 id 作为 key,就可以查到此 id 对应的资源。
R 文件的存在形式
在平常的开发流程中,我们大概可以将「project」分为三种。
- AAR
- Module (com.android.library)
- Application (com.android.application)
其中 module 和 aar 对于 App 来说是一样的。
为了方便演示,我们构造了以下的工程
- app
- lib1
- lib2
- androidx.recyclerview:recyclerview:1.1.0
app 是 application 工程,依赖 lib1,lib1 依赖 lib2,lib2 依赖了 recyclerview。其中我们在 app、lib1、lib2 分别放置了 string 资源
- app <string name="string_from_app">string_from_app</string>
- lib1 <string name="string_from_lib1">string from lib1</string>
- lib2 <string name="string_from_lib2">string from lib2</string>
Apk
首先我们来看一下最终生成的 apk 里面的 R
我们发现会生成所有「library」和 「appliaction」 的 R 文件,并且放在不同的包名下。我们再来看一下每个包的 AndroidManifest.xml
有没有发现什么呢? 每个模块最后都是按照 AndroidManifest.xml 里面定义的 package 来决定要生成的 R 文件的包名。
AAR
我们拆开 recyclerview-1.0.0.aar
找了一圈,除了一个
R.txt
,并没有在其它地方找到 R 相关的踪迹,classes.jar
里面也没有生成的 R.class
。有同学就可能有疑问了,既然
classes.jar
里面没有 R.class
,那么在开发的时候,我们是怎么引用到 aar 里面的资源的呢?首先我们明确一点,所有的 R 都是在生成 apk 的时候由 aapt 完成。为什么要这样做呢?试想如果 R 文件在 aar 打包阶段就已经生成了的话,那么很大概率会导致 id 之间的冲突。比如 recyclerview.aar 用了 0x7f020001,appcompat.aar 也有可能用了 0x7f020001 这个 id,在合并的时候,
resource.arsc
只能将这个 id 映射到一个资源,这样就会出现错乱。 所以 AGP
做了一件事,所有 R 文件的生成都在 apk 生成的时候交与 aapt 完成。在开发期间对于 R 文件的引用都给一个临时的 classpath: R.java,这里面包含了编译时期所需要的 R 文件,这样编译就不会出错。并且在运行时会扔掉这些临时的 R.java,真正的 R.java 是 aapt 去生成的。所以我们总结一下:
- module/aar 里面临时生成的 R.java 只是为了「make compiler happy」,在编译流程中扮演着「compileOnly」的角色。
- 在生成 apk 的时候,aapt 会根据 app 里面的资源,生成真正的 R.java 到 apk 中,运行的时候代码就会获取到 aapt 生成的 id。
这里有一个问题,我们仔细观察一下 app 和 module 里面对 R.id 引用出的地方。
App 的代码
App 生成的字节码
Module 的代码
Module 生成的字节码。
我们发现,在 App 里面的代码发生了「内联」,但是在 module 里面的代码并没有被内联,而是通过运行时查找变量的方式去获取。 结合上面的「R生成过程」,来想一下为什么 module 里面的 id 不被内联而 app 里面的 id 会被内联呢? 答案已经很清楚了。module/aar 在编译的时候,AGP 会为它们提供一个临时的 R.java 来帮助他们编译通过,我们知道,如果一个常量被标记成 static final,那么 java 编译器会在编译的时候将他们内联到代码里,来减少一次变量的内存寻址。AGP 为 module/aar 提供的 R.java 里面的 R.id 不是 final 的,因为如果设计成了 final,R.id 就会被内联到代码中去,那在运行的时候,module 里面的代码就不会去寻找 aapt 生成的 R.id,而是使用在编译时期 AGP 为它提供的假的 R.id,这个 id 肯定是不能用的,不然就会导致 resource not found。
R 文件的生成
在编译的中间产物中,R 大概有这么几种存在形式
「此 project」代表当前在编译的 module 「本地资源」代表当前在编译的 module 里面声明的资源
- R.java(java 文件,给此 projet 做 compileOnly 用)
- R.txt(记录了此 project 的所有资源列表,并生成了 id,最后会根据这个值生成对应的 R.java)
- R-def.txt(记录了此 project 的本地资源,不包括依赖)
- package-aware-r.txt(记录了此 project 的所有资源,没有生成 id)
大概的生成逻辑是
这是一个 module 生成 R.java 的过程。 首先,当前 module 会搜集所有它的依赖,并且拿到它的 R.txt。比如 lib1 依赖 lib2,lib2 依赖 recyclerview-1.0.0.aar,那么 lib1 会拿到这俩个 R.txt。其中
- lib2 的 R.txt 是经历了上图的过程已经生成好的了
- AAR 的 R.txt 是 AGP 在 transform 的时候从 aar 里面解压出来的
这样这个 module 就拿到了所有的依赖的资源。然后 AGP 会独处当前 module 的「本地资源」,结合刚刚拿到的所有依赖的 R.txt,生成 package-aware-r.txt. 它的格式是这样的
com.example.lib1
layout activity_in_lib2
string string_from_lib1
string string_from_lib2
第一行表示了它的 package name,是从 AndroidManifest.xml 里面取的,下面的几行表示这个 module 中所有的资源,包括自己的和依赖的别人的。 然后 AGP 就会根据 package-aware-r.txt 生成 R.txt,那 R.txt 里面的内容和 package-aware-r.txt 有什么不同呢?
int layout activity_in_lib2 0x7f0e0001
int string string_from_lib1 0x7f140001
int string string_from_lib2 0x7f140002
我们可以看到 R.txt 已经很接近我们的 R.java 的内容了。在从 package-aware-r.txt 拿到所有的资源后,AGP 为资源分配了「临时的 id」。
具体的分配逻辑如上,可以看到它维护了一个 "map",每个资源的 id 都是从 0x7fxx0000 开始递增的。当然这里的分配逻辑没什么用,完全可以乱分配,反正最后也用不着。
最后一步就是通过 R.txt 生成 R.java 啦,AGP 会根据 R.txt 里面的资源及其 id 生成最后的 R.java,作为 classpath 供编译时使用。 这样一通操作下来,「此 project」也生成了 R.txt,当它的上层依赖在编译的时候,就可以拿到它的 R.txt 作为依赖,生成自己的 R.txt,重复上面的步骤。
另: 其实在 AGP 最新版本已经没有 R.java 了,取而代之的是 R.jar,R.jar 会把所有生成的 R.java 打成一个 jar 包,作为 classpath 来编译。可是这样做有什么好处呢?
看看 Jake 大神的说法:
如果你的工程是纯 kotlin 工程,那 AGP 就不用启动一个 javac 去编译 R.java,这样会大幅提升编译的速度。(可见 Google 也在很努力的优化 AGP 了,高版本的 AGP 往往能带来很多性能上的优化。
AAPT 生成 R
终于来到了激动人心的时刻了,前面 AGP 生成了这么多 R.java 最后都要被丢掉,统统交给 aapt 去生成我们运行时需要的 R.java 了。 AGP 的高版本已经默认开启 aapt2 了,这里我们就直接看 aapt2 相关的代码。 首先 aapt2 其实是 Android SDK 里面的一个命令,用 c++ 编写。你可以运行一下 aapt2,看它的 readme。你也可以在 aapt2中找到它的说明。
$ANDROID_HOME/build-tools/29.0.2/aapt2
AGP
AGP 是通过 AaptV2CommandBuilder 来生成 aapt 的具体命令。 在 aapt2 中,为了实现增量编译,aapt2 将原来的编译拆成了 compile 和 link。aapt2 先将资源文件编译成 .flat 的中间产物,然后通过 link 将 .flat 中间产物编译成最终的产物。 AGP 对于 Module 模块调用的 link 命令如下:
传入了 android.jar、AndroidManifest.xml、merge & compile 后的资源产物等等。 android.jar 是提供给代码去链接 Android 自身的资源,比如你使用了 @android:color/red 这个资源,就会从 android.jar 中去取。
--non-final-ids
表示不要为 module 生成的 R.java 使用 final 字段,这个我们上面讨论过了。 对应的,application 生成的 aapt link 命令是这样的为 Application 生成的命令中就没有
--non-final-ids
。还传入了一个 --java
的参数,表示生成的 R.java 的存放路径,这里就是我们的 R 的最终存放路径了。aapt 生成 R
调用 aapt2 命令之后就要开始执行 link 了,这里的代码比较多,就不一一啰嗦了。 我们抽一个 id 生成的逻辑来讲。
通过注释我们可以大概了解到:正常的话,id 是从 0 开始生成,每用一个会往后 +1。比如 string 是从 0x7f030000 开始,下一个 string 就是 0x7f030001。
如果你看过 aapt2 的命令,还会发现 aapt2 有个有意思的功能:「固定 id」
--emit-ids 会输出一个 resource name -> id 的映射表。 --stable-ids 可以输入一个 resource name -> 映射表,来决定这次生成的映射关系。
当有 --stable-ids 的输入时,aapt link 会解析这个文件,将映射表提前存入 stable_id_map 中。
在构造 IdAssigner 的时候,将这个 map 传进去,IdAssigner 在遇到在 map 中存在的 resource 时,就会直接分配 map 表里面存的 id。其它的 resource 在分配的时候将会 "Fill the Gap",找到空缺的 id 分配给它。
- -stable-ids 在「热修复」、「插件化」中有很大的用处,我们知道,如果新增了一个资源,按照原来的分配逻辑,是会在原来的 id 里面插入一个新的 id 的。比如原来是
int string string_from_lib1 0x7f140001
int string string_from_lib2 0x7f140002
int string string_from_lib3 0x7f140003
这个时候,如果不固定 id,在 lib1 和 lib2 中间插入一个 lib4,它将会变成如下的样子
int string string_from_lib1 0x7f140001
int string string_from_lib4 0x7f140002
int string string_from_lib2 0x7f140003
int string string_from_lib3 0x7f140004
这就导致原来的 lib2 和 lib3 都发生了变动。 但是如果我们固定了 id,那生成的 id 可能就是以下这样
int string string_from_lib1 0x7f140001
int string string_from_lib4 0x7f140004
int string string_from_lib2 0x7f140002
int string string_from_lib3 0x7f140003
R 文件相关的优化
其实在细品了 R 文件生成的流程之后,我们发现其实在很多方向上 R 文件有优化的空间。
比如,我们可以在编译完成之后将 module 里面对于 R 的引用换成「内联」的,这样就可以少了一次内存寻址,也可以删掉被内联后的 R.class,减少了包体积又做了性能优化。
比如,我们可以在编译的时候通过固定 id 来减少增删改资源带来的大量 id 变动,导致 R.java 被“连根拔起”,带来下游依赖它的 java/kotlin 文件重新编译。
这里就不再一一展开叙述。
总结
题目起的有点大,其实关于 R 这还只是冰山一角。但是光光看完这一部分代码后,就有点醍醐灌顶的感觉。 R 的设计很精妙,开发者完全不用关心怎么去粘合代码和资源,代码里只用 R.xxxx 一把梭,就可以很轻易地访问到想要的资源。当然这背后需要 IDE、agp、aapt 的配合。 如有错误或者不懂的地方,欢迎交流。