一、虚拟内存和物理内存
进程如果能直接访问物理内存无疑是很不安全的,为了解决内存安全,现在的计算机和操作系统在物理内存的基础上又建立了一层虚拟内存。虚拟内存和物理内存这里不做赘述。我们主要通过原理来找到优化App的方案。
1. 虚拟内存
实际上我们平时所看到的进程中可以直接访问的连续内存空间0x000000 ~ 0xffffff,只是一个虚拟地址,需要通过一张映射表映射后才可以获取到真实的物理地址。并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。

2. 虚拟内存分页
刚刚提到虚拟内存和物理内存通过映射表进行映射,但是这个映射并不可能是一一对应的,那样就太过浪费内存了。为了解决效率问题,实际上真实物理内存是分页的。而映射表同样是以页为单位的。换句话说,映射表只会映射到某一页,并不会映射到具体每一个地址。

二、Page Fault
1. Page Fault产生原因

2. Page Fault影响
内存分页触发中断异常 Page Fault 后,会阻塞进程,这是会对性能产生影响的。并且在 iOS 系统的生产环境应用,在发生缺页中断进行重新加载时 ,iOS 系统还会对其做一次签名验证,因此 iOS 生产环境的 Page Fault 所产生的耗时要更多。
对用户而言,使用App时第一个直接体验就是启动 App 时间,而启动时期会有大量的类、分类、三方等等需要加载和执行,此时大量Page Fault所产生的的耗时往往是不能小觑的。
三、二进制重排
1. 二进制重排原理
函数编译在mach-O中的位置是根据ld ( Xcode 的链接器)的编译顺序并非调用顺序来的,因此很可能这两个函数分布在不同的内存页上。

2. 二进制重排操作
苹果已经给我们提供了这个机制,实际上二进制重排就是对即将生成的可执行文件重新排列,这个操作发生在链接阶段。
2.1 Order File

2.2 Linkmap 查看二进制文件布局
Linkmap是iOS编译过程的中间产物,记录了二进制文件的布局,开启步骤如下:
2.2.1 修改Write Link Map File为 YES,然后clean项目并重新编译

-
Products -> show in finder,上上层文件夹,然后找到一个xxx-LinkMap-normal-arm64.txt的txt文件

-
这个文件的
# Symbols:部分存储了所有符号的顺序,前面的 .o 等内容忽略,Address就是实际的物理地址,可用Mach-O工具查看

-
我们发现符号顺序是按照
Compile Sources的文件顺序来排列的
当我们调整Compile Sources中的文件顺序后,会发现符号顺序也有了变化。


2.3 二进制重排原理
我们二进制重排并非只是修改符号地址,而是利用符号顺序,重新排列整个代码在文件的偏移地址,将启动需要加载的方法地址放到前面内存页中,以此达到减少page fault的次数从而实现时间上的优化。
3. 获取App启动时调用的所有方法(使用编译插桩)
备注:Clang插桩实际上就是一个代码覆盖工具 Clang插桩官网地址
要真正的实现二进制重排,我们需要拿到启动时的所有方法、函数等符号,并保存其顺序,然后写入xxx.order文件来实现二进制重排,获取的方案使用 Clang编译插桩。
3.1 在Build Settings中Other C Flags添加编译配置-fsanitize-coverage=func,trace-pc-guard。

3.2 添加完编译配置后,会发现编译报错,如下:

3.3 添加Clang函数
#import "DZHomeViewController.h"
#import <dlfcn.h> // 动态库的显式调用
#import <libkern/OSAtomic.h> //
/*
考虑到插桩方法会调用很多次,使用锁会影响性能,所以使用苹果底层的`原子队列`,其内部实际上是一个链表,遵循先进先出
**/
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
// 定义符号结构体
typedef struct {
void *pc;
void *next;
} PCNode;
@interface DZHomeViewController ()
@end
@implementation DZHomeViewController
void(^blockTest)(void) = ^(void) {
};
+ (void)load {
}
+ (void)initialize {
}
- (void)viewDidLoad {
[super viewDidLoad];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// [self deziTest];
NSMutableArray <NSString *> * symbolNames = [NSMutableArray array];
while (YES) {
PCNode *node = OSAtomicDequeue(&symbolList, offsetof(PCNode, 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];
[symbolNames addObject:symbolName];
}
// 取反
NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
//去重
NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
while (name = [emt nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//干掉自己!
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//将数组变成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"fontResources.order"];
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
- (void)deziTest {
blockTest();
}
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) {
/* 精确定位 哪里开始 到哪里结束! 在这里面做判断写条件!*/
void *PC = __builtin_return_address(0);
DeziNode *node = malloc(sizeof(DeziNode));
*node = (DeziNode){PC,NULL};
//进入
OSAtomicEnqueue(&symbolList, node, offsetof(DeziNode, next));
Dl_info info; // 动态链接库时 通过传递指针给Mach-O头部Mach-O header,引用一个Dl_info结构体
dladdr(PC, &info);
printf("----------------------------------------\nfname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n",
info.dli_fname,
info.dli_fbase,
info.dli_sname,
info.dli_saddr);
}
@end
- dl_info结构体
typedef struct dl_info {
const char *dli_fname; /* 共享对象的路径名 */
void *dli_fbase; /* 共享对象的基本地址 */
const char *dli_sname; /* 最近的符号的名称 */
void *dli_saddr; /* 最近的符号地址 */
} Dl_info;
3.4 汇编断点调试
-
首先打开汇编调试

-
在方法中加断点



-
调试结果



-
结论
3.5 使用__sanitizer_cov_trace_pc_guard
-
断点打印发现
PC就是方法地址
3.6 通过原子队列存取方法
-
插桩时存
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
/* 定位插桩方法的下一个方法,也就是程序中的真实调用方法 */
void *PC = __builtin_return_address(0);
PCNode *node = malloc(sizeof(PCNode));
*node = (PCNode){PC,NULL};
// 进入 &symbolList链表表头,node节点数据,offsetof(PCNode, next) 下一个成员在链表中的偏移地址
OSAtomicEnqueue(&symbolList, node, offsetof(PCNode, next));
}
-
通过touchesBegan方法手动取出原子队列所存方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray <NSString *> *symbolNames = [NSMutableArray array];
while (YES) {
// &symbolList链表表头,
PCNode *node = OSAtomicDequeue(&symbolList, offsetof(PCNode, 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];
[symbolNames addObject:symbolName];
}
// 由于先进先出的特性,所以要取反
NSEnumerator *emt = [symbolNames reverseObjectEnumerator];
// 去重
NSMutableArray<NSString *> *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
while (name = [emt nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
// 去掉自己
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
// 将数组变成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"fontResources.order"];
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
-
将存入本地的
fontResources.order文件取出,放在工程里

-
配置工程的Order File文件

-
二进制重排到此结束,对比前后
xxx-LinkMap-normal-arm64.txt文件,我们会发现启动时调用的方法,已经被排到前边去了


四、使用 System Trace 来检验二进制重排结果
1. 那么如何衡量页的加载时间呢?这里就用到了Instruments中的System Trace工具。

由于获取Page Fault影响因素很多,导致每次获取存在较大波动。只能在尽量保证同一的环境下,多次采样取平均值的来大致估算数据。此处不进行赘述。










