iOS启动优化
概述:通过二进制重排实现iOS启动优化。
page fault(缺页异常/缺页中断)
当CPU通过一个虚拟地址去访问内存,如果虚拟地址对应的物理地址不再内存中,操作系统会阻塞当前进程,发出一个缺页异常/缺页中断(pagefault),然后将磁盘中对应页的数据加载到内存中,完成虚拟内存和物理内存的映射。
二进制重排
二进制重排是做了什么,在启动阶段减少 page fault ,从而提升程序启动速度。
如何做二进制重排,可以通过配置 .order 文件来实现。我们在objc4的源码种也能找到一个order文件如下图。
我们现在所有做的就是如何找到app启动时所有用到的方法,然后生成一个order文件,让编译器帮我们做重排操作,这样这些启动时用到的方法,就会相对集中,不会分散到太多的页。目前iOS每页是16k。
插桩
我们可以通过插桩(clang、Swift)的方式,实现对 objc方法,block,c函数实现插桩,clang是(c、c++、objc)前端,Swift是swift语言前端。
这里主要说一下,clang插桩, Swift插桩道理是一样的,只是配置不同。
Clang插桩
clang 插桩支持三种粒度:
-
edge :边界插桩
-
bb:basic blocks (基本块)插桩
-
func: 方法插桩
基本块:基本块是满足下列条件的联系的三地址指令序列
- 控制流只能从基本块的第一个指令入块。
- 除了基本块的最后一个指令,控制流在离开基本之前不会跳转或者停机。
流图:由基本块块组成程序运行流程图。
如果想要更深入的了解基本块和流图的相关知识,可以看编译原理的书籍。
在 Other C Flags分别配置 -fsanitize-coverage=edge,trace-pc-guard
-fsanitize-coverage=bb,trace-pc-guard
-fsanitize-coverage=func,trace-pc-guard
,来支持三种不同类型的插桩。
目前我们只需要配置 func粒度即可,因为一个函数可能存在好几个基本块,所以我们不需要这么细的插桩粒度。因为我们只需要启动时用到了哪些方法即可。
Clang 插桩代码:
- 可以在一个控制器实现如下代码
我们需要实现 __sanitizer_cov_trace_pc_guard_init
,__sanitizer_cov_trace_pc_guard
这两个函数
其中:如果我们设置的是func粒度,__sanitizer_cov_trace_pc_guard
会在编译期插入到函数的起始位置。
由于启动的时的函数可能存在多线程问题,我们这里可以使用原子队列。
//定义原子队列
static OSQueueHead symbolQueue = OS_ATOMIC_QUEUE_INIT;
//定义结构体
typedef struct {
void *pc;
void *next;
} SQNode;
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) {
void *PC = __builtin_return_address(0);
char PcDescr[1024];
Dl_info info;
dladdr(PC, &info);
NSLog(@"%@",[NSString stringWithUTF8String:info.dli_sname]);
SQNode *node = malloc(sizeof(SQNode));
node->next = NULL;
node->pc = PC;
OSAtomicEnqueue(&symbolQueue, node, offsetof(SQNode, next));
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray *symbolNameArray = [NSMutableArray array];
while (YES) {
SQNode *node = OSAtomicDequeue(&symbolQueue, offsetof(SQNode, next));
if(node == NULL){
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSString *name = [NSString stringWithUTF8String:info.dli_sname];
BOOL isOCFunc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isOCFunc ? name : [@"_" stringByAppendingString:name];
if (![symbolNameArray containsObject:symbolName]) {
[symbolNameArray insertObject:symbolName atIndex:0];
}
}
[symbolNameArray removeObject:[NSString stringWithFormat:@"%s",__func__]];
NSString * symbolStr = [symbolNameArray componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"test.order"];
NSData * file = [symbolStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
NSLog(@"%@",symbolNameArray);
NSLog(@"%@",filePath);
}
把生成的test.order文件配置到xcode如图:
Link Map File
- 直译为链接映射文件,是
Xcode
生成可执行文件时一起生成的文本,用于记录链接相关信息。
如何知道二进制重排成功,可以看linkmap文件的 Symbols 部分看一下,启动时候用到函数方法的符号是不是排在了前面,如果是,证明成功。
如图:
对比上图可知重排成功