虚拟内存&物理内存
在计算机早期,数据的访问都是通过物理地址访问的,即进程直接对应到具体的物理内存;
这种方式有两个问题
一、内存数据的安全问题(可以通过已知地址+偏移量来获取到内存中数据)
二、内存不够用
针对问题,分别有不同的解决方案
内存不够用:虚拟内存
在进程和物理内存之间增加一个中间层,这个中间层就是所谓的虚拟内存,主要用于解决当多个进程同时存在时,对物理内存的管理。提高了CPU的利用率,使多个进程可以同时、按需加载。所以虚拟内存其本质就是一张虚拟地址和物理地址对应关系的映射表
1、每个进程都有一个独立的虚拟内存,其地址都是从0开始,大小是4G固定的,每个虚拟内存又会划分为一个一个的页表(页表的大小在iOS中是16K,其他的是4K),每次加载都是以页表为单位加载的,进程间是无法互相访问的,保证了进程间数据的安全性;页表的每一个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(如果在的话)
2、一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的部分放入物理内存,避免物理内存的浪费(优化空间)
3、当CPU需要访问数据时,首先是访问虚拟内存,然后通过虚拟内存去寻址,即把地址翻译为实际物理内存地址,然后对相应的物理地址进行访问
4、如果在访问时,虚拟地址的内容未加载到物理内存,会发生缺页异常(PageFault),缺页异常的处理过程,操作系统立即阻塞该进程,并将硬盘里对应的页换入内存,然后使该进程就绪,如果内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪个页,就需要看操作系统的页面置换算法是怎么设计的了
内存数据的安全问题:ASLR
虚拟内存的起始地址与大小都是固定的,这意味着,当访问时,其数据的地址也是固定的,这会导致内存的数据非常容易被破解,为了解决这个问题,所以苹果为了解决这个问题,在iOS4.3开始引入了ASLR技术
ASLR的概念:(Address Space Layout Randomization )地址空间配置随机加载,是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术
其目的的通过利用随机方式配置数据地址空间,使某些敏感数据(例如APP登录注册、支付相关代码)配置到一个恶意程序无法事先获知的地址,令攻击者难以进行攻击
由于ASLR的存在,导致可执行文件和动态链接库在虚拟内存中的加载地址每次启动都不固定,所以需要在编译时来修复镜像中的资源指针,来指向正确的地址。即正确的内存地址 = ASLR地址 + 偏移值
优化方案
优化方案可以根据pre-main以及main函数阶段的优化(本章暂时先不讨论)
接下来着重介绍pre-main阶段的一种优化方案:二进制重排
二进制重排原理:
在虚拟内存部分(上面第4点),已知,当进程访问一个虚拟内存,而对应的物理内存不存在时,会触发缺页中断(Page Fault),因此阻塞进程。此时就需要先加载数据到物理内存,然后再继续访问。这个对性能是有一定影响的
基于Page Fault,App在冷启动过程中,会有大量的类、分类、三方等需要加载和执行,此时的产生的Page Fault所带来的的耗时是很大的。看下图
1、打开Instruments-->System Trace
2、选择真机,工程,启动,首个页面加载出来点击停止(冷启动)
3、查看Main Thread 下的Summary: Virtual Memory
注意:此处1958就是冷启动情况下Page Fault次数,367.57就是耗时
4、可以通过设置Write Link Map File来输出加载顺序
从上面的Page Fault的次数以及加载顺序,可以发现其实导致Page Fault次数过多的根本原因是启动时刻需要调用的方法,处于不同的Page导致的。因此,我们的优化思路就是:将所有启动时刻需要调用的方法,排列在一起,即放在一个页中,来减少Page Fault。这就是二进制重排的核心原理
注意:此处3135是Page fault次数,21.65就是对应的耗时
二进制重排具体步骤:
在进行重排前,需要了解几个名次
Link Map
Link Map是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File,Link Map主要包含三部分
Link Map主要包含三部分
1、object Files 生成二进制用到的link单元的路径和文件编号
2、Sections 记录Mach-O每个Segment/section的地址范围
3、Symbols 按顺序记录每个符号的地址范围(如上面黑色图)
ld
Xcode 是用的链接器叫做ld,ld有一个参数叫Order File, 我们可以通过这个参数配置一个order文件的路径(如下图),在这个order文件中,将所需要的符号按照顺序写在里面,在项目编译时,会按照这个文件的顺序进行加载,以此来达到我们的优化
1、在.order文件中 ,需要将从启动到首页展示出来的符号按顺序写在里面
2、当工程 build 的时候 , Xcode 会读取这个文件 , 打的二进制包就会按照这个文件中的符号顺序进行生成对应的mach-O.
可以通过输出Link Map File来对比重排前后文件的顺序
Link Map File查找路径:
/Users/用户名/Library/Developer/Xcode/DerivedData/App名称/Build/Intermediates.noindex/App名称.build/Debug-iphoneos/App名称.build/App名称-LinkMap-normal-arm64.txt
注意:替换其中的中文为实际的地址
通过对比两次加载顺序(上面两幅黑色背景图),可知打的二进制包对应的mach-O是按照.order中的顺序进行加载的
如果项目小,可以很轻易的找到从启动到首页加载出现之间所调用的所有方法,如果项目很大,那么这些文件的查找将是一个十分费力的事情;那么该如何查找呢?
Clang插桩
具体步骤:
1、配置
在TARGETS-->Build Settings --> Other C Flags 添加:-fsanitize-coverage=func,trace-pc-guard
如果有Swift,那么还需要在TARGETS-->Build Settings --> Other Swift Flags 添加:
-sanitize-coverage=func
-sanitize=undefined
2、在首页添加两个方法
添加方法之前需要先定义两个结构用来方便存储和读取
引入相关库:
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h> //调用动态链接库用的
#import <libkern/OSAtomic.h> //原子队列
//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
void*pc;//
void*next;
} SYNode;
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start, uint32_t*stop)
void __sanitizer_cov_trace_pc_guard(uint32_t*guard)
因为添加了Other C Flags后,会自动找这两
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);
//start:起始位置
//stop:并不是最后一个符号的地址,而是整个符号表的最后一个地址,最后一个符号的地址=stop-4(因为是从高地址往低地址读取的,且stop是一个无符号int类型,占4个字节)。stop存储的值是符号的
//函数、方法、block都能拿到
for(uint32_t*x = start; x < stop; x++)
*x = ++N;
}
/// 全面hook方法、函数、以及block调用,用于捕捉符号,是在多线程进行的,这个方法中只存储pc,以链表的形式
/// @param guard 是一个哨兵,告诉我们是第几个被调用的
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return; //这行代码会把load 方法return掉
/*
char PcDescr[1024];
printf("guard: %p %x PC %s \n"guard,*guard,PcDescr)
*/
//PC是当前函数返回到上一个调用的地址!! 参数:0代表当前函数返回到哪里 1代表上层函数返回到哪里去
void *PC = __builtin_return_address(0);
//创建结构体!
SYNode* node =malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//加入队列
//符号的访问不是通过下标访问,是通过链表的next指针,所以需要借用offsetof(结构体类型,下一个的地址即next)
OSAtomicEnqueue(&symbolList, node,offsetof(SYNode,next));//链表数据结构
}
3、获取所有符号并写入文件
while循环从队列中取出符号,处理非OC方法的前缀,存到数组中
3.1数组取反,因为入队存储的顺序是反序的
3.2数组去重,并移除本身方法的符号
3.3将数组中的符号转成字符串并写入到lvjianxiong.order文件中
- (void)getSymbolFile{
//定义数组
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {//一次循环!也会被HOOK一次!!
//解决循环办法:Other C Flags 添加 func,只有func才被hook
//取出
SYNode* node =OSAtomicDequeue(&symbolList,offsetof(SYNode, next));
if(node ==NULL) {
break;
}
Dl_info info = {0};
dladdr(node->pc, &info);//将pc赋值给info
printf("%s \n",info.dli_sname);
//重复的原因是while(YES),即:循环一次会被hook一次
NSString* name =@(info.dli_sname);
free(node);
//
BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
NSString* symbolName = isObjc ? name : [@"_"stringByAppendingString:name];
//是否去重??
[symbolNames addObject:symbolName];
}
//取出来是反的,所以需要反转数组
//反向数组
// symbolNames = (NSMutableArray*)[[symbolNames reverseObjectEnumerator] allObjects];
NSEnumerator* enumerator = [symbolNames reverseObjectEnumerator];
//创建一个新数组
NSMutableArray* funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString* name;
//去重!
while(name = [enumerator nextObject]) {
if(![funcs containsObject:name]) {//数组中不包含name
[funcs addObject:name];
}
}
//去掉自己
[funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
//数组转成字符串
NSString* funcStr = [funcs componentsJoinedByString:@"\n"];
//字符串写入文件
//文件路径
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lvjianxiong.order"];
//文件内容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
4、在首页的touchesBegan方法中调用获取符号文件
-(void)touchesBegan:(NSSet *)toucheswithEvent:(UIEvent*)event{
[self getSymbolFile];
}
此处也可以放入到其他地方,方便的地方即可,只是为了方便获取符号文件,一般来说,是第一个渲染的界面
5、拷贝文件,放入指定位置,并配置路径
一般将该文件放入主项目路径下,并在Build Settings --> Order File中配置./lvjianxiong.order
经过二进制重排,启动速度可提升15%左右
另外:Clang插桩只需要使用一次,所以获取到.order后,直接删除上面代码即可