什么是缺页中端
前面iOS启动时间优化-二进制重排的前世的文章提到当进程去映射表获取数据不存在时,系统会阻塞当前进程,完成数据获取写入物理内存并且和映射表进行映射,这个过程称为缺页中断 ,即[Page fault]。(baike.baidu.com/item/%E7%BC...%25EF%25BC%258C "https://baike.baidu.com/item/缺页中断/5029040?fr=ge_ala)%EF%BC%8C")
当发生缺页中断 ,在iOS系统中,对于生产环境的应用,当产生缺页中断进行重新加载时,iOS系统还会对其做一次签名验证 ,因此iOS生产环境的应用page fault所产生的耗时要更多。
什么是二进制重排
抖音团队分享的一个 Page Fault,开销在 0.6 ~ 0.8ms,我们知道发生缺页中断系统会阻塞进程和签名验证,站在启动时间优化的角度上是可以通过减少缺页中断避免在首屏展示之前消耗过多时间
假设在启动时期我们需要调用两个函数 method1 与 method4 . 函数编译在 mach-o 中的位置是根据 ld ( Xcode 的链接器) 的编译顺序并非调用顺序来的 . 因此很可能这两个函数分布在不同的内存页上 .
那么启动时 , page1与page2 则都需要从无到有加载到物理内存中 , 从而触发两次 page fault .
如果将method1 与method4 放到一个内存页 中 , 那么启动时则只需要加载 page1 即可 , 也就是只触发一次page fault, 达到优化目的 .而这个做法就叫做二进制重排
二进制重排具体如何进行
Xcode用的链接器叫做ld,ld有一个参数叫order File,在启动时也是根据order file定义的文件顺序去加载对应的指令到物理内存.我们需要做的就是把启动相关的函数符号存放在这个order file里面。
配置order file
Xcode提供ld需要的order file的路径配置,如下:
- 在这个 order文件中,将你需要的符号按顺序写在里面
- 当工程build的时候,Xcode会读取这个文件,打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach-O
Tips:
1️⃣ : order文件里符号写错了或者这个符号不存在会不会有问题 ?
- 答:ld 会忽略这些符号 , 实际上如果提供了 link 选项 -order_file_statistics,会以 warning 的形式把这些没找到的符号打印在日志里。 .
2️⃣ : 有部分同学可能会考虑这种方式会不会影响上架 ?
- 答:不影响,二进制重排只是重新排列了所生成的 macho 中函数表与符号表的顺序 .
知道配置ld依赖order file路径,我们只需要把启动相关的函数符号写入order file即可,那么启动相关的函数又怎么获取呢,别慌,继续往下看😄
获取APP启动相关的函数符号
业界方案:
- hook objc_MsgSend(只能拿到 oc 以及 swift @objc dynamic 后的方法,并且由于可变参数个数,需要用汇编来获取参数)
- 静态扫描mach-o特定段和节里面所存储的符号以及函数数据(静态扫描 , 主要用来获取 load 方法,c++ 构造)
- clang插桩(完美版本,完全拿到 swift、 oc 、 c 、 block 全部函数 )
本文主要介绍clang插桩方式在编译时期获取所有启动时需要函数符号
clang插桩原理
插桩实际上是在编译期就在每一个函数内部二进制源数据添加 hook 代码 ( 我们添加的 __sanitizer_cov_trace_pc_guard 函数 ) 来实现全局的方法hook 的效果.
由于本篇注重实践,这里的插桩原理后续会单独拿一篇文章详细介绍原理
demo编译设置1
直接搜索 Other C Flags 来到 Apple Clang - Custom Compiler Flags 中 , 添加
shell
-fsanitize-coverage=trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard #只扫描函数
demo编译配置2(swift或者混编项目) 可选
直接搜索 Other Swift Flags
js
-sanitize-coverage=func
-sanitize=undefined
获取启动相关的函数符号,并且直接写入.order文件
swift
@interface ViewController ()
@property (nonatomic, strong) Login *login;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.login = [[Login alloc] init];
[self.login login];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(writefile)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}
- (void)writefile {
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (true) {
//offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
if (node == NULL) break;
Dl_info info;
dladdr(node->pc, &info);
NSString * name = @(info.dli_sname);
// 添加 _
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
//去重
if (![symbolNames containsObject:symbolName]) {
[symbolNames addObject:symbolName];
}
}
//取反
NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
NSLog(@"%@",symbolAry);
//将结果写入到文件
NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (result) {
NSLog(@"%@",filePath);
}else{
NSLog(@"文件写入出错");
}
}
//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
void * pc;
void * next;
}SymbolNode;
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N; // Counter for the guards.
if (start == stop || *start) return; // Initialize only once.
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N; // Guards should start from 1.
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
*node = (SymbolNode){PC,NULL};
//入队
// offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}
因为这里是demo,我是直接在ViewController测试的,进入后台就会把启动相关函数写入设置tmp目录下文件里
运行项目拿到app启动相关函数符号
运行项目进入后台,xcode-> windows -> device and simulators 里下载对应的.order文件,例如我的demo如下: 确实和启动调用的方法一致
设置ld链接器依赖的order file
准备工作
如何测量我们对order file的修改生效了
这里介绍下link map ,它是编译期间的产物,记录符号加载的顺序,默认情况下和 xcode-> build phases -> compile source 的类的顺序一致
配置输出link map步骤
设置link map输出路径和允许输出link map
新建一个demo,这里以我的为例 运行项目后拿到设置路径下link map文件(下方是我截取了关键信息):
js
# Symbols:
# Address Size File Name
0x100004AE0 0x00000028 [ 1] -[Login login]
0x100004B08 0x00000024 [ 1] _sancov.module_ctor_trace_pc_guard
0x100004B2C 0x0000008C [ 2] -[ViewController viewDidLoad]
0x100004BB8 0x0000021C [ 2] -[ViewController writefile]
0x100004DD4 0x0000006C [ 2] ___sanitizer_cov_trace_pc_guard_init
0x100004E40 0x0000004C [ 2] ___sanitizer_cov_trace_pc_guard
0x100004E8C 0x00000024 [ 2] _sancov.module_ctor_trace_pc_guard
0x100004EB0 0x00000028 [ 3] -[Register registerUser]
0x100004ED8 0x00000024 [ 3] _sancov.module_ctor_trace_pc_guard
0x100004EFC 0x0000023C [ 4] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100005138 0x00000090 [ 4] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x1000051C8 0x0000001C [ 4] -[AppDelegate application:didDiscardSceneSessions:]
0x1000051E4 0x00000034 [ 4] -[AppDelegate objc]
0x100005218 0x0000003C [ 4] -[AppDelegate setObjc:]
0x100005254 0x00000034 [ 4] -[AppDelegate objc1]
0x100005288 0x0000003C [ 4] -[AppDelegate setObjc1:]
0x1000052C4 0x00000034 [ 4] -[AppDelegate login]
0x1000052F8 0x0000003C [ 4] -[AppDelegate setLogin:]
0x100005334 0x00000034 [ 4] -[AppDelegate registerOBj]
0x100005368 0x0000003C [ 4] -[AppDelegate setRegisterOBj:]
0x1000053A4 0x00000074 [ 4] -[AppDelegate .cxx_destruct]
0x100005418 0x00000024 [ 4] _sancov.module_ctor_trace_pc_guard
0x10000543C 0x00000104 [ 5] _main
0x100005540 0x00000024 [ 5] _sancov.module_ctor_trace_pc_guard
0x100005564 0x00000028 [ 6] -[Util Utiltool1]
0x10000558C 0x00000028 [ 6] -[Util Utiltool2]
0x1000055B4 0x00000028 [ 6] -[Util Utiltool3]
0x1000055DC 0x00000028 [ 6] -[Util Utiltool4]
0x100005604 0x00000028 [ 6] -[Util Utiltool5]
0x10000562C 0x00000028 [ 6] -[Util Utiltool6]
0x100005654 0x00000028 [ 6] -[Util Utiltool7]
0x10000567C 0x00000028 [ 6] -[Util Utiltool8]
0x1000056A4 0x00000028 [ 6] -[Util Utiltool9]
0x1000056CC 0x00000028 [ 6] -[Util Utiltool10]
0x1000056F4 0x00000024 [ 6] _sancov.module_ctor_trace_pc_guard
0x100005718 0x0000001C [ 7] -[SceneDelegate scene:willConnectToSession:options:]
0x100005734 0x0000001C [ 7] -[SceneDelegate sceneDidDisconnect:]
0x100005750 0x0000001C [ 7] -[SceneDelegate sceneDidBecomeActive:]
0x10000576C 0x0000001C [ 7] -[SceneDelegate sceneWillResignActive:]
0x100005788 0x0000001C [ 7] -[SceneDelegate sceneWillEnterForeground:]
0x1000057A4 0x0000001C [ 7] -[SceneDelegate sceneDidEnterBackground:]
0x1000057C0 0x00000034 [ 7] -[SceneDelegate window]
0x1000057F4 0x0000003C [ 7] -[SceneDelegate setWindow:]
0x100005830 0x00000038 [ 7] -[SceneDelegate .cxx_destruct]
0x100005868 0x00000024 [ 7] _sancov.module_ctor_trace_pc_guard
0x10000588C 0x00000028 [ 8] -[Tool Tooltool1]
0x1000058B4 0x00000028 [ 8] -[Tool Tooltool2]
0x1000058DC 0x00000028 [ 8] -[Tool Tooltool3]
0x100005904 0x00000028 [ 8] -[Tool Tooltool4]
0x10000592C 0x00000028 [ 8] -[Tool Tooltool5]
0x100005954 0x00000028 [ 8] -[Tool Tooltool6]
0x10000597C 0x00000028 [ 8] -[Tool Tooltool7]
0x1000059A4 0x00000028 [ 8] -[Tool Tooltool8]
0x1000059CC 0x00000028 [ 8] -[Tool Tooltool9]
0x1000059F4 0x00000028 [ 8] -[Tool Tooltool10]
这时加载顺序默认和xcode-> build phases -> compile source 的类的顺序一致
OK,到这里操作order file之前的样本我们准备完毕了,继续往下我看
设置demo的.order文件
拿前面我们的到的APP启动时依赖函数符号.order设置为ld链接器
运行App分析link map
可以看到link map生成符号信息也发生了改变,说明我们修改.order file生效了,确实按我们设置的函数符号顺序进行加载的
前面我们确定完成了ld优先设置启动相关的函数符号,同时通过link map确定生效了,但是对启动时间到底有多少提升呢,我们继续往下看
衡量二进制重排成果
xcode提供了System Trace工具提供发生page fault事件的次数和消耗时间
测试方案
- 启动方式:接近冷启动,使用前多使用其他app,尽可能对app缓存进行覆盖清洗
- 设备选择:最好低性能设备,这样每次测试前打开几个应用就可以清除测试app的内存
- 测试前:需要确认link map的符号顺序确保是优化前后对应的,避免误测
优化前 page fault: 514 150.57ms
优化后 page fault: 170 55.89ms
相比优化前减少: 94.68s
相关信息
- demo
- 获取启动相关函数的pod库
感谢您的阅读,本篇文章已阅读完毕,欢迎浏览我的其他文章!