-
二进制重排
也称clang插桩(swift插桩)
-
官方文档地址 clang 插桩
-
急需使用,直接看第三部分代码即可
1. LLVM
-
再学习
二进制重排
之前,我们需要先了解下 苹果出的一个伟大的编译架构--LLVM
-
LLVM
采用了前、后端分离的模式
,可以使前、后端独立开发
这样提供了相当大的便利:例如新研发一门语言(swift)只需要前端开发。新产出一种架构(M1)只需要后端开发即可
1.1. LLVM 编译架构(前端、优化器、后端)
-
前端
主要是代码解释器
,主要做预编译(宏展开,头文件导入)
,编译(词法分析、语法分析,语义分析,抽象语法树生成)
,中间IR代码生成
-
优化器
主要是优化IR代码
,把传入的IR代码
通过优化等级
进行优化(可在XCode
的Build Settings
的Opitmization Level
中设置)
-
后端
主要是链接生成可执行文件
,(如果工程设置bitcode,苹果会多一步优化,把.ll-->.bc)把传入的IR代码
进行汇编
,生成目标(object)文件
,然后连接
(符号绑定),之后生成可执行文件
(executable)
2. 虚拟内存和物理内存
-
虚拟内存是一张表,而物理内存是真实的内存。
-
应用的启动(虚拟内存,物理内存模仿):
3. 二进制重排
-
当
加载页表
的时候,会有极短的时间出现缺页中断(PageFault)
,出现一次的缺页中断
是感知不到的
,但是当冷启动
的时候,会出现大量的缺页中断(PageFault)
,用户就会感知到
。 -
二进制重排主要减少的是缺页中断(PageFault)的情况。
-
我们再说一下启动过程,毕竟二进制重排是优化的启动。
- 我们在程序运行时,分为
冷启动
和热启动
. - 冷启动: 内存中没有应用信息,从磁盘中启动
- 热启动: 内存中有内存信息,通过内存启动
- 我们在程序运行时,分为
-
我们可以通过工具
Instruments
观看缺页中断(PageFault)
的次数。(Instruments
的使用这里就不说了。)
3.1. 代码实现
- 玩过源码的兄弟们都可以知道,在
objc源码
中有个.order
文件
这个就是可以排序启动页表中function顺序的
那么问题来了,我们怎么可以获取他的启动顺序
?(当时我也想过好多办法,就是hook函数之类的,但是都不理想,直到知道了clang插桩)
目的:获取启动后,所有方法顺序,然后写到order文件中
-
官网 :clang 插桩,已经有了思路和实现方法了。我就直接写代码了
-
1、 在
build Setting
的Other C flag
中添加-fsanitize-coverage=func,trace-pc-guard
- 2、实现代码
//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
void *pc;
void *next;
} SYNode;
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;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return;
//当前函数返回到上一个调用的地址!!
void *PC = __builtin_return_address(0);
//创建结构体!
SYNode * node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//加入结构!
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
// 调用 此方法 ,生成order 文件
- (void) createOrder {
//定义数组
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {//一次循环!也会被HOOK一次!!
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info = {0};
dladdr(node->pc, &info);
// printf("%s \n",info.dli_sname);
NSString * name = @(info.dli_sname);
free(node);
BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
//是否去重??
[symbolNames addObject:symbolName];
}
//反向数组
// symbolNames = (NSMutableArray<NSString *>*)[[symbolNames reverseObjectEnumerator] allObjects];
NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
//创建一个新数组
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString * name;
//去重!
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {//数组中不包含name
[funcs addObject:name];
}
}
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//数组转成字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
//字符串写入文件
//文件路径
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"launch.order"];
//文件内容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
注: 在.m文件中调用。[self createOrder] 即可
-
3、这样就可以在
tmp 文件夹
找到launch.order
文件。 -
4、 最后加入到工程中就可以了
当然我也把
launch.order
文件放在了根目录
注意:这写的代码,不要带进提审包中。只在提审工程中加入order文件即可。
3.2. 扩展
-
说下
swift插桩
,原理和clang插桩
是一样的,只需要在Other Swift Flags
中加入设置
即可
-sanitize-coverage=func -sanitize=undefined
4. 总结
-
二进制重排
可以马上用在工程中的,而且比较大的工程效果很明显(我们做的一个游戏,从3.6s优化到2.7s左右) -
另外我觉得,实现二进制重排网上好多文章,不是最重要的,
重要的是编译链接
这一块,对我们来说比较重要,毕竟这一块有些了解,我们看问题可能会更清澈一些 -
最后,如果小谷接触到其他比较有用的优化技巧 就再来篇博客