# iOS启动优化之二进制重排文章已经了解了App启动优化和order文件的作用,今天我们研究clang插桩和咱们自动生成Order文件。
一. 探索插桩
进程如果能之间访问物理内存无疑是很不安全的,所以操作系统在物理内存的上层又建立了一层虚拟内存。为了提高效率和方便管理,又对虚拟内存和物理内存又进行分页(Page)。当进程访问一个虚拟内存Page而对应的物理内存却不存在时,会触发一次缺页中断(Page Fault),分配物理内存,有需要的话会从磁盘mmap读入数据。
通过App Store渠道分发的App,Page Fault还会进行签名验证,所以一次Page Fault的耗时比想象的要多:
重排order:目的就是通过排列启动时需要使用的函数,把启动时需要函数分配到开头的虚拟内存Page中,来减少Page Fault可能的次数,进而提升App启动的效率。
核心问题:
- 重排效果怎么样 - 获取启动阶段的Page Fault次数
- 重排成功了没 - 拿到当前二进制的函数布局
- 如何重排 - 让链接器按照指定顺序生成Mach-O
- 重排的内容 - 获取启动时候用到的函数 (参考资料:字节跳动:Leo的二进制重排提升App启动速度25%)
1、获取符号数量,启动时刻符号
参考:Clang技术文档
新建一个工程TraceDemo:然后Build Settings->Other c file 配置 -fsanitize-coverage=func,trace-pc-guard 参数如图:
再次编译工程就会报错:
找不到这两个函数,那么系统在我们配置完后需要实现这两个函数。
//原子队列 线程安全 先进后出 (/*只能保存结构体*/不用讲)
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
typedef struct {
void * pc;
int abc;
} SYNode;
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
uint32_t *stop) {
static uint64_t N;
if (start == stop || *start) return;
printf("INIT: %p %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++)
*x = ++N;//内存平移
NSLog(@"%d",N);
}
//每调用一个方法就会执行一次__sanitizer_cov_trace_pc_guard函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"%s",__func__);
}
断点运行发现,start 内存地址是以4字节连续平移存储的,然后在__sanitizer_cov_trace_pc_guard_init中计算平移可得到地址数据。
运行代码发现这个工程启动需要27个符号。
void test(void) {
NSLog(@"%s",__func__);
}
void (^block) (void) = ^{
NSLog(@"%s",__func__);
};
添加这两个函数在运行代码:
发现27增加到了29了。所以不管是隐藏函数,函数,block都会在启动函数中。
APP在启动时每调用一个方法就会执行一次__sanitizer_cov_trace_pc_guard函数。验证添加代码:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"%s",__func__);
test();
block();
}
看到打印了,前执行回调在执行方法,
每次都会在执行方法前回调一次__sanitizer_cov_trace_pc_guard函数。
//每调用一个方法就会执行一次__sanitizer_cov_trace_pc_guard函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// NSLog(@"%s",__func__);
if (!*guard) return;
void *PC = __builtin_return_address(0);
//可以获取一个函数名称和地址
Dl_info info;
dladdr(PC, &info);
printf("dli_fname = %s\n",info.dli_fname);
printf("dli_fbase = %p\n",info.dli_fbase);
printf("dli_sname = %s\n",info.dli_sname);
printf("dli_saddr = %p\n",info.dli_saddr);
}
运行代码查看打印:
我们就得到了APP运行的启动需要调用的函数符号。
(PS:这些代码是我抄的,其实我自己写不出来)
2、保存符号,生成Order文件
//每调用一个方法就会执行一次__sanitizer_cov_trace_pc_guard函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
//可以获取一个函数名称和地址
Dl_info info;
dladdr(PC, &info);
printf("dli_sname = %s\n",info.dli_sname);
}
打印结果:
**INIT: 0x1014cf8d0 0x1014cf948**
**2022-07-10 16:36:28.586884+0800 TraceDemo[31461:846896] 30**
**dli_sname = main**
**dli_sname = -[AppDelegate application:didFinishLaunchingWithOptions:]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate setWindow:]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate scene:willConnectToSession:options:]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[SceneDelegate window]**
**dli_sname = -[ViewController viewDidLoad]**
**dli_sname = -[SceneDelegate sceneWillEnterForeground:]**
**dli_sname = -[SceneDelegate sceneDidBecomeActive:]**
看到打印,我们要生成Order文件 需要去除重复,记录或者写入不重复的符号到Order文件中。
第一步保存所有字符:
//原子队列 线程安全 先进后出 (/*只能保存结构体*/不用讲)
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
typedef struct {
void * pc;
int abc;
} SYNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
NSLog(@"%s",__func__);
if (!*guard) return;
void *PC = __builtin_return_address(0);
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,0};
//插入结构体
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, abc));
}
第二步生成order文件,并去除重复:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//创建数组
NSMutableArray *symbleNames = [NSMutableArray array];
while (YES) {
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, abc));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
//转为OC字符串方便操作
NSString *name = @(info.dli_sname);
//OC方法 直接添加到数组
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbleName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbleNames addObject:symbleName];
}
NSEnumerator *em = [symbleNames reverseObjectEnumerator];
//新数组存储
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
NSString *name;
while (name = [em nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//数组转为字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
//文件路径
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lg.order"];
//文件内容
NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
//写入文件
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
}
运行代码可知:
(swift 需要配置才能被编译器执行到order中):
3、总结
1.目的就是通过排列启动时需要使用的函数,把启动时需要函数分配到开头的虚拟内存Page中,来减少Page Fault可能的次数,进而提升App启动的效率。
2.需要了解系统编译运行过程,及clang相关知识。才有能力思考解决相关问题。
3.在通过一步一步验证,实现最终生成order文件提高App启动效率的目的。