卷起来!抖音Android包体积优化探索( 七 )


我们在平时的代码开发中,常常会写出以下平常的代码:
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 的数组赋值导入 。
3.遍历完成后,判断 R.class 中的是否所有域都被删除,如果全部被删除,则将该 R.class 也移除 。


推荐阅读