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

  • invokevirtual 指令正常方法调用参数依次从右至左依次出栈,然后 this 对象出栈,最后方法返回值 String 入栈 。我们弹出栈顶一个参数,发现其和 String 匹配,然后依次入栈 this 对应的类型 StringBuilder,这里调用的是 toString 方法没有参数就不用再入栈 。
  • 中间其他的指令类似,直到 ldc 指令,本身是向栈中放入一个 int,float 或 String 常量,我们这里弹出一个参数,发现其是 String 匹配,并且此时栈的大小变为 0,也就找到了起始指令的位置 。
  • 方案缺陷不过上述方案存在两个缺陷:
    1.因为分析只在单个方法内分析,针对 Log 方法封装的情况,必须需要配置封装方法作为目标方法,才能删除完全删除,比如下面的方法需要配置 AccountLog.d 才能删除其调用处的 StringBuilder 创建 。
    object AccountLog {@JvmStaticfun d(tag: String, msg: String) = Log.d(tag, msg)}2.可能会误删除一些有用的指令,因为无法认为 Log.i 的两个参数的构建指令都是没有用的,我们只能确定 StringBuilder 的创建是没用的,但是一些其他的方法调用可能会改变一些对象的状态,因此存在一定风险 。
    Proguard 方案在我们上述方案在线上运行一年之后,尝试针对上述弊端进行优化,然后发现 proguard 还提供了 assumenoexternalsideeffects 指令,它可以让我们指定没有任何外部副作用的方法 。
    指定了以后,它只会修改调用这个方法的实例本身,但不会修改其他的对象 。通过如下的配置可以删除无用的 StringBuilder 创建 。
    -assumenoexternalsideeffects class java.lang.StringBuilder {public java.lang.StringBuilder();public java.lang.StringBuilder(int);public java.lang.StringBuilder(java.lang.String);public java.lang.StringBuilder append(java.lang.Object);public java.lang.StringBuilder append(java.lang.String);public java.lang.StringBuilder append(java.lang.StringBuffer);public java.lang.StringBuilder append(char[]);public java.lang.StringBuilder append(char[], int, int);public java.lang.StringBuilder append(boolean);public java.lang.StringBuilder append(char);public java.lang.StringBuilder append(int);public java.lang.StringBuilder append(long);public java.lang.StringBuilder append(float);public java.lang.StringBuilder append(double);public java.lang.String toString();}-assumenoexternalreturnvalues public final class java.lang.StringBuilder {public java.lang.StringBuilder append(java.lang.Object);public java.lang.StringBuilder append(java.lang.String);public java.lang.StringBuilder append(java.lang.StringBuffer);public java.lang.StringBuilder append(char[]);public java.lang.StringBuilder append(char[], int, int);public java.lang.StringBuilder append(boolean);public java.lang.StringBuilder append(char);public java.lang.StringBuilder append(int);public java.lang.StringBuilder append(long);public java.lang.StringBuilder append(float);public java.lang.StringBuilder append(double);}不过,这个配置只适用于 Log 里只传入 String 的情况 。如果是int Log.w (String tag, Throwable tr)这种情况,就无法把Throwable参数也一起去掉 。那还是应该采用我们自己实现的插件才能优化干净 。
    此优化对抖音包体积收益,约为 520KB 。
    短方法内联上面介绍的两个优化是从去除无用的指令的角度出发,开篇 DEX 优化思路中我们有讲过,减少定义方法或者字段数从而减少 DEX 数量也是我们常用优化思路之一,短方法内联就是精简代码指令的情况下,同时减少定义方法数 。
    在和海外竞品的对比过程中,我们发现单个 DEX 文件中的定义方法数远比竞品要多,进一步对 DEX 进行分析,发现抖音的 DEX 中有大量的 access,getter-setter 方法,而竞品中几乎没有 。因此我们打算针对短方法做一些内联优化,减少定义方法数 。
    在介绍优化方案前,先来了解下内联的基础知识,内联作为最常见的代码优化手段,被称为优化之母 。一些语言像 C++、Kotlin 提供了 inline 关键字给程序员做函数的内联,而 Java 语言本身并没有给程序员提供控制或建议 inline 的机会,甚至 javac 编译过程中也没有做方法内联 。为了便于理解,我们通过一个简单的例子来看内联是如何工作的,如下代码中 callMethod 调用 print 函数:
    public class InlineTest {public static void callMethod(int a) {int result = a + 5;print(result);}public static void print(int result) {System.out.println(result);}}在内联之后 inlineMethod 的内容直接被展开到 callMethod 中, 从字节码的角度看变化如下:
    内联前:
    public static void callMethod(int);Code:0: iload_01: iconst_52: iadd3: istore_14: iload_15: invokestatic#2// Method print:(I)V8: return


    推荐阅读