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

内联后:
public static void callMethod(int);Code:0: iload_01: iconst_52: iadd3: dup4: istore_05: istore_06: getstatic#5// Field java/lang/System.out:Ljava/io/PrintStream;9: iload_010: invokevirtual #6// Method java/io/PrintStream.println:(I)V13: return从执行时间的角度看,减少了一次函数调用,从而提升了执行性能 。从空间占用角度看,减少了一处函数声明,从而减少了代码体积 。
那是不是所有的方法都适合内联呢?
显然不是的,对于单次调用的方法说内联能同时取得时间和空间的收益;对于多次调用的的方法则需要考虑方法本身的长短,比如上面的 print 方法展开之后的指令是比 invokestatic 指令本身要长很多的,但是像 access、getter-setter 方法本身比较短就很适合内联 。
access 方法内联public class Foo {private int mValue;private void doStuff(int value) {System.out.println("Value is " + value);}private class Inner {void stuff() {Foo.this.doStuff(Foo.this.mValue);}}}如上述代码,大家都知道 Java 可以在内部类 Foo$Inner 中直接访问外部类 Foo 的私有成员,但是 JVM 并没有什么内部类外部类的概念,认为一个类直接访问另一个类的私有成员是非法的 。编译器为了能实现这种语法糖,会在编译期生成以下静态方法:
static int Foo.access$100(Foo foo) {return foo.mValue;} static void Foo.access$200(Foo foo, int value) {foo.doStuff(value);}内部类对象创建时候会传入外部类的引用,这样当内部类需要访问外部类的mValue 或调用doStuff()方法时,会通过调用这些静态方法来实现 。这里需要生成静态的方法的原因,是因为被访问的成员是私有的,而私有访问控制更多地是在源码层面去约束,防止破坏程序的设计 。在字节码层面只要不破坏语法逻辑,因此我们完全可以将这些私有成员改成 public 的,直接删除掉编译器生成的桥接静态方法 。
优化思路具体的优化分为分为以下几步:
1.收集字节码中的 access 方法 。
static int access$000(com.bytedance.android.demo.inline.Foo);descriptor: (Lcom/bytedance/android/demo/inline/Foo;)Iflags: ACC_STATIC, ACC_SYNTHETICCode:stack=1, locals=1, args_size=10: aload_01: getfield#2// Field mValue:I4: ireturnstatic void access$100(com.bytedance.android.demo.inline.Foo, int);descriptor: (Lcom/bytedance/android/demo/inline/Foo;I)Vflags: ACC_STATIC, ACC_SYNTHETICCode:stack=2, locals=2, args_size=20: aload_01: iload_12: invokespecial #1// Method doStuff:(I)V5: return如上面的字节码所示,它的特征非常明显,因为是编译生成的方法,它有 synthetic 标记,并且是静态方法,方法名字以"access$"开头,通过这些特征在 ClassVisitor visitMethod 时就很容易匹配到相关方法 。
2.分析并记录 access 方法调用处要替换的目标指令 。
access 桥接的访问只有字段和方法两种,相对应的指令是方法访问指令(invokvirtual, invokspecial 等)和字段访问指令(getfield, putfield 等),只需遍历方法找到相应的指令,同时解析出指令访问的字段或方法信息,然后再将对应的 private 成员改为 public 。比如 access$000 方法会找到如下指令,访问的字段是类 Foo 的 mValue 。
getfield#2// Field mValue:I3.替换 access 方法调用处的 invokestatic 为对应的目标指令,并删除 access 方法的定义 。
遍历查找所有对 access 方法的调用点,如下面的 invokestatic 指令,其调用方法在我们第一步收集的 access 方法中,将它替换为 getfield,然后便可以删除 Foo.access$000 方法本身 。
invokestatic#3// Method com/bytedance/android/demo/inline/Foo.access$000:(Lcom/bytedance/android/demo/inline/Foo;)Igetter-setter 内联封装是面向对象编程(OOP)的基本特性之一,使用 getter 和 setter 方法是在程序设计中常见的封装方法之一 。在日常开发中,我们常常会为一些类写一些 getter-setter 方法,如下代码所示:
public class People {private int age;public int getAge() {return this.age;}public void setAge(int age) {this.age = age;}}这些方法完全就是短方法内联的最佳 case 。
优化思路getter-setter 内联整体实现和 access 方法大同小异,整体也分为收集、分析和删除三步 。
public int getAge();descriptor: ()Iflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: getfield#2// Field age:I4: ireturnpublic void setAge(int);descriptor: (I)Vflags: ACC_PUBLICCode:stack=2, locals=2, args_size=20: aload_01: iload_12: putfield#2// Field age:I5: return

  1. 收集代码中要内联的 getter-setter 方法信息 。参考上面的字节码指令,主要是找出只有参数入栈(LOAD 类指令)、字段访问(GETFIELD, PUTFIELD)、RETURN 指令 的方法 。这里需要注意的是要过滤被 proguard 规则 keep 的方法,这些删除风险很大,因为可能会有插件内调用或者反射调用 。


    推荐阅读