欢迎访问新鲜事521❤网站,我们将带您了解更多奇闻世界!
微信扫码关注
看新鲜资讯

首页 >星辰大海

Tik Tok安卓包容量优化探索基于ReDex的DEX优化实践

星辰大海 发布日期: 2023-03-12 浏览:

作者:冯瑞;廖彬彬;刘凤凯

前言应用安装包的大小会显著影响应用的下载速度和安装速度。根据谷歌的实证数据,包大小每增加1M,就会造成0.17%的新损失。Tik Tok的一些实验也证明,包容量会显著影响下载激活的转化率。

Android的安装包是APK格式的,Tik Tok的安装包中DEX的体积占40%以上,所以针对DEX的体积优化是一种有效的优化包体积的方法。

DEX本质上是从java/Kotlin代码编译而来的字节码。因此,用通用的方式优化字节码成为了我们的探索方向。

优化结果在过去的一年里,终端基础技术团队和Tik Tok基础技术团队利用ReDex对Tik Tok包体积的优化有了一些明显的收获,这些优化也同步到了其他各大app上。

在Tik Tok、头条等应用中,我们的优化一般会使APK的体积减少4%以上,DEX的体积减少8% ~ 10%。

优化思路在构建android应用的过程中,Java/Kotlin代码会先编译成类字节码。在这个阶段,gradle为Transformer提供了定制字节码的能力,很多插件在这个阶段处理字节码。然后类文件会被dexBuilder/mergeDex等任务处理生成一个Dex文件,最终进入安装包。整个过程如下:

7bdf1fab4fb8c7abf8e1a7c6c4b103b6.jpg因此,有两个优化字节码的机会:

transformer阶段优化类字节码,DEX阶段优化DEX文件。很显然,优化DEX文件是一个比较理想的方式,因为在DEX文件中,除了字节码指令,还有交叉DEX引用、字符串池等结构,对于这些DEX格式的优化无法在transformer阶段进行。

在确定了优化DEX文件的思路后,我们选择了facebook的开源框架ReDex作为优化工具,并对其进行了定制。

选择ReDex的原因是它提供了丰富的基本功能,包括:

读取、写入和解析DEX的能力,同时读取和解析简单Proguard的能力在一定程度上保持xml和so文件的规则并匹配类/方法/成员变量分析字节码的能力,以及通过常见数据流分析算法检查字节码合法性的能力,包括寄存器检查、类型检查等一系列字节码优化项。每一次优化称为一次通过,几次通过形成一个管道来优化DEX: 86d8c88bc1760f2df32d95c2009b541f.jpg我们基于这些能力进行了定制和扩展,期望最终建立一个完善的优化体系。

在Tik Tok落地的优化项目,包括facebook开源优化和我们自研优化,从其出发点大致可以分为以下几类:

通用字节码优化:通常意义上的编译优化,比如常量传播、内联等。也可以在Transformer阶段实现:除了字节码指令,DEX还包括字符串池、类/方法引用、调试信息等。这几个方面的优化归类为编程语言的DEX格式优化:Java/Kotlin的一些语法糖会产生大量的字节码,可以分析优化,提高压缩率。将DEX打包成APK本质上是一个压缩过程,有针对性地优化DEX内容可以提高压缩率,从而生成更小的APK。这些优化没有明确的标准和界限,有时候一遍会涉及很多种。下面是每次优化的详细介绍。

通用字节码优化ConstantPropagationPass,实际上包括了常量折叠和常量传播。

常数折叠是在编译时简化常数的过程,例如

1 y=7-14/22 - 3 y=0常数传播是在编译时替换指令中已知常数的过程,例如

1 int x=142 int y=7-x/2;3返回y *(28/x ^ 2);4-5 int x=14;6 int y=7-14/2;7返回(7-14/2)*(28/14 2);在恒定折叠和恒定传播优化之后,上面的例子将被简化如下。

1 int x=142 int y=0;3返回0;死代码删除后,最后可以变成return 0。

具体的优化过程是:

分析方法的数据流,主要针对const/move等指令,得到某个寄存器在某个位置可能的值。根据分析结果替换或删除指令,包括:如果值肯定不为空,则可以去掉相应的判断,比如kotlin生成的null check调用。如果该值确实为空,可以用空异常替换该指令。如果该值确实阻止了if分支的遍历,则可以删除相应的分支。如果值是固定的,可以用const指令替换相应的赋值或计算指令。一个方法被ConstantPropagationPass优化后,可能会产生一些死代码,比如例子中的int y=0,这也为后续的死代码删除创造了条件。

AnnoKillPass这个Pass用于删除无用的评论。注释主要分为三种类型:

SOURCE:java源代码在编译成类字节码的时候是不可见的。一般这种注释不需要太关注class:字节码在被dx工具转换成DEX的时候是不可见的,所以在代码运行的时候不需要关注。发现DEX中还存在一些标注,可以优化。运行时:DEX仍然可见。在代码操作中,可以通过getAnnotations等接口获取注释信息。但是,随着业务的迭代,可能获取注释信息的代码被删除了,但是注释没有被丢弃,ReDex会安全地删除这些注释。另外,为了支持一些系统特性,编译器会自动生成系统注释。虽然注释本身是运行时类型,但是可见性是VISIBILITY_SYSTEM。

AnnotationDefault:默认注释,不能删除。EnclosingClass:声明当前内部类的类。EnclosingMethod:声明当前内部类的方法。InnerClass:当前内部类的名称。MemberClasses:当前类的所有内部类的列表。方法参数:方法参数签名:泛型相关抛出:异常相关示例。

247980700dbfaf8083d82d4c61a65152.jpg编译器生成1MainApplication$1,这是一个带有EnclosingMethod和InnerClass批注的匿名innerclass。

a756e54bac347894c1a761308c78f1a4.jpg系统提供以下接口来获取类相关信息,这是通过分析相关的系统注释来实现的。

班级。getEnclosingMethodClass。getSimpleNameClass。匿名类.如果代码中没有使用这些接口获取类信息的逻辑,这部分注释就可以安全删除,从而达到减小包大小的目的。

RenameClassesPass通过减少传递名的字符串长度来减小包的大小。

比如把类名从La/b/c/d/e改成;改成LX/a;可以命名类字符串的长度,从而达到减小包大小的目的。事实上,Proguard本身也提供了类似的功能:-重新打包class ' X '效果如下:

fa0acb64dd843511f4f95094aa7a6198.jpg但是-repackageclasses 'X '的处理会影响ReDex的InterDexPass (InterDexPass可以参考以下)的算法逻辑,导致收益减少。

盈利能力测试Proguard -repackageclasses 'X '盈利能力:600K Redex指数间盈利能力:400K申请Proguard -repackageclasses 'X '和Redex指数间盈利能力:40K的本质原因是Proguard。D更名后,影响了InterDex功能的参考权重分配,导致Interdex收入恢复。

Solution InterDexPass深入分析原理,优化权重算法首先由InterDexPass实现,然后由类似于Proguard的-repackageclasses 'X '权重算法实现。优化相对复杂,存在很多不确定性,比如与其他优化的潜在冲突,所以我们采用第二种方案。

这里要解决的一个关键点是如何确定一个类名是否可以安全地重命名。我们采用了更巧妙的方法。ReDex将分析Proguard并上传mapping.txt文件。只要保持和Proguard类重命名优化一样的处理策略,就不会造成反射/原生调用/序列化等一系列问题。

188167863db71d5b5ef5faeac2586b98.jpg但是在实现中还是存在各种奇怪的问题,比如签名系统注释失效。签名批注的内容是非标准的类名格式,因此在类被重命名后简单地写回字符串或更新类型类型会导致签名批注失效。最后,通过深入分析签名格式来避免这个问题。

StringBuilderOutlinerPass这个Pass是对StringBuilder的调用点的分析和缩写的优化,和死代码删除一起使用可以有很好的优化效果。

为什么要优化StringBuilder?在Java代码开发过程中,无论是实际处理字符串拼接,还是不同数据类型之间的拼接,字符串操作几乎是我们最常做的事情。而这些拼接操作会被Java的脱糖优化为StringBuilder操作。比如:var log=' A ' 1 ' B ' 1.0f other _ var将被优化为:

1 StringBuilder builder=new StringBuilder();2 builder . append(' A ');builder . append(1);3 builder . append(' B ');builder . append(1.0f);4 builder . append(other _ var);5 builder . tostring();因此,我们分析StringBuilder的所有调用点。在最好的情况下,多个方法调用可以优化成一个调用。这个方法是一个outline方法,具体的参数拼接和toString隐藏在函数内部:

1 invoke-static {v1,v2,v3}大纲;bind:([Ljava/lang/Object)Ljava/lang/String;优化步骤可以简单地分为以下步骤:

生成一个泛型外联方法的方法和几个具体的参数:我们可以认为生成的方法大概是这样的:1 @ keep2公共静态字符串bind (object.args){ 3 string builder builder=new stringbuilder();4 for(int I=0;I此通道针对交叉索引引用进行了优化。

跨DEX引用是指当一个DEX需要在另一个DEX中“使用”一个类/方法/变量时,需要在这个DEX中保存一个对应的类/方法/变量的id。如果两个索引使用相同的字符串,那么这个字符串需要在两个索引中定义。因此,改变DEX中类/方法/变量和字符串的分布可以减少引用的数量,从而减小DEX的大小。从原理上也可以看出,对于单一DEX的应用,优化是无效的。

d8201c6c95a971d2d8c5c07a4531363a.jpg从上图可以看出,类重排后,DEX0的类引用和方法引用的数量减少了,DEX的体积也减小了。

具体的优化过程是:

收集每个类涉及的所有引用,根据引用的数量和类型计算每个类的权重,根据权重计算每个类的优先级,根据优先级选择一个类放入DEX,然后调整剩余类的优先级,重复此步骤,直到处理完所有类。ReBindRefsPass是对方法引用的优化,原理和InterDexPass一样。

在字节码中,invoke-virtual/interface指令需要一个方法引用。在许多情况下,这个引用指向一个子类或实现类的引用。用父类和接口的方法引用替换这个引用不会影响运行时逻辑,而且会减少DEX中方法引用的数量。生成DEX时,方法引用的65536限制通常是第一个瓶颈,这个优化也可以缓解这种情况。

7773080f5ffb3510b5de00482f447cb3.jpg如上图所示,优化前调用者方法的invoke指令使用了子类引用,其伪指令如下,需要两个引用。

1 new-instancev0,sub12invoke-virtual v0,sub1.a () 3new-instancev1,sub24invoke-virtual v1和sub2.a()进行了优化,invoke指令都指向其父应用,两个引用可以合并为一个,减少了DEX中的引用数量。

1 new-instancev0,sub 12 invoke-virtual v0,base.a () 3 new-instancev1,sub 24 invoke-virtual v1,base.a()编程语言Kotlindata classPass的优化是对Kotlin数据类的优化,基本思想是简化data class的生成代码。

解构声明优化Kotlin有解构声明的语法,可以更方便的创建多个变量。基本用法如下。

1数据类Person (val name: string,val age: Int)2 val (name,age)=person('John '20)kotlinc将为Person类生成get方法和componentN方法,如下面的伪代码所示。

1个人{ 2字符串名称;3 Int年龄;4 5 getName():String { return name;} 6 getAge():Int { return age;} 7 component1(): String {返回名称;} 8 component 2():Int { return age;} 9 }10 //解构宣言编译成11 valname=person。Component121 () 13 valage=person。组件2()。可以看到,get和component的逻辑是一样的,所以在编译时,可以执行全局匹配,用get替换component,然后删除component。

toString等生成方法优化kotlin编译器为数据类生成的toString有类似的代码结构,所以可以生成一个辅助方法,然后在数据类的所有ToString方法中调用这个辅助方法,也就是outreach,这样就减少了指令的数量。

Equals和hashCode也可以进行类似的优化,但是风险比较大,所以针对这些优化单独配置了开关,业务方可以酌情开启。

优化提高压缩比RegAllocPassDEX和其他文件被压缩到APK。如果可以通过改变DEX的含量来提高压缩率,那么最终的包大小也会减小。RegAllocPass通过重新分配寄存器来提高压缩率。

当dx生成DEX时,它使用线性寄存器分配算法。它的基本步骤是分析幸存变量,然后计算每个变量的活跃区间,再根据活跃区间依次给变量分配寄存器。有效间隔之外的寄存器可以重新分配。它的优点是运行速度快,但结果往往不是最优的。

比如下面这段代码,dx分配了6个寄存器,v0 ~ v5。

1 public static double calculate luminance(@ ColorInt int color){ 2 final double[]result=gettempdouble 3 array();3 colorToXYZ(颜色,结果);4返回结果[1]/100;5} 51bd6d23a0d932ca84116b56107db9e5.jpg相比之下,ReDex使用图着色算法来分配寄存器。基本步骤是分析生存变量,建立冲突图。冲突图的每个节点都是一个变量。如果两个变量可以同时存活,则在两个节点之间建立一条边。最后,冲突图被着色,每种颜色代表一个寄存器。着色完成后,寄存器分配也就完成了。着色法相对较慢,效果一般较好。对于上面同样的代码,着色方法使用了四个寄存器,v0 ~ v3。

7d75875ea34e206f77ceddbaaa21faf9.jpgDEX中的方法使用更少的寄存器,内容重复率越高,压缩率越高,从而减小了分组大小。

Tik Tok的Tik Tok登陆是字节跳动最大、最复杂的应用之一。ReDex落地初期,由于对复杂度的低估,在独立灰阶和全灰阶期间造成了一些问题。在解决问题的过程中,我们逐渐形成了一个迭代过程,以保证优化的稳定性。下面是我们遇到的一些典型问题和目前的迭代过程。

遇到的问题兼容性问题一般来说,只要优化了字节码规范,就不会出现兼容性问题,因为dalvik/art也是按照规范来检查和运行字节码的,即使优化错误,造成的问题也应该是常见问题。但是很多事情都有例外。ReDex在某品牌手机的部分Android 5.x机型上遇到过问题。

根据日志和一些钩子,某品牌手机在5.x art上做了大量的魔改,可以推断其魔改存在一些问题,可能导致正确字节码的检查和操作出现问题。一个可能的原因是,ReDex优化的时候,一些方法体的指令顺序会重新排列。这种重排不会影响方法的逻辑,但可能会改变一些指令。魔术改变后的艺术在检查这些方法时可能会报告验证错误,导致崩溃。

最后,黑名单配置跳过了这些方法的优化,回避了问题。在随后的优化过程中,没有再遇到类似的问题。

复杂场景的优化Tik Tok业务复杂,代码编写多样,给静态分析和优化增加了一定难度,也更容易遇到问题。这里有两个典型的问题:

空方法优化问题的代码中可能存在一些空方法,在排除反射、natvie调用等场景后,应该删除剩余的空方法。但是做优化的时候遇到了crash。如下面的代码1 object xxxsdk helper { 2 init { 3 init xxxsdk()4 } 5 fun fake init(){ 6 } 7 } 8 9//初始化任务10 public class xxinittask实现runnable { 11 @ override 12 public void run(){ 13 xxxsdk helper . instance . fake init();14 }15}在初始化代码中调用fakeInit,这是一个空方法。调用它的目的是触发XXSDKHelper类的加载,以执行init语句块。如果这个空方法被删除,初始化将不会被执行,指针将在后续进程中被清空。

复杂的反射问题可以通过简单反射用法的静态分析来分析.),但是字符串拼接或者嵌套后的一些反射就很难分析了。因此,需要非常小心地优化可能被反映的代码。一般来说,匿名内部类不会被反射调用。基于这个前提,我们优化了匿名内部类的重命名。但是灰化后我们发现有些第三方SDK会通过复杂的运行时逻辑对匿名内部类进行反射调用,最终导致ClassNotFoundError。

复杂场景下的优化问题,有些是业务代码不规范导致的,但更多的是优化前提失败导致的(空方法可以删除/匿名内部类不会体现),所以优化时需要先仔细验证假设。

迭代过程为了减少稳定性问题,我们总结了ReDex Pass的迭代过程。

有了通行证的初步想法后,会在小组内进行可行性讨论。如果理论上可行,则进入开发验证阶段,之后至少进行两轮独立灰阶验证和业务通过审核,最后进行全灰阶验证。任何一个环节发现问题,整个过程都会重复。

4ca7527a319b791a107872e9141dae2c.jpg通过这个过程,我们大大降低了稳定性问题被遗留到灰色阶段的可能性。在不断改进迭代过程的同时,我们也在探索通过加强单元测试和自动化测试来提高质量的方法。

后续规划ReDex还在持续迭代中,未来我们会在以下几个方向继续探索:

更多的包容量优化的探索和迭代,同时探索字节码优化在性能提升、提高字节码质量和更严格的合法性验证方面的可能性;ReDex之前已经检测到了一些自定义插件和proguard的问题,并在编译期对问题进行了封堵,未来将继续提升这一能力,建立更加完善的质量验证体系;ReDex是编译时的全局字节码优化方案。如果保证优化后的字节码质量一直是痛点,我们会继续在单元测试、自动化测试等方向探索提高质量的方法,增加编译时监控,更加快速便捷地解决字节码在编译时的问题,改善访问体验。如插桩法、特定条件下的静力代码分析等。加入我们吧。字节跳动客户端基础设施是大前端基础技术的全球化R&D团队(在北京、上海、杭州、深圳、广州、新加坡和美国山景城有R&D团队),负责字节跳动大前端基础设施的建设,提升公司整个产品线的性能、稳定性和工程效率;支持的产品包括但不限于Tik Tok、今日头条、西瓜视频、飞书、瓜瓜龙等。在移动终端、Web、桌面等终端都有深入的研究。

就现在!面向全球招聘的客户端/前端/服务器/终端智能算法/测试开发!让我们用科技改变世界。如果你有兴趣,请联系fengrui.0@bytedance.com。请发送电子邮件至您的主题简历-姓名-工作意向-预期城市-电话号码。

Tik Tok的Android基础技术团队是一个追求深度极致的团队。我们关注的是性能、架构、包大小、稳定性、基本库、编译和构造等。保证超大型团队的R&D效率和上亿用户的体验。目前北京、上海、杭州、深圳都有大量的人才。欢迎有志之士与我们一起打造亿级用户APP!

可以去xiaolin.gan@bytedance.com招聘官网咨询“安卓,Tik Tok的基础技术”相关的职位,也可以通过邮件联系,直接投简历或者咨询相关信息!

抖音Android包体积优化探索基于ReDex的DEX优化落地实践

OPPO新人喜欢系统拍Tik Tok,这些会优化吗?这种优化向第三方应用开放应该是迟早的事。ColorOS 11更新的时候说OPPO的超级防抖、超广角、视频HDR能力开放给第三方应用,Hyper Boost、Link Boost等系统级能力也开放给第三方,可以缩短视频生成和上传的时间,大大提升这些第三方应用在Tik Tok的体验。

9507d21df37d525feba3aa4a76ee8244.jpg

与此同时,OPPO还向第三方应用开放了ROI解码和画质增强功能,不仅让镜头稳定,也让画面更好看。从这些方面来看,OPPO非常重视短视频软件的使用体验,所以向第三方开放全新的人像视频系统,这本身可能就是OPPO计划的一部分。

1639a4e47ed183dfdd823b15929c1922.jpg

而且从OPPO发布的OPPO FDF全维人像视频技术系统优化来看,主要是针对时下喜欢拍短视频的人群和部分主播。这个技术体系可以大大提高视频中人物的颜值。相比传统美颜简单粗暴的优化,“OPPO FDF全维人像视频技术系统”的优化会更加自然真实,这也一直是美颜技术的发展方向。毕竟如果第一眼看上去是假的,会大大降低观众看视频的欲望。

b409aae96f00f0ad457259a4bc22528f.jpg ef0470b035ed5ed5b0c8586731531b94.jpg 52a9f3e81fc346d92241fa83fc3c4b26.jpg

联想到OPPO Reno系列一贯的发布时间,OPPO Reno5系列可能会在12月发布。按照视频手机的一贯定位,OPPO Reno5系列很有可能会推出“OPPO FDF全维人像视频技术系统”,届时大家可以关注一下。