Android 性能优化之 R 文件优化详解

语言: CN / TW / HK

theme: smartblue highlight: a11y-dark


前言

Android在构建过程中会根据资源生成R文件,其中包含了所有res/目录下资源的索引,使用该索引可以在最终生成的resources.arsc资源映射表中找到对应资源。
对于开发者来说在代码中引用资源很方便,你只需要使用类似R.string.***的方式就可以获取到相应的资源,但是使用R文件也会带来一定的冗余问题,包括增大包体积与dex数量等。

本文主要包括以下内容
1. R文件目前存在的问题 2. R文件内联优化方案 3. AGP官方R文件优化方案

R文件目前存在的问题

App模块中的R文件目前是final的(在AGP8.0之后,App模块的资源Id同样将变成非final的),但是为了解决多个library模块R文件的id冲突问题,library工程中引用的R资源索引不是final的,所以我们在Library工程不能在switch - caseAnnotation中使用资源索引

apk 打包的过程中,module 中的 R 文件采用对依赖库的R进行累计叠加的方式生成。如果我们的 app 架构如下:

编译打包时每个模块生成的 R 文件如下:

1. R_lib1 = R_lib1; 2. R_lib2 = R_lib2; 3. R_lib3 = R_lib3; 4. R_biz1 = R_lib1 + R_lib2 + R_lib3 + R_biz1(biz1本身的R) 5. R_biz2 = R_lib2 + R_lib3 + R_biz2(biz2本身的R) 6. R_app = R_lib1 + R_lib2 + R_lib3 + R_biz1 + R_biz2 + R_app(app本身R)

可以看出各个模块的R文件都会包含上层组件的R文件内容,如果项目依赖层次越多,上层的业务组件越多,将会导致 apk 中的 R 文件将急剧的膨胀。这就是R文件冗余的由来,这将会给我们的包体积与dex数量带来很大的影响

R文件内联优化方案

由于App模块目前的R文件中的资源id全部是final的,java编译器在编译时会将final常量进行inline内联操作,也就是将变量替换为常量值,这样项目中就不存在对于app模块R文件的引用了,这样在代码缩减阶段,app模块R文件就会被移除,从而达到包体积优化的目的

基于以上原理,如果我们将library模块中的资源id也转化为常量的话,那么library模块的R文件也可以移除了,这样就可以有效地减少我们的包体积

现在有不少开源的R文件内联方法,比如滴滴开源的booster与字节开源的bytex都包含了R文件内联的插件

它们的基本原理都是在 Transform 之前拿到所有资源名称与索引值的映射关系,然后在 Transform 的过程中将 getfield 指令替换成 ldc 指令,将变量替换为常量值,关于这些插件的具体原理分析可参考:浅谈Android中的R文件作用以及将R资源inline减少包大小

通过以上插件,可轻松实现内联library模块R文件,以实现包体积优化与Dex数量优化,但是可能存在的问题在于这些方案毕竟是第三方的,官方可能随时会对R文件生成的规则进行变更,而到时就需要第三方插件及时跟进适配

比如AGP8.0之后,app模块的资源也要变成非final的了,如下所示

app模块的资源id都不再内联后,很难说R文件内联还是不是一个好的方案

AGP官方R文件优化方案

R 类如此庞大的主要原因是它们包含重复项。为构建的每个模块生成 R 类,并且特定于模块的 R 类包括对其传递依赖项的所有资源的引用。

如上所示,MDC 库包含 com.google.android.material.R 类,其中包含对资源文件的引用。

我们的 :lib 模块依赖于 MDC 库并且包含很少的其他资源。它还有自己的 com.example.myapp.lib.R 类,其中包含了对其自身资源的引用和对 MDC 库资源的传递引用。

最后,:app 模块有自己生成的 com.example.myapp.R 类,并再次包含 :lib 模块和 MDC 库引用 ID

Material Components 库资源 ID 生成了 3 次!这就是项目中R文件快速膨胀的根本原因

因此快速减小R文件的一种实现方式是禁止R文件传递

非传递R

AGP 3.3引入了实验性质的namespacedRClass,并在AGP4.1.0中重命名为nonTransitiveRClass

启用非传递性 R 类 (non-transitive R-class) 后,您应用中的 R 类将只会包含在module中声明的资源,依赖项中的资源会被排除在外。这样一来,module中的 R 类大小将会显著减少

这一改动可以在您向运行时依赖项中添加新资源时,避免重新编译下游模块。在这种场景下,可以给您的应用带来 40% 的性能提升。另外,在清理构建产物时,性能有 5% 到 10% 的改善。

您可以在 gradle.properties 文件中添加下面的标记来启用非传递R类:

android.nonTransitiveRClass=true

Android Studio Bumblebee 开始,新项目的非传递 R 类默认处于开启状态。对于使用早期版本的 Studio 创建的项目,您可以依次前往 Refactor > Migrate to Non-transitive R Classes,将项目更新为使用非传递 R 类。这种方法还可以在必要时帮助您修改相关源代码。目前,AndroidX 库已经启用此特性,因此 AAR 阶段的产物中将不再包含来自传递性依赖项的资源。

启用R文件代码缩减

AGP 4.1.0之前,包含默认对R的keep规则,如下所示:

-keepclassmembers class **.R$* { public static <fields>; }

我们需要查看项目中自定义的keep规则,如果存在以上默认keep规则需要及时删除,不然还是会保留没有用的R文件资源id

AGP4.1.0之后默认不再保留 R 类中的字段。这样一来,启用代码缩减的应用的 APK 大小将会显著减少。这应该不会导致行为变更,除非您通过反射功能访问 R 类;如果您通过反射功能访问 R 类,就必须针对这些 R 类添加保留规则。

总结

本文主要介绍了目前R文件传递带来的一些问题,并介绍了R文件内联与非传递R类两种方案

R文件内联方案library模块中的R文件也将被内联并删除,在包体积大小方面优化的更加彻底,但由于不是官方方案,存在一定适配成本,在AGP版本更新后需要及时适配,同时对编译速度也会有一定影响

在多模块情况下,R文件传递是R文件快速膨胀的重要原因,因此非传递R类在包体积方面也有很大的作用,同时可以在添加资源时,避免重新编译下游模块,从而加快编译速度

我们在项目中启用了非传递R类,包体积减小了2M左右,dex数量减少了1/3,总得来说效果还是很明显的。使用简单,接入方便,效果明显,感兴趣的同学也可以尝试下~

参考资料

加快构建速度:非传递 R 文件
Android agp 对 R 文件内联支持

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿