KangQingYu
Articles48
Tags16
Categories7
APP启动优化——二进制重排,从入门到精通

APP启动优化——二进制重排,从入门到精通

一 理论介绍

1.1缺页中断

cpu加载数据到内存时,先根据数据对应的虚拟内存的地址,在页表找到其在物理内存中的地址。如果不存在相应的物理内存、该地址非法或没有权限,都会造成缺页中断。每个缺页中断耗时约0.6-0.8ms,虽然很短。但是消耗的时间 = 单次时间 * 次数。那什么时候,次数会非常大呢?冷启动的时候。尤其是对于大型APP,启动时调用的方法非常多。

举个例子,内存的布局如图,page1中有方法1-5,page2中有方法6-10,page3中有方法11-15。假如APP启动时调用方法1、6、11。则需要触发缺页中断3次。如果能将方法1、6、11收敛在一起,都放置在page1,则只会触发一次缺页中断。
(img)

1.2 Linkmap

Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,里面记录了可执行文件的路径、CPU架构、目标文件、符号等信息。
linkmap主要包括三大部分,如下图

  • Object Files 生成二进制用到的link单元的路径和文件编号,如图从第3行开始;
  • Sections 记录Mach-O每个Segment/section的地址范围,如图从第15行开始;
  • Symbols 按顺序记录每个符号的地址范围,如图从第44行开始。

其中Symbols中可以看到方法的地址、大小,其顺序是Build Phases中的Link Binary With Libraries中文件的顺序。我们的目标就是要修改这些方法的顺序。 下面分析一下详细步骤。

1.3 看二进制文件布局

xcode编译的工程,二进制文件内的数据、代码是如何分布的?Xcode提供了Write Link Map File选项。打开如图所示选项。

接着在选择Products -> show in finder,看到项目名+LinkMap-normal-arm64.txt的文件,即为二进制文件的符号表。

从图中可以看到从第46行开始,即为符号地址、大小、方法名。第46行的0x100004218 + 0x00000054,相加结果即为第47行的0x10000426C。

我们的目标,就是重排这些顺序,使启动时调用的方法,收敛在一起,达到减少缺页中断个数的目的。

二 探索重排方案

如何找到启动时方法的调用顺序。其实比较容易想到的方法就是hook所有的方法。但是hook方案也非常多。

静态扫描+运行时trace。

有个团队也公开了自己的方法与效果。包括以下几种:

  • 扫描linkmap的__TEXT,__text,正则匹配拿到load方法,
  • 扫描linkmap的__DATA,__mod_init_func,C++静态初始化方法
  • 通过hook来获取oc方法、block符号。

但是initialize hook拿不到,部分block hook不到,C++通过寄存器的间接函数调用静态扫描不出来。
最终结论是覆盖率达到80%,启动速度提升了15%。

思维方式,自顶向下的思维方式

如果拿一个金字塔作比喻,那这种方法就是自底向下的方式,即最底层找所有的方法,有哪些种类,对其依次进行解决。这种思维方式也可以解决问题。但我可以转换一下思维:自顶向下。即从顶层向下层拆分,其需要满足MECE原则,即各部分之间满足两个原则

  • mutually exclusive,各部分之间相互独立 ,没有重叠、具有排他性。
  • collectively exhaustive,没有遗漏。

有一种测试覆盖率的方法就可以满足这些要求。

Clang SanitizerCoverage 的方案

想一想如果让我们测试代码覆盖率,我们可以怎么办?
Clang提供了__sanitizer_cov_trace_pc_guard能力。其将代码分为函数、基本块、边界三类。 这样就可以覆盖所有的方法。__
本来是用于测试代码覆盖率的,但其实也可以用在二进制重排中。
这种方式叫做静态插桩。将“桩”插入到了所有函数中。

三 Clang SanitizerCoverage操作步骤

1 打开选项

搜索Other C Flags,如图所示,添加-fsanitize-coverage=trace-pc-guard

添加完这个选项之后,即可在编译期,为每一个函数内部插入一行代码__sanitizer_cov_trace_pc_guard,以此来达到AOP的效果。

2 收集order file

接下来需要在APP首屏加载之后,调用方法AppOrderFiles,即可收集所有启动时调用的方法。
源码

因为用真机运行,在沙盒中可以拿到符号表文件,将其改名为app.order。准备写入order file文件。

3 写入order file文件

在链接阶段,可以修改即将生成的可执行文件的代码段进行重排。
Xcode使用的链接器是ld,ld有一个参数是Order File,通过配置路径$(SRCROOT)/Binary/app.order ,并将文件放入工程相应的路径下即可,如图所示。

四 效果验证

这一章节放在最后压轴,足以说明其重要性之高。验证效果应该是做性能优化的第一步,即通过制定一个目标,作为自己需要达到的标准与方向的指针,只有指针的指向正确,才能距离目标越来越近。
我们需要参考的指标有两个:缺页中断个数(毕竟直接优化的就是这个值)和启动时间。

指标1:缺页中断个数

打开 Instruments,选择 System Trace,运行之后。分析数据如图,选择“Main Thread”,底部的File Backed Page In即为缺页中断个数。

指标2:启动时间

虽然我们优化的是缺页中断的个数,但其最终目的还是启动时间。统计时间有几种:
1 打开Xcode的DYLD_PRINT_STATISTICS选项。
2 Instrument AppLaunch功能。

如何分析数据

自动化平台

无疑这是最好的分析方式,只要有大量的用户数据,接着做一下可视化的分析,即可清晰看到效果。
但是有些团队可能没有很完善的平台,那是否可以使用手动的方式呢?

手动

冷启动与杀进程

为了避免缓存所造成的误差,需要杀进程,但杀进程 = 冷启动?显然并非如此,因为如果只是杀进程,因为内存还没有被其他进程使用,所以也没必要清空所有的缓存,苹果做了一些优化。即杀进程 != 冷启动。那如何保证尽可能得接近冷启动的效果呢?
杀进程之后,再多打开几个其他耗内存很高的APP。
并且删除Xcode的缓存~/Library/Developer/Xcode/DerivedData
这样虽然可以尽可能接近冷启动,但是每次完全编译,分析缺页中断的个数、启动时间。再优化前后对比2次。总共要完全重新编译4次。如果是比较大的项目,可能一个小时都不够。
并且,不同机型,也会有较大的差距。
所以,不建议使用手动的方式进行效果对比。

五 风险

order 文件里符号写错了或不存在会不会有问题

ld 会忽略这些符号,如果提供了 link 选项 -order_file_statistics,他们会以 warning 的形式把这些没找到的符号打印在日志里。

会不会影响上架

不会,order文件只是重新排列了所生成的 mach-O(可执行文件) 中函数表与符号表的顺序。

参考

iOS调优 | 深入理解Link Map File

App 二进制文件重排已经被玩坏了

Clang SanitizerCoverage¶

Author:KangQingYu
Link:http://example.com/2021/12/29/20211229_APP%E5%90%AF%E5%8A%A8%E4%BC%98%E5%8C%96%E2%80%94%E2%80%94%E4%BA%8C%E8%BF%9B%E5%88%B6%E9%87%8D%E6%8E%92%EF%BC%8C%E4%BB%8E%E5%85%A5%E9%97%A8%E5%88%B0%E7%B2%BE%E9%80%9A/
版权声明:本文采用 CC BY-NC-SA 3.0 CN 协议进行许可
×