阅读 1750

iOS基于二进制重排启动优化

一、重排原理

当我们向操作系统申请内存时,操作系统并不是直接分配给我们物理内存,而是只标记当前进程拥有该段内存,当真正使用这段内存时才会分配。这种延迟分配物理内存的方式就通过 page fault 机制来实现的。

1.page fault产生原因

当我们访问一个内存地址时,如果该地址非法,或者我们对其没有访问权限,或者该地址对应的物理内存还未分配, cpu 都会生成一个 page fault ,进而执行操作系统的 page fault handler 。如果是因为还未分配物理内存,操作系统会立即分配物理内存给当前进程,然后重试产生这个 page fault 的内存访问指令。

2.二进制重排原理

App启动时首先的调用的几个方法,会分布在虚拟内存的各个⻚面中, 执行这些方法时,需要从读取到物理内容中,就会产生多次 page fault 。

如果能将启动阶段需要的读取代码集中排布,将这些方法全都放到相邻的区域中,我们读取这些方法可能就只需要极少的 page fault 次数。可以减少不必要的 page fault 时间。达到优化启动时间的效果。

重排前后的函数在页面的布局对比

根据经验优化一个Page Fault,启动速度提升0.6~0.8ms,AppStore分发应用还要做签名认证,所以耗时相对更长

二、实现

1、System Trace调试

  • 首先我们打开项目command + i,打开Instruments调试工具,选择System Trace

  • 选择真机 , 选择工程,点击开始后,当首个页面加载出来点击停止,这里我们搜索Main thread,选择我们的app,然后点击Main thread ,再到下面选择Main Thread --> Virtual Memory(虚拟内存)

  • 这里面File Backed Page In就是page fault的次数
  • 当我们把APP杀死后里面再启动,结果发现File Backed Page In这个值变得很小,说明APP就算杀死后,在启动不是冷启动,还是有一部数据在系统的缓存中
  • 如何才是真正的冷启动呢,我们可以把APP杀掉后启动多个手机里面的APP,然后再启动APP,发现File Backed Page In又变得很大
  • 二进制重排是在链接阶段生成的,重排之后生成可执行文件,所以我们只能在编译阶段来优化,而无法对已生成的ipa进行优化

2.二进制重排

我们可以在XCode配置二进制重排,首先我们要确定符号的顺序,才能知道怎么重排,XCode使用的链接器叫做ld,ld有个参数叫order_file,我们可以将文件的路径告诉XCode,在order_file文件中把符号的顺序写进去,XCode编译的时候就会按照文件中的符号顺序打包成二进制可执行文件。

我们可以在苹果的objc4-750源码中找到这种文件

打开后是下面这种格式:

里面全是函数符号,我们打开项目,在build setting 里面搜索order file,发现这里面指定了order的文件路径,因为一旦在这里指定了order file的路径,XCode就会在编译的时候按照文件里面写进去的顺序

我们现在写一个Demo,AppDelegate添加如下方法

+ (void)test111 {
    NSLog(@"test111");
}

+ (void)test222 {
    NSLog(@"test222");
}

+ (void)test333 {
    NSLog(@"test333");
}
复制代码

然后编译,如何查看整个项目的符号顺序呢,我们到Build Settings搜索Link Map,Link Map就是我们链接的符号表,我们把它改成YES,这样编译的时候就会把链接的符号表给我们写出来

command + R我们运行下,然后在Products里面的.app文件,在我们Intermediates.noindex-->项目名.build--->Debug-iphoneos-->项目名.build--->项目名-LinkMap-normal-x86_64.txt,这个文件里面就有链接的符号顺序表

我们在项目中用touch创建test.order文件,修改方法顺序

然后在Build setting里面搜下order file,然后在后面将该文件地址添加进去

这样Xcode在编译时候就会按照order文件中的符号顺序链接代码了,我们编译一下,再看一下LinkMap-normal-x86_64.txt文件

我们发现是按照order的符号顺序来的,而且如果order里面写了项目中不存在的方法符号,XCode会自动过滤掉,不存在影响

我们二进制重排并非只是修改符号地址 , 而是利用符号顺序 , 重新排列整个代码在文件的偏移地址 , 将启动需要加载的方法地址放到前面内存页中 , 以此达到减少 page fault 的次数从而实现时间上的优化 , 一定要清楚这一点

三、获取APP启动时候调用的所有方法

第一种 通过静态扫描和运行时 Trace 等方法确定 order_file

参考抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%

流程如下:

1.设置条件触发流程

2.工程注入Trace动态库,选择release模式编译出.app/linkmap/中间产物

3.运行一次App到启动结束,Trace动态库会在沙盒生成Trace log

4.以Trace Log,中间产物和linkmap作为输入,运行脚本解析出order_file

缺点

目前不足的是,该方案无法覆盖 initialize、block 和 C++ 通过寄存器的间接函数调用静态扫描不出来调用,因为是用fishHook去hook 系统的 objc_msgSend这个函数,因为oc的方法都是通过发送消息的形式,但是这个函数参数是可变的参数,所以只能通过汇编形式hook,但是这种情况initialize和block以及直接调用函数方式hook不到

第二种 基于llvm插桩的方案

简单来说 SanitizerCoverage 是 Clang 内置的一个代码覆盖工具。它把一系列以 __sanitizer_cov_trace_pc_ 为前缀的函数调用插入到用户定义的函数里,借此实现了全局 AOP 的大杀器。其覆盖之广,包含 Swift/Objective-C/C/C++ 等语言,Method/Function/Block 全支持。

开启 SanitizerCoverage 的方法是:在 build settings 里的 “Other C Flags” 中添加 -fsanitize-coverage=func,trace-pc-guard。如果含有 Swift 代码的话,还需要在 “Other Swift Flags” 中加入 -sanitize-coverage=func-sanitize=undefined。所有链接到 App 中的二进制都需要开启 SanitizerCoverage,这样才能完全覆盖到所有调用。

腾讯大神写了个工具 AppOrderFiles。CocoaPods 接入,程序启动完成函数一行调用,生成 Order File。全在 GitHub 里了:github.com/yulingtianx…

AppOrderFiles(^(NSString *orderFilePath) {
    NSLog(@"OrderFilePath:%@", orderFilePath);
});
复制代码

也可以参考facebook LLVM插桩视频

缺点

通过 llvm 插桩的确定 order_file 的方案,需要使用源码重新打包。如果项目全是已经编译好的二进制模块,使用该方案效果不佳

第三种 手机淘宝团队静态库插桩方案

通过在汇编层面对 pod 编译后的静态库进行插桩。在启动时,插桩后的方法都会调用记录方法,从而获得启动方法的执行顺序。我们编译过的静态库由 .o 文件组成,我们可以对 .o 中的函数代码进行修改,在每个函数的开头插入调用我们指定记录函数的指令。

参考资料 手淘架构组最新实践 | iOS基于静态库插桩的⼆进制重排启动优化

总结

本文主要介绍 page fault产生原因,在启动阶段通过 order file 机制实现二进制重排 ,减少执行 page fault 的次数,加快应用的启动速度。生成order file主流有两种方式,一种是通过静态扫描和运行时 Trace 等方法确定 order file,另外一种是基于llvm插桩的方案,然后工程配置order file,实现二进制重排

文章分类
iOS
文章标签