我们在平时的代码开发中,常常会写出以下平常的代码:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 此处我们使用R中的id来获取MainActivity的layout资源setContentView(R.layout.activity_main);}}
我们在该例中使用R.layout.activity_main来获取了 MainActivity 的 layout 资源,那我们将其转化为字节码会是如何呢?这需要分两种情况讨论:
- 当 MainActivity 在 application module 下时,其字节码为:
protected void onCreate(android.os.Bundle);Code:0: aload_01: aload_12: invokespecial #2// Method android/support/v7/app/AppCompatActivity.onCreate:(Landroid/os/Bundle;)V5: aload_06: ldc#4// int 21312962858: invokevirtual #5// Method setContentView:(I)V11: return
可以看到使用R.layout.activity_main直接被替换成了常量 。- 然而,当 MainActivity 在 library module 下时,其字节码为:
protected void onCreate(android.os.Bundle);Code:0: aload_01: aload_12: invokespecial #2// Method android/support/v7/app/AppCompatActivity.onCreate:(Landroid/os/Bundle;)V5: aload_06: getstatic#3// Field com/bytedance/android/R$layout.activity_main:I9: invokevirtual #4// Method setContentView:(I)V12: return
可以看到其从使用 ldc 指令导入常量,变成了使用 getstatic 指令访问 R$layout 的 activity_main 域 。为什么会出现差别我们知道,library module 在提供给 application module 的时候一般是通过 aar 的形式提供的,因此为了在 library module 打包时,javac 能够编译通过,AGP 默认会给 library module 提供一个临时的 R.java 文件(最终不会打入 library module 的包中),并且为了防止被 javac 内联,会将 R 中 field 的修饰符限定为public static,这样就使得 R 的域都不为常量,最终逃过 javac 内联保留到了 application module 的编译中 。
为什么 library module 不内联在 Android 中,我们每个资源 id 都是唯一的,因此我们在打包的时候需要保证不会出现重复 id 的资源 。如果我们在 library module 就已经指定了资源 id,那我们就和容易和其他 library module 出现资源 id 的冲突 。因此 AGP 提供了一种方案,在 library module 编译时,使用资源 id 的地方仍然采用访问域的方式,并记录使用的资源在 R.txt 中 。在 application module 编译时,收集所有 library module 的 R.txt,加上 application module R 文件输入给 aapt,aapt 在获得全局的输入后,按序给每个资源生成唯一不重复的资源 id,从而避免这种冲突 。但此时,library module 已经编译完成,因此只能生成 R.java 文件,来满足 library module 的运行时资源获取 。
为什么 ProGuard 没有优化我们在使用 ProGuard 的时候,Google 官方建议我们带上一些 keep 规则,这也是新建 application 默认会生成的模版代码
buildTypes {release {proguardFiles getDefaultProguardFile('proguard-android-optimize.txt')}}
官方给的 keep 规则(https://android.googlesource.com/platform/sdk/+/master/files/proguard-android-optimize.txt)中,为了保证运行时正确(如避免程序运行时反射获取 R class 的字段),所以加了下面这条规则:-keepclassmembers class **.R$* {public static <fields>;}
该 keep 规则的作用是,将所有 R 以及 R 内部类的以 public static 修饰的域保留,使其不被优化 。因此在我们最终的 APK 中,R.class 仍然存在,这造成了我们包体积的膨胀 。实际上,造成我们包体积膨胀的原因不止 R 的域的定义和赋值,在 Android 中,一个 DEX 可放置的 field 的数量上限固定是 65536,超过这个限制则我们需要将一个 DEX 拆分为两个 。多个 DEX 会导致 DEX 中的复用数据变少,从而进一步提升了包体积的膨胀 。因此我们对于 R 的优化,在 DEX 层面上也会有很大的收益 。解决方法了解问题根源后,解决方案也十分简单 。既然 R.class 中各个域的值确认后就不再改变,那我们完全可以将通过 R 获取资源 id 的调用处内联,并删除对应的域,来获取收益 。
优化思路大概如下:
1.遍历所有的方法,定位所有的getstatic指令
2.如果该getstatic指令的目标 Class name 的为**.R 或者**.R$*形式的 Class
- a. 如果getstatic指令的目标 Field 为public static int类型,则使用ldc指令将getstatic替换,直接将 Field 的实际值导入;
- b. 如果getstatic指令的目标 Field 为public static int[]类型,则使用newarray指令将getstatic替换,将<clinit>中 Field 的数组赋值导入 。
推荐阅读
- Android WebRTC 对 AudioRecord 的使用
- TikTok/国际版抖音/海外版抖音2万字干货教程,新手必看
- 作为Android开发,这个知识点一定要知道,官方也改了 2 次
- Android logcat日志封装
- 抖音防烧屏脚本 – Tasker 脚本分享,适用于 OLED 屏幕
- Android开发:使用Kotlin+协程+自定义注解+Retrofit的网络框架
- Android开发:当前项目以Module形式引用别的项目的步骤
- Android恶意木马伪装成游戏APP,通过华为AppGallery分发
- 2021年Android开发新技术动向,未来的路该怎么走?
- Uni-app离线打包Android APK详细教程