启动优化--二进制重排

257 阅读4分钟
  • 二进制重排也称clang插桩(swift插桩)

  • 官方文档地址 clang 插桩

  • 急需使用,直接看第三部分代码即可

1. LLVM

  • 再学习 二进制重排 之前,我们需要先了解下 苹果出的一个伟大的编译架构--LLVM

  • LLVM 采用了前、后端分离的模式,可以使前、后端独立开发

LLVM设计图.png

这样提供了相当大的便利:例如新研发一门语言(swift)只需要前端开发。新产出一种架构(M1)只需要后端开发即可

1.1. LLVM 编译架构(前端、优化器、后端)

    1. 前端主要是 代码解释器,主要做 预编译(宏展开,头文件导入)编译(词法分析、语法分析,语义分析,抽象语法树生成)中间IR代码生成
    1. 优化器 主要是优化IR代码,把传入的IR代码通过优化等级进行优化(可在XCodeBuild SettingsOpitmization Level 中设置)
    1. 后端主要是链接生成可执行文件,(如果工程设置bitcode,苹果会多一步优化,把.ll-->.bc)把传入的IR代码进行汇编,生成目标(object)文件,然后连接(符号绑定),之后生成可执行文件(executable)

编译链接图.png

2. 虚拟内存和物理内存

  • 虚拟内存是一张表,而物理内存是真实的内存。

  • 应用的启动(虚拟内存,物理内存模仿):

物理内存和虚拟内存.png

3. 二进制重排

  • 加载页表的时候,会有极短的时间出现缺页中断(PageFault),出现一次的缺页中断感知不到的,但是当冷启动的时候,会出现大量的缺页中断(PageFault),用户就会感知到

  • 二进制重排主要减少的是缺页中断(PageFault)的情况。

  • 我们再说一下启动过程,毕竟二进制重排是优化的启动。

    • 我们在程序运行时,分为冷启动热启动.
    • 冷启动: 内存中没有应用信息,从磁盘中启动
    • 热启动: 内存中有内存信息,通过内存启动
  • 我们可以通过工具 Instruments 观看缺页中断(PageFault)的次数。(Instruments 的使用这里就不说了。)

3.1. 代码实现

  • 玩过源码的兄弟们都可以知道,在objc源码中有个.order文件

objc-order文件.png

这个就是可以排序启动页表中function顺序的

那么问题来了,我们怎么可以获取他的启动顺序?(当时我也想过好多办法,就是hook函数之类的,但是都不理想,直到知道了clang插桩)

目的:获取启动后,所有方法顺序,然后写到order文件中

  • 官网 :clang 插桩,已经有了思路和实现方法了。我就直接写代码了

  • 1、 在build SettingOther C flag 中添加 -fsanitize-coverage=func,trace-pc-guard

Other C Flag.png

  • 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、 最后加入到工程中就可以了

order添加.png

当然我也把 launch.order 文件放在了根目录

注意:这写的代码,不要带进提审包中。只在提审工程中加入order文件即可。

3.2. 扩展

  • 说下swift插桩,原理和clang插桩是一样的,只需要在 Other Swift Flags加入设置即可

swift flag.png

-sanitize-coverage=func -sanitize=undefined

4. 总结

  • 二进制重排可以马上用在工程中的,而且比较大的工程效果很明显(我们做的一个游戏,从3.6s优化到2.7s左右)

  • 另外我觉得,实现二进制重排网上好多文章,不是最重要的,重要的是编译链接这一块,对我们来说比较重要,毕竟这一块有些了解,我们看问题可能会更清澈一些

  • 最后,如果小谷接触到其他比较有用的优化技巧 就再来篇博客