- 一 理论介绍
- 1.1缺页中断
- 1.2 Linkmap
- 1.3 看二进制文件布局
- 二 探索重排方案
- 静态扫描+运行时trace。
- 思维方式,自顶向下的思维方式
- Clang SanitizerCoverage 的方案
- 三 Clang SanitizerCoverage操作步骤
- 1 打开选项
- 2 收集order file
- 3 写入order file文件
- 四 效果验证
- 指标1:缺页中断个数
- 指标2:启动时间
- 如何分析数据
- 自动化平台
- 手动
- 冷启动与杀进程
- 五 风险
- order 文件里符号写错了或不存在会不会有问题
- 会不会影响上架
- 参考
一 理论介绍
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
,即可收集所有启动时调用的方法。
[https://github.com/yulingtianxia/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(可执行文件) 中函数表与符号表的顺序。
参考
[https://www.jianshu.com/p/52e0dee35830] iOS调优 | 深入理解Link Map File
[http://yulingtianxia.com/blog/2019/09/01/App-Order-Files/] App 二进制文件重排已经被玩坏了
[https://clang.llvm.org/docs/SanitizerCoverage.html#tracing-pcs] Clang SanitizerCoverage¶