Android在打包时,通过AAPT工具,对主工程和引入的依赖里的所有资源文件进行编译压缩,并会对res/里的资源文件如drawable、layout、values等生成唯一的id,同时生成R.java文件,保存所有的id值,以及生成resource.arsc文件,建立id对应资源的值(如string)或文件路径(如png)的关系表。
如上图是我们apk中的最终R文件的样子,可以发现里面会按类型分为不同的类,如anim、attr、string等,每个类里有相应类型的所有资源id,id是16进制的int值,变量名就是我们在资源文件中定义的资源名字。
上图是一个id的组成:
- 第一个字节是指资源所属包,7F代表普通应用程序
- 第二个字节是指资源的类型,如02指drawable,03指layout
- 第三四个字节是指资源的编号,往往从00开始递增
除此之外我们还可以看到R文件里的id,都是public static final的静态常量,这会有什么好处呢?
可以看到,由于java编译器的优化,在编译时,所有使用静态常量的地方,会被直接替换为常量值。
这样一来,R文件里的id在编译完java文件后,就没有被引用的地方了,此时如果开启proGuard混淆,就会删除整个R文件,从而会减少field数和包大小。
AAR
以上是R文件的基本原理,但是这里有一个特殊,就是AAR的R文件规则。
AAR是被主工程引入的SDK,一个工程可能会引入多个AAR,这也就导致了一个问题:每个AAR的module如果在自己编译生成AAR时,按照正常的流程生成R文件,那么由于资源id的值都是从00递增,会导致集成到主工程时的冲突(大量id重复)。
所以其实AAR在编译时不会进行真正的R.java文件的生成,而是等到在主工程集成编译时统一进行所有资源的id分配。
这里需要说明的是,如果不同的AAR中有同类型的同名资源,则可能会在运行中有很多莫名其妙的问题,所以我们需要保证资源名的唯一性。
这里AAR其实做了两步工作:
-
为了支持我们的调用语法,生成了一个R.java文件,所以我们才可以在AAR的代码中调用AAR包名下的R.xxx
aar里的Activity调用aar.R.layout.xxx 这里生成的R文件里的id,并不是public static final的常量,而是public static的变量,这是为什么呢?因为如果是常量,则会在编译打包时,调用的地方被替换为常量值,而这个值是AAR内部生成的临时id,是不对的,这样的话主工程编译时将无法修改这个值,就有问题了。
-
因为AAR生成的R.java并不是最终正确的,所以这个R.java文件不会被带入AAR中,但是会生成一个R.txt文本文件,这个文件以文本的形式记录了AAR中所有资源的类型、名字等,以便于主工程打包时,可以依据这些资源信息统一生成最终的R.java文件。
主工程
主工程在编译时,会将主工程下的所有资源,连同所有AAR依赖里的R.txt文件一起,为所有的资源统一分配id,并生成R.java文件和resource.arsc文件,这时就可以保证每个资源都是唯一的id值。
这里需要注意的是:
- 主工程编译时,最终除了会在主工程包名下,生成一个包含主工程和AAR所有资源的R.java文件之外,还会在每个AAR相应的包名下,生成一个包含AAR资源的R.java文件,当然,相同资源的id是一样的。这就是为什么我们可以在主工程中调用主包名的R文件,和AAR包名的R文件,都可以获取到一些资源id的原因。
- 上述最终生成的所有R.java文件里的id值,都是public static final的静态常量,因为此刻的id值都已经确定了。然后在编译java文件时,常量值会被替换(包括资源文件中的引用也会被替换),从而使R文件的field无引用,可以通过proGuard删除。
上述第2点,在主工程编译完成后,我们会发现一个问题,就是AAR里面的文件,使用到资源id的地方,并没有被替换为相应的常量值,但是R文件里面的资源id确实是常量。
这是因为AAR的class文件,在主工程编译时,不会再次进行编译,也就是说AAR的class文件原封不动的打包进apk。而资源id为常量是在主工程编译时才行程的,但AAR生成class时,使用的是上面说到的变量,所以一直被保留了下来。
这个可能是因为Google怕影响编译打包速度,而将AAR的class文件直接带入apk中,但是却忽略了资源id的引用被保留下来的问题。
这个问题可以被gradle插件解决,大致原理就是:gradle插件将所有AAR中引用到R文件资源id的地方,全部都替换为相应的id常量值,然后在proGuard混淆时,所有的R文件就会因为没有被引用到而删除了。