【腾讯Bugly干货分享】Redex初探与Interdex:Andorid冷启动优化

本文来自于腾讯bugly开发者社区,未经做者赞成,请勿转载,原文地址:http://dev.qq.com/topic/583b9e3ee8992c2c2df6e6acjava

导语

早在去年10月份,facebook就发布了介绍redex的文章,这个听说能够直接对apk作处理,既提升启动性能,又可减小安装包的利器让安卓开发者们都心动不已。直到今年4月,redex终于开源了,咱们也第一时间对redex作了研究(有观众可能要说我骗人,这都11月了怎么还第一时间呢?好把这个总结是拖了好久才写),虽然因为坑多,最终没有接入到项目构建中,但受Interdex启发,在应用冷启动速度优化方面有了新的收获。git

PS:本篇提到的冷启动速度优化,不包括Android 5.0及以上系统github

1、redex的使用与坑

1.安装与使用

使用redex的第一个坑就是环境。很遗憾的是这个工具不支持windows系统(用mac开发的壕请忽略),只好装虚拟机来跑ubuntu。解决了系统,就能够按照github上的官方指引一步步来了,这里须要安装茫茫多的依赖库和解决若干环境问题,幸亏各类典型issue已经有了解决方案,这里再也不赘述。ubuntu

2.优化原理与配置

Redex的优化项众多,而且能够很方便的修改配置文件来选择须要执行的优化,默认的配置文件以下windows

根据官方的介绍文档,redex的优化主要有如下几项:微信

A.内联。
简单说就是去除一些多级调用的中间层级,举个例子:app

func1 -> static func2 -> static func3

优化后就是less

func1 -> static func3

这样能够减小函数调用时间和字节码。除了静态方法调用,对象引用也有相似优化。函数

B.删除无用代码,移除空类。工具

C.对于只有一个实现类的接口或父类,直接用实现类代替。

D.SynthPass
翻译不能,官方例子,内部类B访问外部类A的private static变量,compile后实际上是经过生成额外的acces方法来帮助内部类访问外部类私有成员。这个优化能够去除额外生成的字节码,方法至关于把变量的做用域改为public。

E.字符串缩减,包括提供字节码层面的混淆能力,相似Proguard,以及DEX文件中metadata的优化,能够有效缩减安装包大小。

F.Interdex
须要使用者提供程序启动时加载类序列做为配置文件,按此顺序调整dex中类的顺序,能够有效提高冷启动速度,提高幅度在30%左右。优化的原理facebook推测是优化读取IO和内存(按研究的结果来看其实另有缘由,后面再说)

3.实践中遇到的坑

如此多的优化项累加,想来效果应该很是可观。但残酷的现实是,通过对手Q安装包的处理验证,redex中还存在很多bug和坑,接入使用的性价比不高:

A.IlegalAccessError

这是redex的一个bug,缘由是在内联优化中,移除中间层的方法时没有考虑做用域,好比:

Func1 -> public static func2 -> private static func3

会被优化成:

Func1 -> private static func3

而调用类又不能访问其余类的私有方法,致使抛异常(这个问题有很多issue,近期redex彷佛已经修复了,还未验证)。

B.NoClassDefError

一个比较诡异的问题,运行时报这个错,但反编译Dex文件,这个类是存在的,怀疑是redex的bug,github也有少部分相似的issue,缘由未明。

C.NoSuchMethodError

一个坑。由于手Q里不少业务是以插件机制运行的,部分插件是非独立的,也就是和手Q工程一块儿编译,而且会引用手Q代码,在编译完成后,这些插件也分别打包好存放在手q的apk里。这样会致使的问题是:
redex在作优化时可能会把手Q部分方法移除,若是插件恰好引用了这个方法,就出现NoSuchMethodError了。

D.Interdex

这个优化项会彻底打乱原有的dex分布,甚至dex的数量也会发生改变,用来校验分dex是否注入成功的Foo类,以及补丁patch也被打乱,对启动时分dex注入,补丁等逻辑都有很大影响。

E.签名

redex执行后须要对apk从新签名,而手Q在签名以后还有一些优化逻辑。

这个时候redex可配置优化项的方便之处就体现出来了。遇到问题时,能够把可疑的优化项屏蔽掉,继续验证。可即便如此,屏蔽到最后悲催的发现可用优化项已经很少,优化的效果也不太明显(安装包能够减小100k左右,启动速度方面由于interdex须要较大改动,何尝试)。仅存的几个优化项没通过更细致的测试也可能存在隐患,而就算只使用这少数优化,在编译脚本修改和rdm构建环境搭建上也会有很大的工做量。

2、Interdex,冷启动速度优化

想直接接入redex成本较大,但要咱们直接放弃这些优化空间,心里也是拒绝的。那么咱们可否参考facebook的思路,尝试自行实现一些优化项呢?

在redex中,大部分优化原理都须要解析dex格式,从中还原出引用、继承关系,加以分析,工做量巨大。但Interdex比较例外,这个优化不须要去分析类引用,它只须要调整Dex中类的顺序,把启动时须要加载的类按顺序放到主dex里,这个工做咱们彻底能够在编译过程当中实现,并且这个优化能够提高启动速度,优化效果从facebook公布的数据来看也比较可观,性价比高。

1.如何实现Interdex

根据interdex官方介绍的原理,咱们能够知道要实现这个优化须要解决三个问题:如何获取启动时加载类的序列?如何把须要的类放到主dex中?如何调整主dex中类的顺序?

A.如何获取启动时加载类的序列?

redex中的方案是dump出程序启动时的hprof文件,再从中分析出加载的类,比较麻烦。这里咱们采用的方案是hook住ClassLoader.findClass方法,在系统加载类时日志打印出类名,这样分析日志就能够获得启动时加载的类序列了。

B.如何把须要的类放到主dex中?

redex的作法应该是解析出全部dex中的类,再按配置的加载类序列,从主dex开始从新生成各个dex,因此会打乱原有的dex分布。而在手q中,分dex规则是编译脚本中维护的,所以咱们能够修改分包逻辑,将须要的类放到主dex。

C.如何调整主dex中类的顺序?

开源就是好。Android编译时把.class转换成.dex是依靠dx.bat,这个工具实际执行的是sdk中的dx.jar。咱们能够修改dx的源码,替换这个jar包,就能够执行自定义的dx逻辑了。简单说下具体修改方法:

这里须要对dex的文件格式作必定了解,再也不细说,网上有一篇很好的文章,有兴趣能够了解下
Android逆向之旅---解析编译以后的Dex文件格式

借网上的一张图,dex文件的基本结构以下:

从dex的文件格式咱们能够知道,dex被据划分为多个section,一个类的完整信息也被分散到各个section里。想从dex中解析一个类必需要先从classDef段找到类定义,从中找到类包含的各类信息的偏移地址,再从对应地址去读取数据,因此要调整dex的类排列顺序,理论上只须要对classDef段修改便可。

(从这里看其实类的排列顺序对读取时的内存影响应该不大,由于在dex中类的数据并非连续存储的)

在dx执行时,最终将dex数据写入到文件也是以section为单位逐个写入,而且每一个section写入前都会执行orderItems作排序,修改这个方法便可实现咱们的目的。

2.优化效果

一番折腾后,终于实现将启动时加载的类按顺序放到主dex中了,赶快用专项测试跑下数据,启动过程actLoginA的耗时减小了30%左右,提高效果仍是比较明显的,数值上与facebook的结论也比较接近。

惋惜没能高兴过久,当我把改动上传到rdm,用rdm构建的release包作专项测试时,发现并无什么效果。此时心里是有点懵x的,难道是专项测试时偶现了偏差?仍是测试时用的参照包和我本地包不是一个version?

仍是我眼花看错了,实际没效果?

怎么办,前一天写日报好像已经把优化30%的结果同步出去了,过了一天还能撤回邮件吗?

冷静,这个时候不能着急,总之先冷静下来找找哪里有时光机。

通过反复、仔细的验证,能够确认的事实是,rdm构建的release包无明显优化,本地debug包和rdm构建的debug包,都有明显优化。

3.为啥release不生效?

手q最终发布的包必然是release包,只对debug包生效的优化并无什么做用。而且这个优化的原理咱们也没有弄清楚,facebook的理论主要是优化IO和内存带来的速度提高,但前面也提过,从dex文件的结构来看,这个解释并不能让人信服。因此还要继续分析,若是弄明白了为何release包不生效,也许就能够推测出优化原理。

首先怀疑的是混淆。Release构建中会作混淆,不少类名都会变化,而咱们优化时用的类加载序列是原始类名,因此在release构建时不能正确的调整顺序。嘿嘿,应该是缘由了把,这个好修复,混淆是在dx以前执行的,只要混淆后拿到混淆表,把类加载序列里的类名替换成混淆后的便可。修改后再次测试,结果仍没什么变化。

再找缘由,release构建有作ZipAlign优化而debug没有,是否是这个影响?验证后排除。

继续怀疑,是否是release包类加载顺序变了?这个按说是不太可能,但抱着死马当活马医的心态试了下,果不其然是匹死马,排除。

finally,在和hyim、大龙两位老司机讨论时发现了新的嫌疑人,插桩。当时手q使用的热补丁是classloader方案:反射修改classloader的DexPathList。这个方案为了解决加载补丁类时verify出错的问题,须要对全部的类进行插桩,而插桩逻辑只有在release构建才会执行。在relesse构建中去掉插桩逻辑,再次测试,actLoginA终于有了提高。

4.优化原理

插桩的目的是避免安装时虚拟机作pre-verify,让类打上CLASS_ISPREVERIFIED标识。这会致使Interdex优化失效,而系统作pre-verify是为了提高性能,再结合Interdex的实现,综合来看interdex真正的优化原理就比较明显了:

将启动时加载的类放到主dex,提高了这些类的内聚,让更多的类知足pre-verify的条件,在安装时就作了校验和优化,以减小首次加载的耗时,从而优化冷启动耗时。

(这个结论也再次证实dex中类排列顺序应该不影响性能,由于打不打pre-verify只看类引用关系。去掉启动类排序逻辑后再次验证,确实仍有明显优化效果)

而插桩会致使全部类必然不能打上pre-verify,因此无论怎么调整类分布,都没用。

一个小疑问:手Q刚开始用热补丁时,为啥没有发现明显的actLoginA降低?

缘由:手q有多个分dex,而且以前主要是按包名来作分dex,因此主dex中除了主依赖集外,剩余的不少类可能都已经不知足pre-verify条件了,因此插不插桩区别不大。

3、总结

  1. Interdex优化确实能够明显提高应用冷启动速度,原理也比较简单:把互相引用的类尽可能放在同个dex,增长类的pre-verify。这个思路其实不只仅能够用在启动上,一些其余的关键场景也可能用相似方法提高性能。不过这个优化与修改classloader.DexPathList的热补丁方案有冲突,想要两者兼得须要选择其余补丁方案。

    好比zhekai的新方案详见
    QFix探索之路——手Q热补丁轻量级方案

  2. redex仍是一个很好的工具,有不少优化项能够挖掘,小型app相对来讲应该更容易接入,大型项目会遇到更多的坑,直接接入不易,但也能够从中了解到新的思路。赞开源精神。

  3. 保持怀疑和好奇。再牛x的项目,也不能全部理论都是对的,仍是要多实践。好比Interdex中调整类顺序,在这个优化项自己是没什么用,而整个研究中这部分是最花费时间的。
    (固然长远来看,了解dx执行和自定义dx实现,了解dex文件结构都是挺有用的,这波不亏)


更多精彩内容欢迎关注bugly的微信公众帐号:

腾讯 Bugly是一款专为移动开发者打造的质量监控工具,帮助开发者快速,便捷的定位线上应用崩溃的状况以及解决方案。智能合并功能帮助开发同窗把天天上报的数千条 Crash 根据根因合并分类,每日日报会列出影响用户数最多的崩溃,精准定位功能帮助开发同窗定位到出问题的代码行,实时上报能够在发布后快速的了解应用的质量状况,适配最新的 iOS, Android 官方操做系统,鹅厂的工程师都在使用,快来加入咱们吧!

相关文章
相关标签/搜索