二进制重排启动优化

二进制重排

image.png

前言

需求越来越多,app应用也越来越大,功能越多,导致性能 和体验问题也越来越多,
其他还好说,启动速度最能  直观影响体验,一般我们优化都是减少不必要代码,懒加载
,多线程,删除无用图片代码,压缩文件体积去处理

关于启动

image.png

app启动时,会加载二进制,动态库初始化,对象初始化,执行 load 函数执行 c++ 构造函数,最后进入 main 函数,然后执行 App 初始化逻辑,在这个过程中,会执行代码,在寄存器中不停跳转,完成函数调用和上下文切换
我们说,动态库开始初始化App时,会把应用二进制文件映射进内存,当使用到具体内存时,去触发物理内存加载,然后访问,在这个过程中,就会在寄存器中寻址,取指令,译码,比较耗时的就是去指令,取指令会涉及page fault

Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。这样,进程就可以很方便地访问内存,更确切地说是访问虚拟内存。虚拟地址空间的内部又被分为内核空间和用户空间两部分。并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。

内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系。页表实际上存储在 CPU 的内存管理单元 MMU 中。而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核空间分配物理内存、更新进程页表,最后再返回用户空间,恢复进程的运行,这是一个次缺页异常(minor page fault)。minor page fault 也称为 soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。

major page fault指需要访问的内存不在虚拟地址空间,也不在物理内存中,进入内核空间分配物理内存,更新进程页表,还需要swap从磁盘中读取数据换入物理内存中。

 ![![截屏2020-09-14 上午10.52.32.png](https://upload-images.jianshu.io/upload_images/2318672-1687ccd5fc4fb593.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
](https://upload-images.jianshu.io/upload_images/2318672-a64090a1a4109547.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)


什么是page fault
       当进程访问它的虚拟地址空间中的PAGE时,如果这个PAGE目前还不在物理内存中,此时CPU是不能干活的,Linux会产生一个hard page fault中断。系统需要从慢速设备(如磁盘)将对应的数据PAGE读入物理内存,并建立物理内存地址与虚拟地址空间PAGE的映射关系。然后进程才能访问这部分虚拟地址空间的内存。

page fault 又分为几种,major page fault、 minor page fault、 invalid(segment fault)。

major page fault 也称为 hard page fault, 指需要访问的内存不在虚拟地址空间,也不在物理内存中,需要从慢速设备载入。从swap 回到物理内存也是 hard page fault。

minor page fault 也称为 soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。

当一个进程在调用 malloc 获取虚拟空间地址后,首次访问该地址会发生一次soft  page fault。
通常是多个进程访问同一个共享内存中的数据,可能某些进程还没有建立起映射关系,所以访问时会出现soft page fault
invalid fault 也称为 segment fault,指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问,内核会报 segment fault错误

page fault, (严格说, 这里指的是major page fault)名字听起来挺严重, 实际上, 并不是什么"错误".
大致是这样, 一个程序可能占几Mb, 但并不是所有的指令都要同时运行, 有些是在初始化时运行, 有些是在特定条件下才会去运行. 因此linux并不会把所有的指令都从磁盘加载到page内存. 那么当cpu在执行指令时, 如果发现下一条要执行的指令不在实际的物理内存page中时, CPU 就会 raise a page fault, 通知MMU把下面要执行的指令从磁盘加载到物理内存page中

二进制重排原理

image.png

实现

首先我们打开项目command + i,打开Instruments调试工具,选择System Trace

截屏2020-09-14 上午10.52.32.png

选择真机 , 选择工程,点击开始后,当首个页面加载出来点击停止,这里我们搜索Main thread,选择我们的app,然后点击Main thread ,再到下面选择Main Thread --> Virtual Memory(虚拟内存)
截屏2020-09-14 上午10.56.13.png

二进制重排

  • page fault 过多 会导致二进制不停的执行指令,当执行的代码文件偏移过于随机,会导致寄存器不同切换,触发内存加载,导致指令执行耗时,增大崩溃风险
  • 如果说待执行的关键指令和代码都紧凑的排列在相邻物理页,就能尽可能减少page fault 次数,崩溃概率也会极大降低

我们可以在XCode配置二进制重排,首先我们要确定符号的顺序,才能知道怎么重排,XCode使用的链接器叫做ld,ld有个参数叫order_file,我们可以将文件的路径告诉XCode,在order_file文件中把符号的顺序写进去,XCode编译的时候就会按照文件中的符号顺序打包成二进制可执行文件。


截屏2020-09-14 上午11.24.21.png

项目,在build setting 里面搜索order file,发现这里面指定了order的文件路径,因为一旦在这里指定了order file的路径,XCode就会在编译的时候按照文件里面写进去的顺序

编译,如何查看整个项目的符号顺序呢,我们到Build Settings搜索Link Map,Link Map就是我们链接的符号表,我们把它改成YES

command + R我们运行下,然后在Products里面的.app文件,在我们Intermediates.noindex-->项目名.build--->Debug-iphoneos-->项目名.build--->项目名-LinkMap-normal-x86_64.txt,这个文件里面就有链接的符号顺序表

截屏2020-09-14 上午11.05.40.png
截屏2020-09-14 上午11.06.50.png
截屏2020-09-14 上午11.13.16.png

我们二进制重排并非只是修改符号地址 , 而是利用符号顺序 , 重新排列整个代码在文件的偏移地址 , 将启动需要加载的方法地址放到前面内存页中 , 以此达到减少 page fault 的次数从而实现时间上的优化 , 一定要清楚这一点

怎么做

针对应用中的 objc,c,c++ 代码和符号我们要怎么知道他们的执行顺序并监控呢?即只要我们能通过某种手段 trace 到所有启动阶段执行的函数符号,然后把这些函数符号按顺序排列好,组成 order file 交给编译器即可
网上的实现方案

  • 通过静态扫描和运行时 Trace 等方法确定 order_file (抖音)
参考抖音研发实践:基于二进制文件重排的解决方案 APP启动速度提升超15%

流程如下:

1.设置条件触发流程

2.工程注入Trace动态库,选择release模式编译出.app/linkmap/中间产物

3.运行一次App到启动结束,Trace动态库会在沙盒生成Trace log

4.以Trace Log,中间产物和linkmap作为输入,运行脚本解析出order_file
image.png
  • 基于llvm插桩的方案:(facebook 工具 AppOrderFiles )
简单来说 SanitizerCoverage 是 Clang 内置的一个代码覆盖工具。它把一系列以 __sanitizer_cov_trace_pc_ 为前缀的函数调用插入到用户定义的函数里,借此实现了全局 AOP 的大杀器。其覆盖之广,包含 Swift/Objective-C/C/C++ 等语言,Method/Function/Block 全支持。

开启 SanitizerCoverage 的方法是:在 build settings 里的 “Other C Flags” 中添加 -fsanitize-coverage=func,trace-pc-guard。如果含有 Swift 代码的话,还需要在 “Other Swift Flags” 中加入 -sanitize-coverage=func 和 -sanitize=undefined。所有链接到 App 中的二进制都需要开启 SanitizerCoverage,这样才能完全覆盖到所有调用。

腾讯大神写了个工具 AppOrderFiles。CocoaPods 接入,程序启动完成函数一行调用,生成 Order File。全在 GitHub 里了:github.com/yulingtianx…
  • 手机淘宝团队静态库插桩方案
通过在汇编层面对 pod 编译后的静态库进行插桩。在启动时,插桩后的方法都会调用记录方法,从而获得启动方法的执行顺序。我们编译过的静态库由 .o 文件组成,我们可以对 .o 中的函数代码进行修改,在每个函数的开头插入调用我们指定记录函数的指令。
static OSQueueHead qHead = OS_ATOMIC_QUEUE_INIT;
static BOOL stopCollecting = NO;

typedef struct {
    void *pointer;
    void *next;
} PointerNode;

// start和stop地址之间的区别保存工程所有符号的个数
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint32_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.
    
    printf("totasl count %i\n", N);
}


// 每个函数调用时都会先跳转执行该函数
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    // +load方法先于guard_init调用,此时guard为0
//    if(!*guard) { return }

    if (stopCollecting) {
        return;
    }

    // __builtin_return_address 获取当前调用栈信息,取第一帧地址
    void *PC = __builtin_return_address(0);
    PointerNode *node = malloc(sizeof(PointerNode));
    *node = (PointerNode){PC, NULL};
    // 使用原子队列要存储帧地址
    OSAtomicEnqueue(&qHead, node, offsetof(PointerNode, next));

    
}

extern NSArray <NSString *> *getAllFunctions(NSString *currentFuncName) {
    NSMutableSet<NSString *> *unqSet = [NSMutableSet setWithObject:currentFuncName];
    NSMutableArray <NSString *> *functions = [NSMutableArray array];
    while (YES) {
        PointerNode *front = OSAtomicDequeue(&qHead, offsetof(PointerNode, next));
        if(front == NULL) {
            break;
        }
        Dl_info info = {0};
        // dladdr获取地址符号信息
        dladdr(front->pointer, &info);
        NSString *name = @(info.dli_sname);
        // 去除重复调用
        if([unqSet containsObject:name]) {
            continue;
        }
        BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
        // order文件格式要求C函数和block前需要添加_
        NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        [unqSet addObject:name];
        [functions addObject:symbolName];
    }

    return [[functions reverseObjectEnumerator] allObjects];;

}

#pragma mark - public

extern NSArray <NSString *> *getAppCalls(void) {
    
    stopCollecting = YES;
    __sync_synchronize();
    NSString* curFuncationName = [NSString stringWithUTF8String:__FUNCTION__];
    return getAllFunctions(curFuncationName);
}




extern void appOrderFile(void(^completion)(NSString* orderFilePath)) {
    
    stopCollecting = YES;
    __sync_synchronize();
   NSString* curFuncationName = [NSString stringWithUTF8String:__FUNCTION__];
    // 异步存储到文件中
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *functions = getAllFunctions(curFuncationName);
        NSString *orderFileContent = [functions.reverseObjectEnumerator.allObjects componentsJoinedByString:@"\n"];
        NSLog(@"[orderFile]: %@",orderFileContent);
        NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"orderFile.order"];
        [orderFileContent writeToFile:filePath
                           atomically:YES
                             encoding:NSUTF8StringEncoding
                                error:nil];
        if(completion){
            completion(filePath);
        }
    });
}


深入探索 iOS 启动速度优化

image
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343