iOS 启动优化--二进制重排

为什么需要二进制重排?

因为iOS App 进程访问内存时, 操作的内存都是虚拟内存,而不是直接访物理内存。虚拟内存和物理内存之间的映射是通过虚拟内存表。虚拟内存表的最小单位是页。iOS 内存页的大小为 16K (macOS是 4k)。

iOS 进程冷启动之前, 所有的方法和数据都没有加载进内存, 在启动时访问具体方法时,通过虚拟内存表查询到相关的方法未物理内存中,这个时会发生缺页中断, 也就是所说的 (page fault)。这个缺页中断如果发生的次数太多, 就会影响启动时间。 (内存置换:发生在iOS系统杀死不活跃的后台App时)

如果启动时用到的方法分配到大量不同的内存页中, 发生缺页中断的次数就会越大, 为了减少这个次数, 我们可以将启动时用到的方法优先排在前面。 这就是要说的二进制重排。

查看 page fault 次数

打开Instrumentsystem trace 运行相应的项目在真机上, (模拟器和真机一页的内存大小不一样, 4k, 16k)。

找到项目下的主线程, 查看Summary: Virtual Memory ->Page Cache Hint 的count 数 和 duration, 依此来对比优化前后的效果

默认的方法排序

创建一个例如 OCDemo01 项目

Build Settings 搜索 link map, 找到 Write Link Map File 设置为 YES。

编译项目,找到 Products 目录下的 OCDemo01.app show in finder, 往上上级目录找, 找到 Intermediates.noindex , 找到 OCDemo01.build/Debug-iphonesimulator/OCDemo01.build 这个目录下:名为 OCDemo01-LinkMap-normal-x86_64.txt 的文件。

因为是模拟器, 所以是x86_64. 打开文件, 可以看到方法的顺序:

x1000013C0	0x00000090	[  2] -[AppDelegate application:didFinishLaunchingWithOptions:]
0x100001450	0x00000130	[  2] -[AppDelegate application:configurationForConnectingSceneSession:options:]
0x100001580	0x00000080	[  2] -[AppDelegate application:didDiscardSceneSessions:]
0x100001600	0x00000016	[  2] _sancov.module_ctor_trace_pc_guard
0x100001620	0x000000B0	[  3] _main
0x1000016D0	0x00000016	[  3] _sancov.module_ctor_trace_pc_guard
0x1000016F0	0x000000B0	[  4] -[SceneDelegate scene:willConnectToSession:options:]
0x1000017A0	0x00000060	[  4] -[SceneDelegate sceneDidDisconnect:]
0x100001800	0x00000060	[  4] -[SceneDelegate sceneDidBecomeActive:]
0x100001860	0x00000060	[  4] -[SceneDelegate sceneWillResignActive:]
0x1000018C0	0x00000060	[  4] -[SceneDelegate sceneWillEnterForeground:]
0x100001920	0x00000060	[  4] -[SceneDelegate sceneDidEnterBackground:]
0x100001980	0x00000050	[  4] -[SceneDelegate window]
0x1000019D0	0x00000060	[  4] -[SceneDelegate setWindow:]
0x100001A30	0x00000050	[  4] -[SceneDelegate .cxx_destruct]
0x100001A80	0x00000016	[  4] _sancov.module_ctor_trace_pc_guard
0x100001AA0	0x00000060	[  5] -[ViewController viewDidLoad]

如果是Xcode 13, 找不到 Products 目录

打开 project.pbxpro 这个文件, 找到 productRefGroup 将 mainGroup 的值赋值给它, 就可以看到 Products 目录了

扫描二维码关注公众号,回复: 14360045 查看本文章
mainGroup = 97DABBF6286EFD4500F8A0CA;

productRefGroup = 87DABBF6286EFD4511F8A0CB;

projectDirPath = "";

projectRoot = "";

targets = (

97DABBFE286EFD4500F8A0CA /* OCDemo01 */,

);

order file 修改方法顺序

  • 其实通过手动, 在 Build Phases -> Compile Sources 中拖拽文件也可以修方法的顺序。可以拖拽完编译调试尝试。

  • 还可以通过修改一个文件内, 比如 ViewController.m 中方法前后的排序, 可以更改 link map 文件中方法的顺序。

上面两种方式都很麻烦,不推荐

我们可以通过在 Build Settings 搜索 order file, 找到 Order File 添加 order file 的文件路径, 例如我们在当前项目目录下 添加 ./yong.order

我们通过copy link map file 中的方法排序, 在 yong.order 中添加并修改方法顺序, 完成之后编译,再次查看link file map 文件, 看方法顺序是否是 yong.order 文件中定义的顺序。

如何获取启动时用到的方法? 我们想到的首先肯定是 hook objc_msgSend 方法, 但是这只能获取到 OC 方法, 对于C函数和 block, 无法获取到, 所以这个方案不完美。Clang 插桩 可以解决这个问题。

Clang 插桩

1.配置 Clang 插桩

Build Settings 搜索 other C 找到 Other C Flags 中添加 -fsanitize-coverage=trace-pc-guard

配置完成之后我们编译项目会发现报错,就是告诉我们找不到 __sanitizer_cov_trace_pc_guard_init 与 __sanitizer_cov_trace_pc_guard 这两个函数的实现。所以我们下面实现这两个函数。

我们在 ViewController.m 文件中(其他文件也可以)导入头文件

#include <stdint.h>

#include <stdio.h>

#include <sanitizer/coverage_interface.h>

添加两个方法的实现


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) {

   
}

这次编译, 就不会报错了。

2.获取方法个数

运行项目, 会输出两个16 进制数

**INIT: 0x10dc1f680 0x10dc1f6b8**

代表开始位置与结束位置,x 结束位置 - 4 就代表符号个数。 通过打断点, 可以验证:

**(lldb) x (0x10dc1f6b8-4)**

0x10dc1f6b4: 0e 00 00 00 00 00 00 00 00 00 00 00 0e 00 00 00

0e 代表有 14 个符号。我们再添加两个符号, 例如

void test(void) {

    NSLog(@"test 函数执行");

    block();

}


void(^block)(void) = ^(void){

    NSLog(@"block 函数执行");

};

再次运行, 断点调试之后, 我们发现符号个数增加了两个, 变为 0x10, 也就是16.

在这里我们已经知道方法和函数还有block 的个数可以通过这个__sanitizer_cov_trace_pc_guard_init 方法获取到。

3.获取符号的名称

通过debug 汇编代码, 发现调用 ViewController viewDidLoad 的时候, 会插入__sanitizer_cov_trace_pc_guard 这个C 函数的调用。

// 这里我们导入 `#import <dlfcn.h>` 头文件

// HOOK 一切的函数调用
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {

    void *PC = __builtin_return_address(0);
    Dl_info info; dladdr(PC, &info);
    NSLog(@"fname:%s\n fbase:%p\n sname:%s\n saddr:%p\n",
                        info.dli_fname, 
                        info.dli_fbase, 
                        info.dli_sname,
                        info.dli_saddr);

}

这里 __builtin_return_address 函数的意思就是获取到当前函数的内部返回地址,也就是上一个函数的地址,当前函数的调用者。所以这里 *PC 就是上一个函数的第 0 句代码的地址。这里我们就有机会拿到上一个函数的符号名称。dladdr(PC, &info) 这一句相当于把 PC 指向的地址数据信息赋值给 info 这个结构体。最后通过打印也输出了对应的信息。这里就能拿到所有符号的名称。

结构体说明

typedef struct dl_info {
    const char *dli_fname; /* 文件名称,也就是 mach-o 文件 */ 
    void *dli_fbase; /* 文件地址 */ 
    const char *dli_sname; /* 函数名称 */ 
    void *dli_saddr; /* 函数地址 */ 
} Dl_info;

4.生成order 文件

多线程的问题: 如果是子线程中的方法,那么 __sanitizer_cov_trace_pc_guard 函数也会在子线程中执行,所以这里写入的时候要注意多线程影响,这里通过原子队列保存。

while 循环的影响: 因为 __sanitizer_cov_trace_pc_guard 函数也会拦截到 while 循环,这样写入的时候就会造成递归调用,所以配置 other c flags 参数的时候要多加一个条件 -fsanitize-coverage=func,trace-pc-guard,只拦截方法。

生成 order 文件的时候要对符号方法进行取反,因为写入的时候是倒序的,以及对符号名称去重。

函数名称前面要添加_

添加原子队列

// 导入头文件 #import <libkern/OSAtomic.h>
//定义原子队列

static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;

//定义符号结构体

typedef struct {

    void * pc;

    void * next;

} SYNode;

将符号信息赋值给结构体, 生成队列

void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {

    void *PC = __builtin_return_address(0);

    //创建结构体

        SYNode * node = malloc(sizeof(SYNode));

        *node = (SYNode){PC,NULL};

        //把结构体 node 写入到 symbolList,offsetof(SYNode, next) 表示设置 node 的 next,下一个节点,就是在上一个 next 的基础上加上 SYNode 大小

        OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));

}

生成order 文件


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    //定义数组

        NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];


        while (YES) {//循环体内!进行了拦截!!这里需要注意的是 while 也会被拦截,要做工程中做配置只拦截方法

            SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode,next));

            if (node == NULL) {

                break;

            }
            Dl_info info;

            dladdr(node->pc, &info);

            NSString * name = @(info.dli_sname);//转字符串

            //给函数名称添加 _,这里要判断是否是函数,是函数的话要添加_

            BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];

            NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];

            [symbleNames addObject:symbolName];

        }

        //反向遍历数组,因为入栈函数调用顺序是反的,所以这里要取反

        NSEnumerator * em = [symbleNames reverseObjectEnumerator];

        NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];

        NSString * name;

        // 这里要对数组进行去重,因为有的方法会多次调用

        while (name = [em nextObject]) {

            if (![funcs containsObject:name]) {//数组没有name

                [funcs addObject:name];

            }

        }

        //去掉自己!也就是当前方法

        [funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];

        //写入文件

        //1.编成字符串

        NSString * funcStr = [funcs componentsJoinedByString:@"\n"];

        NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"chenxi.order"];

        NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];

        //写入到沙盒路径

        [[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];

        // 打印数组字符串

        NSLog(@"%@",funcStr);

        // 打印文件地址

        NSLog(@"order 文件地址%@",filePath);

}

这时候我们就可以把 order 文件 配置到工程

Swift 符号覆盖

添加 swift 方法到项目中

需要在other swift flags 这里添加 -sanitize-coverage=func 跟 -sanitize-undefined 这两个参数。

这时候再打印可以看到 swift 方法被收集到了,而且编译器对 swift 符号名称做了混淆,这也可以看出 swift 相对于 OC 会更安全。

参考: juejin.cn/post/700445…

猜你喜欢

转载自juejin.im/post/7116405187517874190