二进制重排这个概念,最早提出是由抖音团队技术分享的一篇文章,使大家能知道这个技术,当时是在19年,里面运用的是针对OC、C++、C、block的分别单独hook处理,但是只有OC能进行全部获取,C++、C、block、swift等处理依旧有缺陷,这篇文章会利用抖音结尾的进一步优化操作进行获取全部符号表。
原因
由于现在的操作系统中都引入了虚拟内存,由虚拟内存转换为物理内存的映射表(页表)是分页的,macOS每页大小为4kb,iOS大小为16kb,在内存分页触发会出现缺页异常,iOS 系统还会对其做一次签名验证,因此 iOS 生产环境的 Page Fault 所产生的耗时要更多。
上图1就是就是虚拟内存通过页表转换为物理内存
上图2是页表的显示,前面的0、1表示的是否已经加载到内存中,后面的是实际的物理地址
当我们启动程序,系统会把所有的文件加载到内存中,加载顺序即是Build Phases,Compile Sources中的文件顺序,如图
加载进去的类会把依次把每个方法加载,但是当前类中的方法有可能是启动时不需要,所以未加入到内存的,这时系统就会触发一次缺页中断,即 Page Fault。
在未优化前,我们当前的程序就有3426次page fault,需要535.79ms
怎么做
APP 的二进制重排很容易做,因为xcode中是可以配置的,在Build Setting中查找 Write Link Map File 改为 YES, 再查找 Order File ,把你编写好的重排文件填入,这样二进制重排就完成了
所以配置二进制重排是很容易的,难的是我们的Order File文件怎么生成,总不能一个个方法去写,然后排列吧。
如何生成Order
生成Order文件,就需要我们知道自己的程序在启动时调用了哪些方法,这些方法的调用顺序是如何的,如果是纯OC代码其实很简单,因为所有的OC代码都会走 objc_msgSend 消息转发方法,所以我们只需要用方法交换进行获取方法列表即可,但是现在的程序很多都会用swift、c、c++混编,这些都是不走 objc_msgSend 方法的,所以获取不全。现在我们可以用编译期插桩方案来 100% 获取方法。
我们可以通过这个地址 clang.llvm.org/docs/Saniti… 去了解,其中给了我们例子。
Other C flags 添加参数 -fsanitize-coverage=trace-pc-guard
**while** (**YES**) {
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, 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];
[symbolNames addObject:symbolName];
}
//取反
NSEnumerator * emt = [symbolNames reverseObjectEnumerator];
//去重
NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
**while** (name = [emt nextObject]) {
**if** (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//干掉自己!
[funcs removeObject:[NSString stringWithFormat:@"%s", **__FUNCTION__** ]];
//将数组变成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:**nil**];
如果是OC与Swift混编的项目,需要在Other swift flags 中添加-sanitize=undefined 、 -sanitize-coverage=func,这样就能获取Swift方法
检测
运行了程序之后,我们怎么知道程序是否进行二进制重排了呢,在生成的项目里会有 项目名-LinkMap-normal-arm64.txt文件,其中可以查看
运行前
运行后
显示加载的顺序是按照我们order文件中配置的顺序加载的,到此就完成了二进制重排
再运行一下APP
由300ms提升到224ms,与抖音提出的提升15%以上的效率结论也基本一致。
总结
LLVM里提供了很多辅助工具,可以实现一些高端操作,这是在我们业务代码之外供我们的提升。现在虽然clang可以获取全部的OC、Swift,但是在flutter、RN里还没找到好的解决方案,需要进一步的去探索。