0%

二进制重排

为什么进行二进制重排

当用户点击 App 启动的时候,系统会创建进程并为进程申请一块虚拟内存,虚拟内存和物理内存是需要映射的。当进程需要访问的一块虚拟内存页还没有映射对应的物理内存页时,就会触发一次缺页中断 Page In。这个过程中会发生 I/O 操作,将磁盘中的数据读入到物理内存页中。如果读入的是 Text 段的页,还需要解密,并且系统还会对解密后的页进行签名验证。所以,如果在启动过程中频繁的发生 Page In 的话,Page In 引起的 I/O 操作以及解密验证操作等的耗时也是影响很大的。需要注意的是,iOS13 及以后苹果对这个过程进行了优化,Page In 的时候不再需要解密了。


Page In 的具体情况我们可以通过 Instruments 中的 System Trace 工具来分析,其中找到 Main Thread 进程,再选择 Summary:Virtual Memory 选项,下面看到的 File Backed Page In 就是对应的缺页中断数据了,从数据上看Page In对启动的影响并非瓶颈,如下图所示:
此处输入图片的描述

在启动过程中过多的 Page In 会产生过多的 I/O 操作以及解密验证操作,这些操作的耗时影响也会比较大。针对 Page In 的影响,我们可以通过二进制重排来减少这个过程的耗时。我们知道进程在访问虚拟内存的时候是以页为单位的,而启动过程中的两个方法如果在不同的页,系统就会进行两次缺页中断 Page In 操作来加载这两个页。而如果启动链路上的方法分散在不同的页的话,整个启动的过程就会产生非常多的 Page In 操作。为了能减少系统因缺页中断产生的 Page In 操作,我们需要做的就是把启动链路上所有用到的方法都排在连续的页上,这样系统在加载符号的时候就可以减少相应的内存页数量的访问,从而减少整个启动过程的耗时,如下图所示:

此处输入图片的描述

要实现符号的重排,一是需要我们收集整个启动链路上的方法和函数等符号,二是需要生成对应的 order 文件来配置 ld 中的 Order File 属性。当工程在编译的时候,Xcode 会读取这个 order 文件,在链接过程中会根据这个文件中的符号顺序来生成对应的 MachO。一般业界中收集符号的方案有两种:

  • Hook objc_msgSend,只能拿到 OC 以及 swift @objc dynamic 的符号;
  • Clang 插桩,能完美拿到 OC、C/C++、Swift、Block 的符号;
    故这里采用Clang插桩方式来搜集符号,具体实现如下:

具体实现

  1. 添加 Build Setting 设置
    Target -> Build Setting -> Custom Complier Flags -> Other C Flags 添加
1
-fsanitize-coverage=func,trace-pc-guard

Other Swift Flags 添加

1
-sanitize-coverage=func -sanitize=undefined

项目如果是组件化的话,需要分别对主工程,及各组件进行设置,可以通过脚本在podfile设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 二进制重排设置
  installer.pods_project.targets.each do |target|
    target.build_configurations.each do |config|
      if config.name == 'Debug'
        config.build_settings['OTHER_CFLAGS'] ||= '$(inherited)'
        config.build_settings['OTHER_CFLAGS'] << ' '
        config.build_settings['OTHER_CFLAGS'] << '-fsanitize-coverage=func,trace-pc-guard'
      end
    end
  end

# 主工程二进制重排设置
  app_project.native_targets.each do |target|
    if target.name == 'AiWayFashionCar'
      target.build_configurations.each do |config|
        if config.name == 'Debug'
          config.build_settings['OTHER_CFLAGS'] ||= '$(inherited)'
          config.build_settings['OTHER_CFLAGS'] << ' '
          config.build_settings['OTHER_CFLAGS'] << '-fsanitize-coverage=func,trace-pc-guard'
        end
      end
    end
  end
  1. 添加代码
    在启动队列添加代码
1
2
3
4
5
#if DEBUG
    AppOrderFiles(^(NSString * _Nonnull orderFilePath) {
        NSLog(@"orderFilePath:%@",orderFilePath);
    });
#endif
1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN
@interface AWLaunchManager : NSObject
extern void AppOrderFiles(void(^completion)(NSString *orderFilePath));
@end
NS_ASSUME_NONNULL_END
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
#import "AWLaunchAnalysisLog.h"
@implementation AWLaunchManager
@end

#if DEBUG

#import <dlfcn.h>
#import <libkern/OSAtomicQueue.h>
#import <pthread.h>

// 队列头的数据结构。
static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
static BOOL collectFinished = NO;
typedef struct {
    void *pc;
    void *next;
} PCNode;

// The guards are [start, stop).
// This function will be called at least once per DSO and may be called
// more than once with the same values of start/stop.
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                         uint32_t *stop) {
    static uint32_t Counter;  // 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 = ++Counter;  // Guards should start from 1.
    }
}

// This callback is inserted by the compiler on every edge in the
// control flow (some optimizations apply).
// Typically, the compiler will emit the code like this:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
// 该回调由编译器插入到
// 控制流(适用一些优化)
// 通常,编译器将发出如下代码:
//    if(*guard)
//      __sanitizer_cov_trace_pc_guard(guard);
// But for large functions it will emit a simple call:
//    __sanitizer_cov_trace_pc_guard(guard);
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
    if (collectFinished || !*guard) {
        return;
    }
    // If you set *guard to 0 this code will not be called again for this edge.
    // Now you can get the PC and do whatever you want:
    //   store it somewhere or symbolize it and print right away.
    // The values of `*guard` are as you set them in
    // __sanitizer_cov_trace_pc_guard_init and so you can make them consecutive
    // and use them to dereference an array or a bit vector.
    *guard = 0;
    // __builtin_return_address(0)的含义是,得到当前函数返回地址,即此函数被别的函数调用,然后此函数执行完毕后,返回,所谓返回地址就是那时候的地址
    void *PC = __builtin_return_address(0);
    PCNode *node = malloc(sizeof(PCNode));
    *node = (PCNode){PC, NULL};
    OSAtomicEnqueue(&queue, node, offsetof(PCNode, next));
}
#endif

extern void AppOrderFiles(void(^completion)(NSString *orderFilePath)) {
#if DEBUG
    collectFinished = YES;
    __sync_synchronize();
    NSString *functionExclude = [NSString stringWithFormat:@"_%s", __FUNCTION__];
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSMutableArray <NSString *> *functions = [NSMutableArray array];
        while (YES) {
            PCNode *node = OSAtomicDequeue(&queue, offsetof(PCNode, next));
            if (node == NULL) {
                break;
            }
            Dl_info info = {0};
            dladdr(node->pc, &info);
            if (info.dli_sname) {
                NSString *name = @(info.dli_sname);
                BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
                NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
                [functions addObject:symbolName];
            }
        }
        if (functions.count == 0) {
            if (completion) {
                completion(nil);
            }
            return;
        }
        NSMutableArray<NSString *> *calls = [NSMutableArray arrayWithCapacity:functions.count];
        NSEnumerator *enumerator = [functions reverseObjectEnumerator];
        NSString *obj;
        while (obj = [enumerator nextObject]) {
            if (![calls containsObject:obj]) {
                [calls addObject:obj];
            }
        }
        [calls removeObject:functionExclude];
        NSString *result = [calls componentsJoinedByString:@"\n"];
//        NSLog(@"二进制重排MethodOrder地址:\n%@", result);
//        printf("%s",[result cStringUsingEncoding:NSUTF8StringEncoding]);
        
        NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"app.order"];
        NSData *fileContents = [result dataUsingEncoding:NSUTF8StringEncoding];
        BOOL success = [[NSFileManager defaultManager] createFileAtPath:filePath
                                                               contents:fileContents
                                                             attributes:nil];
        if (success ) {
            if (completion) {
                completion(filePath);
            }
        }
    });
#endif
}
  1. 取出 order file
  • 在debug模式下编译代码后会在控制台打印 lbfunc.order 的路径
  • 在控制台搜 “orderFilePath”
  • Finder 前往路径取出 order file
  1. 设置 order file
  • 把 lbfunc.order 的路径放到主工程根目录
  • Target -> Build Setting -> Linking -> Order File 设置路径

此处输入图片的描述

查看自己工程的符号顺序

重排前后我们需要查看自己的符号顺序有没有修改成功 , 这时候就用到了 Link Map .

Link Map 是编译期间产生的产物 , ( ld 的读取二进制文件顺序默认是按照 Compile Sources - GUI 里的顺序 ) , 它记录了二进制文件的布局 . 通过设置 Write Link Map File 来设置输出与否 , 默认是 no ,在编译完成后通过验证 LinkMap 文件中 #Symbols: 部分符号顺序是否和 order 文件中的符号顺序一致来确定是否配置成功

此处输入图片的描述

效果对比

优化前

优化后
此处输入图片的描述