iOS启动优化

概述:通过二进制重排实现iOS启动优化。

page fault(缺页异常/缺页中断)

当CPU通过一个虚拟地址去访问内存,如果虚拟地址对应的物理地址不再内存中,操作系统会阻塞当前进程,发出一个缺页异常/缺页中断(pagefault),然后将磁盘中对应页的数据加载到内存中,完成虚拟内存和物理内存的映射。

二进制重排

二进制重排是做了什么,在启动阶段减少 page fault ,从而提升程序启动速度。

如何做二进制重排,可以通过配置 .order 文件来实现。我们在objc4的源码种也能找到一个order文件如下图。

objcorder.png

我们现在所有做的就是如何找到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如图:

testorder.png

  • 直译为链接映射文件,是 Xcode 生成可执行文件时一起生成的文本,用于记录链接相关信息。

如何知道二进制重排成功,可以看linkmap文件的 Symbols 部分看一下,启动时候用到函数方法的符号是不是排在了前面,如果是,证明成功。

如图:

testlinkmap.png

对比上图可知重排成功