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


char... whose default value is the null code point ('u0000')
The floating-point types are:
float... whose default value is positive zero
double... whose default value is positive zero
2.4. Reference Types and Values
...The null reference initially has no run-time type, but may be cast to any type. The default value of a referencetype is null.
总结来说,在 Java 中的基本类型和引用类型的 Field 都会在 Class 被加载的同时赋予一个默认值,byte、short、int、long、float、double类型都会被赋为 0,char 类型会被赋为'u0000',引用类型会被赋为 null 。
我们将开头那段代码通过命令行java -p -v转化为字节码:
public com.bytedance.android.dexoptimizer.MyClass();Code:0: aload_01: invokespecial #1// Method java/lang/Object."<init>":()V4: aload_05: iconst_06: putfield#2// Field aBoolean:Z9: returnstatic {};Code:0: iconst_01: putstatic#6// Field aBooleanStatic:Z4: returnprivate void boo();Code:0: aload_01: getfield#2// Field aBoolean:Z4: ifne157: getstatic#4// Field java/lang/System.out:Ljava/io/PrintStream;10: ldc#5// String in aBoolean false!12: invokevirtual #6// Method java/io/PrintStream.println:(Ljava/lang/String;)V15: aload_016: getfield#3// Field aBooleanStatic:Z19: ifne3022: getstatic#4// Field java/lang/System.out:Ljava/io/PrintStream;25: ldc#7// String in aBooleanStatic false!27: invokevirtual #6// Method java/io/PrintStream.println:(Ljava/lang/String;)V30: return通过上述字节码发现,虽然 JVM 会在运行时将 aBoolean 赋值为 0,但是我们在字节码中仍然会再赋值一次 0 给到 aBoolean,aBooleanStatic 同理 。
public com.bytedance.android.dexoptimizer.MyClass();Code:0: aload_01: invokespecial #1// Method java/lang/Object."<init>":()V4: aload_05: iconst_06: putfield#2// Field aBoolean:Z9: return以上标红部分出现了重复赋值,去除了不影响运行时逻辑 。因此,我们考虑在 Class 字节码处理阶段,将这种冗余的字节码移除来获取包大小收益 。
优化思路理解了问题产生的原因后,就很容易得到对应的解决方案 。首先,能够被优化的 Field 赋值,需要满足这三个条件:

  1. Field 是属于其直接定义的 Class 的,而非在父类定义过的;
  2. Field 赋值是在 Class 的clinit、init方法中,这样做很大程度是为了降低复杂度(因为只在这两个方法中调用的 private 方法也是能做这样的优化,但分析这样的方法复杂度很高);
  3. Field 赋值是默认值,当出现多个赋值时,在非默认赋值后的赋值都无法被优化 。
我们结合下面的代码,具体说明一下各种情况是否可以被优化:
Class MyClass {// 可以优化,直接定义的,且是默认值private boolean aBoolean = false;// 不可优化,因为赋值为非默认值private boolean bBoolean = true;// 可以优化,直接定义的,且是默认值private static boolean aBooleanStatic = false;static {// 可以优化,第一处出现,且是默认值aBooleanStatic = false;// 其他代码...// 可以优化,前面没有非默认值赋值,且是默认值aBooleanStatic = false;// 其他代码...// 不可优化,因为赋值为非默认值aBooleanStatic = true;// 其他代码...// 不可优化,因为之前出现了非默认值的赋值aBooleanStatic = false;}private void boo() {// 不可优化,因为函数为非clinit或initaBoolean = false;}}具体实现上,我们的优化思路是这样的:
  • 遍历 Class 所有方法,找到<clinit>和<init>方法,从上往下进行字节码指令遍历
  • 遍历这两种方法的所有字节码指令,找到所有的 putfield 指令,将 putfield 指令的目标 ClassName 和 FieldName 使用-连接,构建一个唯一的 Key,如果
    • putfield 目标 Class 不是当前 Class,跳过
    • putfield 前的 load 指令不为iconst_0,fconst_0,dconst_0,lconst_0,aconst_null,并将该 putfield 所关联的唯一的 Key 放入已经遍历过的 Key 的集合中
    • putfield 前的 load 指令为iconst_0,fconst_0,dconst_0,lconst_0,aconst_null,且该 putfield 所关联的唯一的 Key 没有在遍历过的 Key 的集合出现过,则标记为可清除的字节码指令
  • 遍历完成后,删除所有被标记为可清除的字节码指令
我们用一个简单的例子来说明下我们的思路:
public com.bytedance.android.dexoptimizer.MyClass();// 1. 判断是<init>方法,进入优化逻辑Code: // 2. 从上往下进行代码遍历0: aload_01: invokespecial #Method java/lang/Object."<init>":()V4: aload_05: iconst_06: putfield#Field MyClass.aBoolean:Z. // 3.发现是该Class的域,且赋值为iconst_0,标记往上三个指令可以删除7: aload_08: iconst_19: putfield#Field MyClass.aBoolean:Z// 4.发现是该Class的域,且赋值不为iconst_0,则在遍历过的Key的集合中添加MyClass-aBoolean,继续往下10: aload_011: iconst_012: putfield#Field MyClass.aBoolean:Z// 5.发现是该Class的域,但在遍历过的Key的集合中发现存在MyClass-aBoolean,继续往下15: return


推荐阅读