美文网首页
iOS 启动优化之二进制重排

iOS 启动优化之二进制重排

作者: AndyGF | 来源:发表于2020-05-25 12:37 被阅读0次

首先需要了解什么是二进制重排, 二进制重排为什么能减少启动时间.

编译器把源文件编译成Mach-O可执行文件时, 是按照 Build Phases -> Compile Sources 中的文件顺序进行编译各个类文. 在 App 启动时, DYLD并不会把所有二进制都加载到内存中等待调用, 当调用某个方法或者函数时, 内存中已经存在的不需要重新加载, 如果不存在就去加载, 这个加载过程会堵塞主线程, 是个耗时过程, 这个加载过程叫缺页加载(Page Fault), 每次缺页加载大概是 6 - 8 ms(抖音给出的时间), 这个时间并不是确定的, 和程序的实际情况有关系. App 的启动时间是从点击 App 图标开始到第一个界面展示出来的时间, 可以划分为 main 函数之前和 main 函数之后, Page Fault 发生在 main 函数之后, 二进制重排可以让编译器 不 按照 Build Phases -> Compile Sources 中的文件顺序进行编译各个类文, 而是按照我们指定的顺序, 把 main 函数到第一个界面展示这之间用到的类文件等放在最前边, 尽量减少 Page Fault 次数, 心达到减少启动时间的目的.

Page Fault 自己百度,有时间我会补上这个点.

二进制重排是靠什么实现的呢 ?

需要在根目录放一个以 ".order" 为后缀的文件并在添加到配置中 , Build Setting 中搜索 order file, 添加这个文件的路径,如: ./xxx.order (根目录下), 这个文件里放的是我们指定的需要优化加载的符号, 此处只放 main 函数之后到第一外界面展示用到的符号. 有了这个文件, 编译器就会把文件里的符号放在最前边, 这样先用到的符号就会优化加载到内存中, 当使用的时候就会减少 Page Fault 次数.

lb.order

那么问题来了, 这个 "xxx.order" 文件从哪里来,

这就需要 Hook 到 App 在启动时到底调用了哪些方法或者函数等, 然后将这些符号写到文件中, 具体用哪种方案去 Hook 呢, 此处应该用 Clang 插桩技术. 理由如下:

runtime 的 Method Swizzling 只能 Hook 到 OC 的方法.
fishhook 只能 Hook 系统的 C 函数.
Clang 静态插桩可以 Hook 到所有 OC 方法, C 函数, Block, Swift 函数, 闭包.

写代码前需要添加两项配置,

  • Build Setting -> Other C Flags 和 Build Setting -> Other C++ Flags 添加 fsanitize-coverage=func,trace-pc-guard
other c/c++ flags
  • Build Setting -> Other Swift Flags 添加 -sanitize-coverage=func -sanitize=undefined
other swift flags

下面开始上代码,

#import "ViewController.h"
// swift 桥接文件
#import "ChaZhuangOCDemo-Swift.h"
// Clang 插桩需要的头文件
#import <dlfcn.h>
#import <libkern/OSAtomic.h>


@interface ViewController ()

@end

@implementation ViewController

- (void)setupUI {
    
    self.view.backgroundColor = UIColor.whiteColor;
    self.title = @"Clang 插桩测试 Demo";
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self setupUI];
    
    [self testOne];

    TwoController *twoVC = [[TwoController alloc] init];
    twoVC.view.frame = [UIScreen mainScreen].bounds;
    [self.view addSubview:twoVC.view];
    
}

+ (void)load {
    
}

//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;

//定义符号结构体
typedef struct {
    void * pc;
    void * next;
} SymbolNode;

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.
    }
}

// Hook 到 符号, 保存到原子队列中
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//    if (!*guard) return;  // Duplicate the guard check.
 
    void *PC = __builtin_return_address(0);
    
    SymbolNode * node = malloc(sizeof(SymbolNode));
    
    * node = (SymbolNode) {
        PC,
        NULL
    };
    
    //入队
    // offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
    OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}

// 从队列里取出 Hook 的符号,并写到 .order 文件中, 我用 lb.order 作为文件名.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    
    NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
    
    while (true) {
        
        //offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
        SymbolNode *node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, next));
        
        if (node == NULL) { break; }
        
        Dl_info info;
        
        dladdr(node->pc, &info);
        
        NSString * name = @(info.dli_sname);
        
        // block c函数 swift函数 添加下划线 _
        BOOL isObjc = [name hasPrefix: @"+["] || [name hasPrefix: @"-["];
        
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingFormat: @"%@", name];
        
        if (![symbolNames containsObject: symbolName]) {
            [symbolNames addObject: symbolName];
        }
        
        // 数组倒序
        NSArray * symbolArray = [[symbolNames reverseObjectEnumerator] allObjects];
        NSLog(@"symbolArray = %@", symbolArray);
        
        // 写入文件
        NSString * funcString = [symbolArray componentsJoinedByString: @"\n"];
        NSLog(@"写入文件的字符串: %@", funcString);
        NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
        NSData * fileContents = [funcString dataUsingEncoding: NSUTF8StringEncoding];
        BOOL result = [[NSFileManager defaultManager] createFileAtPath: filePath contents: fileContents attributes: nil];
        if (result) {
            NSLog(@"%@",filePath);
        }else{
            NSLog(@"文件写入出错");
        }
    }
}


- (void)testOne {
    
    NSLog(@"I am testOne");
    testCFunction();
}

void testCFunction() {
    lldbBlock();
}

void (^lldbBlock)(void) = ^(void){
    NSLog(@"I am block");
};

@end

TwoController 是个 swift 类, 代码如下:


import UIKit

class TwoController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        
        testSwiftFunc()
    }
    
    func testSwiftFunc() {
        
        print("已经调用了 swift")
    }
}

用 真机 把程序跑起来, 第一个界面出现之后, 点击屏幕, lb.order 文件就已经写在 tmp 文件夹下了,

把手机里的项目文件下载到电脑

找到下载的文件, 显示包内容, 搜索 lb.order, 将其放入根目录中, 下次编译时这些符号就会排列在前面, 这个文件打开查看是这样的, 这里边所有的符号都是 main 函数之后到 第一个界面展示 用到的所有符号.

lb.order

如何查看是否已经重排成功, 在 Build Setting 里 搜索 link map, 找到 Write Link Map File 改成 YES, 记得完成重排之后, 要改成 NO .

link map 配置

设置好之后再次编译就可以, 不需要 run, 然后找出 link map 文件, 找到 products 下的 app, 然后 show in finder, 往回找 两级就是 products 文件夹, 然后找到与 products 同级的 Intermediates.noindex , 按下图找到 link map 文件, txt 格式的,

项目名称.app
link map txt 文件

link map 文件打开之后找到 symbols 下面这部分, 对比 lb.order 和 此处的顺序是否一致, 一致就说明重振成功. 然后删除查找符号相关代码, 保留 lb.order 和 order file 配置.

link map symbols

注意: 如果 lb.order 中的符号在项目中找不到, 编译器会忽略掉, 并不会报错.

相关文章

网友评论

      本文标题:iOS 启动优化之二进制重排

      本文链接:https://www.haomeiwen.com/subject/hvcknhtx.html