零. 前言
头条团队去年编写的基于二进制文件重排的解决方案,为APP启动速度提升了超过15%,引起了各路大神的兴趣,业界也多了几篇优质的二进制重排的文章,下面我将会尝试用这些文章的方法实践一下,看看效果,也研究一下能不能应用于自己的工程内。
一. 有关Order File
在一份说明文档中,他的描述是这样的,大概意思就是将生成的order file的路径配置到 Xcode 的 Build Settings 中的 Order File 配置项,随后链接器就会按照 order file 中的顺序来排列符号了。
The path to a file which alters the order in which functions and data are laid out. For each section in the output file, any symbol in that section that are specified in the order file is moved to the start of its section and laid out in the same order as in the order file. Order files are text files with one symbol name per line. Lines starting with a # are comments. A symbol name may be optionally preceded with its object file leafname and a colon (e.g. foo.o:_foo). This is useful for static functions/data that occur in multiple files. A symbol name may also be optionally preceded with the architecture (e.g. ppc:_foo or ppc:foo.o:_foo). This enables you to have one order file that works for multiple architectures. Literal c-strings may be ordered by quoting the string in the order file (e.g. "Hello, world"). Generally you should not specify an order file in Debug or Development configurations, as this will make the linked binary less readable to the debugger. Use them only in Release or Deployment configurations.
换言之,order file的意义就是让函数方法进行重排,以尽量避免缺页Page Fault
的次数,从而降低启动过程中缺页造成的耗时。
二. 怎么看效果
看效果主要从几个方面来考察:
- 重排成功了没 - 看LinkMap
- 重排效果怎么样 - 看Page Fault次数
- 对于工程的优化效果 - 看启动时间
1. LinkMap
LinkMap的生成方法和结构在我之前写过的LinkMap初探已经提到过,这里简单提一下。
order file在iOS上只支持__text代码段的重排,而对于其余section,如__cstring,__ustring,__const,_objc等都是不支持重排的,所以如果我们想看重排在LinkMap的表现,只需要看# Symbols下的排列方法有没有按照我们的Order File来排序就可以了。
2. Page Fault
其实在头条那篇技术博客也有提到了,就是用Instrument
的System Trace
来看,再结合os_signpost
来定位到启动前的时间段,即可看到Page Fault的次数。
os_signpost可以用来打点,我们只需要在停止收集order file的那个方法里面打个点,在System Trace
上面加个os_signpost
方法,就可以知道order file对Page Fault的效果了。
最后看项目主线程的File Backed Page In,即为缺页次数
更详细的os_signpost教程在这里
3. 启动时间
XCode里面的Edit Scheme
,添加一个DYLD_PRINT_STATISTICS
,对应Value为1即可看到。在Pre-main中,可以大致分为load dylib->rebase->bind->Objc setup-> initializer,开发能掌握和度量的是initializer部分。
每个时间点的具体分析在这里
三. 各种方法尝试
简谈二进制重排提到,trace的方式有两种:编译插桩和运行时插桩,他们各有优缺点。
1. 编译插桩
编译插桩采用的是腾讯大神杨帝的方法,这个方法的思路是利用了SanitizerCoverage
收集到了工程内所有方法,在MainVC的ViewDidAppear方法
停止收集(停止收集的时机可以自定义),即可获取到主VC显示前调用的所有方法,从而生成Order File
。
extern void AppOrderFiles(void(^completion)(NSString *orderFilePath)) {
// 停止收集
collectFinished = YES;
__sync_synchronize();
// 排除当前函数名
NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSMutableArray <NSString *> *functions = [NSMutableArray array];
while (YES) {
PCNode *node = OSAtomicDequeue(&queue, offsetof(PCNode, next));
if (node == NULL) {
break;
}
Dl_info info = {0};
dladdr(node->pc, &info);
if (info.dli_sname) {
NSString *name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[functions addObject:symbolName];
}
}
if (functions.count == 0) {
if (completion) {
completion(nil);
}
return;
}
// 获取到所有的方法,开始遍历
NSMutableArray<NSString *> *calls = [NSMutableArray arrayWithCapacity:functions.count];
NSEnumerator *enumerator = [functions reverseObjectEnumerator];
NSString *obj;
while (obj = [enumerator nextObject]) {
if (![calls containsObject:obj]) {
[calls addObject:obj];
}
}
[calls removeObject:functionExclude];
NSString *result = [calls componentsJoinedByString:@"\n"];
// 写入文档
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"app.order"];
NSData *fileContents = [result dataUsingEncoding:NSUTF8StringEncoding];
BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath
contents:fileContents
attributes:nil];
if (completion) {
completion(success ? filePath : nil);
}
});
对于LinkMap,Order File的确让LinkMap的顺序发生了改变,也说明设置OrderFile是有效的,他会按照生成的app.order的顺序重排了一遍。
2. 运行时插桩
运行时插桩用的则是rhythmkay大佬的方案PGOAnalyzer,不过由于项目并不开源,所以只能按照GitHub说明直接导入Framework来运行,大概原理是通过hook或动态插桩来记录,所以一些.a的函数也可以被拿出来。
导出来的OrderFile行数是编译插桩的6倍,而且确确实实有许多Pod的函数被导出来了,缺页次数和ReOrder差不多,启动时间则有点不稳定,总Pre-main时间时而比之前的长,时而比之前的短,且主工程启动时间较长。
四. 效果对比
为了让效果看上去更明显,我特地拿了一部祖传的、卡的一批的小6,来看看没有OrderFile、使用原代码的生成OrderFile、改良后的OrderFile(我命名为ReOrder)的效果,每个情况运行七次查看缺页次数:
1. 对于Page Fault:
没有Order File的缺页次数
3882 505 661 618 294 426 300
平均缺页:955.142857143
中位数:505
编译插桩的缺页次数
492 303 299 356 769 735 1003
平均缺页:565.285714286
中位数:492
运行时插桩的缺页次数
563 323 1387 2152 296 1382 1521
平均缺页:1089.14285714
中位数:1382
虽然没有设置OrderFile的平均次数看上去高一点,但只不过是第一次偶然的3000+的缺页拖高了平均数,其他数据看上去比较正常..相对来说,有没有OrderFile看上去对缺页影响也不大。
2. 对于启动时间:
启动时间这个东西有点玄学,即时是加了OrderFile也有可能冲上1.5s,没加OrderFile也有可能少于1s,感觉耗时偏差过大,也不好作对比,反正我是没看出什么明显的效果来。
3. 手动测一下首屏时间:
好吧,自己再弄多一个APP,计时一下两个APP从开启到显示首屏的时间,为了看得出点差距,我把手机里面的游戏全部开了一遍,再打开这两个APP,但经过秒表计时,发现两个APP的首屏时间其实还是不相上下,没有看出明显的效果。
我不禁怀疑自己是不是设置OrderFile的姿势不对导致效果不明显,毕竟其他技术团队都宣称OrderFile有着非常显著的重排效果,疑惑之下看到了杨帝下GitHub也有人遇到效果不明显的疑惑,和杨帝的回复:
好吧,既然杨帝也这样说了,那这次的二进制重排探索先告一段落了,之后再看看有没有什么有利于工程的优化点可做了= =