Android 性能优化之 R 文件优化详解
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 - case
和Annotation
中使用资源索引
在 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 文件内联支持
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。
- K2 编译器是什么?世界第二高峰又是哪座?
- Android 性能优化之 R 文件优化详解
- Kotlin 快速编译背后的黑科技,了解一下~
- 别了 KAPT , 使用 KSP 快速实现 ButterKnife
- Android Apk 编译打包流程,了解一下~
- 如何优雅地扩展 AGP 插件
- ASM 插桩采集方法入参,出参及耗时信息
- Transform 被废弃,ASM 如何适配?
- 【从入门到了解】Android稳定性优化深入解析
- Google I/O : 使用 Baseline Profiles 优化启动性能
- Google I/O : Jetpack Compose 中常见的性能问题
- 如何简单方便地Hook Gradle插件?
- Compose 渲染性能到底怎么样?
- 关于 Android 渲染你应该了解的知识点
- Vue 实现 Chrome 小恐龙游戏
- Vue的双向绑定和单向数据流冲突吗?
- 【Gradle7.0】依赖统一管理的全新方式,了解一下~
- 官方推荐 Flow 取代 LiveData,有必要吗?
- 相比 Groovy 脚本, KTS 性能到底怎么样?
- Gradle 7.0都有哪些新特性?