当前位置: 首页 > 科技观察

抖音Android包大小优化探索:基于ReDex的DEX优化实践

时间:2023-03-15 22:24:21 科技观察

作者|冯锐;包体积每增加1M,就会产生0.17%的新损失。抖音的一些实验也证明,包大小会显着影响下载激活的转化率。Android安装包为APK格式。在抖音的安装包中,DEX的体积占比超过40%,因此针对DEX的体积优化是一种有效的包体积优化方法。DEX本质上是一种由Java/Kotlin代码编译而成的字节码。因此,通用的字节码业务不敏感优化成为我们探索的方向之一。优化成果终端基础技术团队和抖音基础技术团队在过去一年中使用ReDex在抖音包大小的优化上取得了一些明显的收益,这些优化也同步到其他各大APP。在抖音、今日头条等应用中,我们的优化一般会将APK体积降低4%以上,将DEX体积降低8%~10%。/Kotlin代码会先被编译成Class字节码。在这个阶段,gradle提供了Transformer来自定义处理字节码。许多插件在这个阶段处理字节码。然后,该Class文件经过dexBuilder/mergeDex等任务处理,生成DEX文件,最后打包到安装包中。整个过程如下:因此,优化字节码的机会有两种:在transformer阶段优化Class字节码和在DEX阶段优化DEX文件显然,优化DEX的一种方式是比较理想的,因为在DEX文件,除了字节码指令外,还有交叉DEX引用、字符串池等结构,无法在transformer阶段对这些DEX格式进行优化。在确定了优化DEX文件的思路后,我们选择了Facebook的开源框架ReDex作为优化工具,进行定制化开发。选择ReDex的原因是它提供了丰富的基础能力。ReDex的基本能力包括:读、写、解析DEX的能力。同时可以在一定程度上读取和解析xml等文件。Parsesimpleproguardkeeprulesandmatchclasses/Method/Abilityofmembervariables分析字节码数据流的能力提供了使用常用数据流分析算法检查字节码有效性的能力,包括一系列字节如注册检查和类型检查。代码优化项,每一个优化称为一个pass,多个pass组成一个流水线来优化DEX。我们基于这些能力进行了定制和扩展,期望最终建立一个完整的优化体系。抖音中实现的优化项,包括Facebook的开源优化和我们自研的优化,从出发点来看大致可以分为以下几类:通用字节码优化:通常意义上的编译优化,如如常量传播、内联等,一般DEX格式优化也可以在Transformer阶段实现:DEX除了字节码指令外,还包括字符串池、类/方法引用、调试信息等。DEX格式。针对编程语言的优化:Java/Kotlin的一些语法糖会产生大量的字节码,可以对其进行分析优化,提高压缩率。优化:将DEX打包成APK本质以上是一个压缩过程。有针对性地优化DEX内容可以提高压缩率,从而使APK更小。这些优化没有明确的标准和界限。有时一张通行证会涉及多种类型。下面就来详细介绍一下吧。各种优化。通用字节码优化ConstantPropagationPass这个Pass其实包括常量折叠和常量传播。常量折叠是在编译时简化常量的过程,如y=7-14/2--->y=0常量传播是在编译时替换指令中已知常量的过程,如intx=14;整数y=7-x/2;返回y*(28/x+2);--->整数x=14;整数y=7-14/2;返回(7-14/2)*(28/14+2);经过constantfolding+constantpropagation优化后,上面的例子会简化为intx=14;整数y=0;返回0;死码删除后返回0。具体的优化过程是:分析方法的数据流,主要针对const/move等指令,获取某个位置某个寄存器的可能值。根据分析结果,进行指令替换或指令删除,包括:如果值为正如果该值不为空,则可以去掉相应的空校验。比如kotlin产生的nullcheck调用的值肯定是空的,指令可以换成抛出异常。如果该值确实阻止了一个if分支到达,则可以删除对应的分支。固定该值,将对应的赋值或计算指令替换为const指令。一个方法经过ConstantPropagationPass优化后,可能会产生一些死码,比如例子中的inty=0,这也为后续删除死码创造了条件。.AnnoKillPassThisPass用于去除无用的注释。Annotations主要分为三种:SOURCE:java源代码编译成类字节码,不可见。一般这样的注解不需要太关注CLASS:字节码通过dx工具转为DEX是不可见的,运行信息时也不需要获取代码,所以一般来说不需要注意它。实际测试发现DEX中仍然存在部分注解,这部分注解可以进行优化。注释信息的迭代可能已经被移除,但是注释并没有被删除。ReDex将安全地删除这部分注释。另外,其实为了支持某些系统特性,编译器会自动生成系统注解。虽然注解本身是RUNTIME类型,但是可见性是VISIBILITY_SYSTEMAnnotationDefault:默认注解,不可删除EnclosingClass:声明当前内部类的类EnclosingMethod:声明当前内部类的方法InnerClass:当前内部类的名称classMemberClasses:当前所有类内部类列表MethodParameters:方法参数Signature:泛型相关Throws:异常相关example编译器生成1MainApplication$1匿名内部类,带有EnclosingMethod和InnerClass注解系统提供如下接口,通过解析相关获取类相关信息systemsClass.getEnclosingMethodClass.getSimpleNameClass.isAnonymousClass通过注解实现。如果代码中没有使用这些接口获取类信息的逻辑,可以安全地去掉这部分注解以减小包的大小。RenameClassesPassThisPass通过减少类名的字符串长度来减少包的大小。例如,将类名从La/b/c/d/e更改;至LX/a;可以通过增加类名字符串的长度来达到包的大小。减量的目的。其实Proguard本身已经提供了类似的功能:-repackageclasses'X',效果如下:但是-repackageclasses'X'的处理会影响到ReDex的InterDexPass的算法逻辑(InterDexPass可以参考下文),导致在减少收入。收益测试Proguard-repackageclasses'X'income:600K+RedexInterDexPassincome:400K+Proguard-repackageclasses'X'andRedexInterDexPassincome:40K+本质原因是Proguard重命名后影响了InterDexPass函数引用的权重分配,导致在InterDex收益回收解决方案InterDexPass深入分析原理,优化权重算法先执行InterDexPass,再执行Proguard-like-repackageclasses'X'权重算法优化比较复杂,存在很多不确定性,比如潜在与其他优化有冲突,所以我们采用了前两种方案。这里需要解决的一个关键点是如何判断一个类名是否可以安全重命名。我们采用了一种更棘手的方法。ReDex会对Proguard传递过来的mapping.txt文件进行解析,只要我们保持与Proguard类重命名优化一致的处理策略,就不会出现反射/原生调用/序列化等一系列问题。但是在执行过程中还是会遇到各种奇怪的问题,比如Signature系统注解失效。Signature注解的内容是非标准的类名格式,所以在类重命名后简单地写回字符串或者更新Type类型都会导致Signature注解失效。最后通过对Signature格式的深入解析避免了这个问题。StringBuilderOutlinerPass该Pass是针对StringBuilder的CallSites的解析和简写进行优化的,配合删除死代码使用可以起到很好的优化效果。为什么要优化StringBuilder?在Java代码开发过程中,字符串操作几乎是我们最常做的事情之一,无论是实际处理字符串拼接还是各种数据类型之间的拼接操作。而这些拼接操作会被Java的脱糖优化成StringBuilder操作。例如:varlog="A"+1+"B"+1.0f+other_var;将优化为:StringBuilderbuilder=newStringBuilder();builder.append("A");builder.append(1);builder.append("B");builder.append(1.0f);builder.append(other_var);builder.toString();所以我们分析StringBuilder的所有Callsites,最好情况下多个方法调用可以优化为一个调用,这个方法是一个outline(大纲)方法,具体的参数拼接和toString隐藏在函数内部:invoke-static{v1,v2,v3}大纲;.bind:([Ljava/lang/Object)Ljava/lang/String;优化步骤可以简单分为以下几步:生成一个通用的外联方法和一个带有几个特定参数的方法:我们可以认为生成的方法大概是这样的@KeeppublicstaticStringbind(Object...args){StringBuilder构建器=newStringBuilder();对于(inti=0;i