摘要:相比profdata这种「通用」的方式,抖音采用的是「拼凑」的方式:把启动过程中用到的所有方法拼凑起来,逐个攻破,最终使用linkmap文件确定符号的地址,并交给链接器ld按照指定的顺序排列符号。简单说,如果App启动中几乎都是oc代码,抖音这个方法够用了。

2018年11月份,支付宝发布了一篇文章 支付宝 App 构建优化解析: 通过安装包重排布优化 Android 端启动性能》 ,简单来说这篇文章说明、实践且验证了通过「安装包重排」可以加快Android应用的启动速度。

而作为支付宝曾经的一员,很早就知道了这个方案,当时也在想iOS能否有类似的方案,很快找到了一篇“退休”的文档:

Code Size Performance Guidelines

https://developer.apple.com/library/archive/documentation/Performance/Conceptual/CodeFootprint/Articles/ImprovingLocality.html#//apple_ref/doc/uid/20001862-CJBJFIDD

reduce the number of virtual memory pages 是想要的东西,但再仔细看,这篇文章是针对gcc的,原理虽然讲的很透彻,但没办法用在现在的llvm上。

再后来调查后发现了 Profile-Guided Optimization。

苹果有更详细的使用文档,介绍也很明确:

Profile Guided Optimization (PGO) is a means to improve compiler optimization of an app. PGO utilizes a specially instrumented build of the app to generate profile information about the most commonly used code paths and methods. The compiler then uses this profile information to focus optimization efforts on the most frequently used code, taking advantage of the extra information about how the program typically behaves to do a better job of optimization.

地址是:

https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/xcode_profile_guided_optimization/Introduction/Introduction.html#//apple_ref/doc/uid/TP40014459-CH1-SW1

如图:

以及这两个Clang选项:

原理上简单说如下:

1. 编译一个所有方法插桩了的可执行文件,

2. 运行可执行文件(启动一次app),此时插桩的方法会把执行过的方法都记录下来,并记录方法的执行频率。例如上图的profdata文件。

3. 重新build(原则上只是链接时需要profdata),让链接器按照profdata的信息把「启动中用到的、或者频率高的方法」放到一起。

此时这个新的可执行文件就完成了「二进制文件重排」,减少了page交换(加载)的次数,提高了系统加载和运行app二进制文件的IO性能,加快了执行速度,原理下面抖音的文章中有讲,这里不再陈述。

当然上面只是当时想到和简单测试的,由于换工作等等原因,我没有继续投入时间和资源研究,就搁置了。

今天看到抖音的这篇文章,有感而写出此文。文章是: 抖音研发实践: 基于二进制文件重排的解决方案 APP启动速度提升超15%

然而文章作者说没有找到hook c/c++ static initializers的方法,转而采用静态扫描的方法。但还是有方法hook的,hook方法我在博客 (https://everettjf.github.io ) 写过,后来也转到了这个订阅号,文章是: Hook static initializers

如果你看了上文,也看了抖音的文章,你会发现抖音并没有使用上图Xcode自带的生成profdata的方式。或许这个方式抖音的作者也尝试过,或许默认生成的profdata的方式太过于「通用」,而并不「专于启动速度优化」,也或许有其他原因。

相比profdata这种「通用」的方式,抖音采用的是「拼凑」的方式:把启动过程中用到的所有方法拼凑起来,逐个攻破,最终使用linkmap文件确定符号的地址,并交给链接器ld按照指定的顺序排列符号。

我想了想,这种「拼凑」的方法有局限性,但也足够。app启动过程中,有以下几个执行代码的阶段:

  1. +load方法

  2. c/c++ static initializers

  3. main到首页显示

这里面基本可以认为有三类代码:

  1. c/c++代码

  2. oc 代码

  3. swift代码

采用拼凑的方式,主要就是要找到方法的地址。对于+load和c/c++ static initializers 中的代码,因为一个+load(或者c/c++ static initializers )就是一个代码块,整体参与重排即可。而main到首页显示之间的代码,只能尽量的多照顾了。

采用hook objc_msgSend的方法,可以把oc方法照顾到。但如果有c/c++和swift代码,就照顾不到了。但据我了解,国内的几个大厂的app很少有用swift代码的,而c/c++代码基本只会在特定的功能sdk中使用。因此hook objc_msgSend方法,也足够了。简单说,如果App启动中几乎都是oc代码,抖音这个方法够用了。

「通用」和「拼凑」,两个途径,「拼凑」的方式抖音完成了方案落地,「通用」(PGO方案)呢,或许大家也可以试试。

最后抖音提供的15%这个数字,我觉得只要认为是「很多」就足够了,哈哈。因为启动速度是有浮动的,也有高低端设备、系统版本等各种外界因素,是平均值减少了,还是90分位数(启动耗时从快到慢排列,第90%的那个启动速度)减少了,就不确定了。

总之,抖音把这个方案完成了落地,给力,很给力,让我们对抖音团队刮目相看。

最后,再看看Android,Android有了安装包重排,那Android的二进制能否也重排的,搜索后发现,官方文档就有介绍

https://source.android.com/devices/tech/perf/pgo

很有趣 :)

相关文章