iOS startup optimization -- binary rearrangement

Why is binary rearrangement necessary?

Because when the iOS App process accesses memory, the operating memory is virtual memory instead of directly accessing physical memory. The mapping between virtual memory and physical memory is through virtual memory tables. The smallest unit of a virtual memory table is a page. iOS memory page size is 16K (macOS is 4K).

Before the iOS process is cold started, all methods and data are not loaded into the memory. When accessing a specific method at startup, the relevant method is queried in the physical memory through the virtual memory table. At this time, a page fault will occur. said (page fault). If this page fault occurs too many times, it will affect the startup time. (Memory replacement: occurs when the iOS system kills an inactive background app)

If the methods used at startup are allocated to a large number of different memory pages, the number of page fault interrupts will be greater. In order to reduce this number of times, we can prioritize the methods used at startup in the front. This is the binary rearrangement to speak of.

View page fault times

Open Instrumentand system tracerun the corresponding project on the real machine, (the memory size of one page of the simulator and the real machine is different, 4k, 16k).

Find the main thread under the project, check the count and duration of Summary: Virtual Memory-> , and compare the effects before and after optimization.Page Cache Hint

default method ordering

Create a project such as OCDemo01

In the Build Settingssearch link map, find Write Link Map Fileset to YES.

Compile the project, find OCDemo01.app in the Products directory, go to the upper- show in finderlevel directory, find, find Intermediates.noindex, find the file named OCDemo01-LinkMap-normal-x86_64.txt in this directory: OCDemo01.build/Debug-iphonesimulator/OCDemo01.build.

Because it is an emulator, it is x86_64. Open the file and you can see the sequence of methods:

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 目录了

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…

Guess you like

Origin juejin.im/post/7116405187517874190