为什么需要二进制重排?
因为iOS App 进程访问内存时, 操作的内存都是虚拟内存,而不是直接访物理内存。虚拟内存和物理内存之间的映射是通过虚拟内存表。虚拟内存表的最小单位是页。iOS 内存页的大小为 16K (macOS是 4k)。
iOS 进程冷启动之前, 所有的方法和数据都没有加载进内存, 在启动时访问具体方法时,通过虚拟内存表查询到相关的方法未物理内存中,这个时会发生缺页中断, 也就是所说的 (page fault)。这个缺页中断如果发生的次数太多, 就会影响启动时间。 (内存置换:发生在iOS系统杀死不活跃的后台App时)
如果启动时用到的方法分配到大量不同的内存页中, 发生缺页中断的次数就会越大, 为了减少这个次数, 我们可以将启动时用到的方法优先排在前面。 这就是要说的二进制重排。
查看 page fault 次数
打开Instrument
的system 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 目录了
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
会更安全。