热启动与冷启动
-
冷启动
:App
点击启动前,此时App的进程
还不在系统
里,内存
中不包含app相关数据
,需要系统新创建
一个进程
分配给App
。 -
热启动
:App
在冷启动后
用户将App退回后台
,此时App的进程
还在系统里,数据
仍然存在,用户重新返回App
的过程。
APP冷启动完整流程
冷启动
的整个过程是指从用户唤起 App
开始到 AppDelegate
中的 didFinishLaunchingWithOptions
方法执行完毕为止,并以执行main()
函数的时机为分界点
,分为pre-main
和 main()
两个阶段。
pre-main阶段
pre-main阶段
,即main函数
之前,操作系统加载App可执行文件
到内存,执行一系列的加载&链接
等工作,简单来说,就是dyld加载
过程
-
dylib loading time
(动态库耗时):主要是加载动态库 -
rebase/binding time
(偏移修正/符号绑定耗时):进行rebase
指针调整和bind
符号绑定-
rebase
:任何一个app生成的二进制文件
,所有的方法和函数调用
都对应一个地址,这个地址就是当前二进制文件中的偏移地址
,为了安全,通常会随机分配一个ASLR随机值
,只有ASLR+偏移地址
才是方法或函数对内存的真实地址
-
binding
:将编译期
创建的符号
在运行时
与地址
进行绑定
,一般是dyld执行
,也可以叫动态库符号绑定
*ObjC setup time
(OC类注册的耗时):ObjC相关Class
、category
注册、selector
唯一性检查等,OC
类越多,时间越长
*initializer time
(执行load和构造函数的耗时):+load()
方法、attribute
修饰的函数调用、创建C++
静态全局变量等
-
pre-main优化方案
- 减少外部动态库加载,官方建议自定义的动态库
不要超过6个
,如果超过则需要合并动态库
- 减少OC类
- 将不必须在
+load
方法中做的事情延迟到+initialize
中,尽量不要用C++
虚函数
main函数
main函数
执行后的阶段,指的是:从 main
函数执行开始,到appDelegate
的 didFinishLaunchingWithOptions
方法里首屏渲染相关方法执行完成。
即,从main
函数执行到设置self.window.rootViewController
执行完成的阶段。
在didFinishLaunching中
的业务主要分为三个类型
- 【第一类】初始化第三方sdk
- 【第二类】app运行环境配置
- 【第三类】自己工具类的初始化等
main函数阶段优化方案
- 尽量使用纯代码来进行UI框架的搭建,尤其是主UI框架,例如UITabBarController。尽量避免使用Xib或者SB,相比纯代码而言,这种更耗时
- 将耗时操作滞后或异步处理。
通常的耗时操作有:网络加载、编辑、存储图片和文件等资源,不要占用主线程时间 - main函数执行后到首屏渲染完成前,只处理首屏渲染相关业务。
首屏渲染外的其他功能放到首屏渲染完成后去初始化 - 减少启动初始化的流程,能懒加载的懒加载,能延迟的延迟,能放后台初始化的放后台,尽量不要占用主线程的启动时间
- 启动阶段能使用多线程来初始化的,就使用多线程
- 删除废弃类、方法
二进制重排
对用户而言,使用App
时第一个直接体验就是启动 App 时间
,而启动时期会有大量的类、分类、三方
等等需要加载和执行
,此时多个 Page Fault
所产生的的耗时往往是不能小觑的,下面我们就通过二进制重排
来优化启动耗时。
虚拟内存
在
进程
和物理内存
之间增加一个中间层
,这个中间层就是所谓的虚拟内存
,主要用于解决当多个进程
同时存在时,对物理内存的管理。提高了CPU的利用率,使多个进程可以同时、按需加载
。所以虚拟内存其本质就是一张虚拟地址和物理地址对应关系的映射表
每个进程
都有一个独立的虚拟内存
,其地址都是从0开始
,大小是4G固定
的,每个虚拟内存又会划分为一个一个的页(页的大小在iOS中是16K
,其他的是4K
),每次加载都是以页为单位
加载的,进程间是无法互相访问
的,保证了进程间数据的安全性
。一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的部分放入物理内存,避免物理内存的浪费
当CPU需要访问数据时,首先是访问虚拟内存,然后通过虚拟内存去寻址,即可以理解为在表中找对应的物理地址,然后对相应的物理地址进行访问
如果在访问时,虚拟地址的内容
未加载到物理内存
,会发生缺页异常(pagefault)
,将当前进程阻塞
掉,此时需要先将数据载入到物理内存
,然后再寻址,进行读取。这样就避免了内存浪费
page Fault 调试
打开Instruments
,选择 System Trace
选择真机,选择工程,选择启动(启动前最好重启手机,清除缓存数据,确保是冷启动状态),当页面加载出来的时候,停止
二进制重排实践
二进制重排就是对即将生成的可执行文件重新排列,即它发生在链接阶段。
首先理解几个名词
- Link Map:
Linkmap
是iOS编译过程
的中间产物,记录了二进制文件的布局
,需要在Xcode的Build Settings
里开启Write Link Map File
,Link Map
主要包含三部分:
*Object Files
生成二进制用到的link单元
的路径和文件编号
-
Sections
记录Mach-O
每个Segment/section
的地址范围
-
Symbols
按顺序记录每个符号的地址范围
-
- ld :
ld
有一个参数叫Order File
,通过Build Settings -> Order File
配置一个 后缀名 为order
的文件路径。在这个order
文件中,将你需要的符号按顺序写在里面。当工程build 的时候
,Xcode
会读取这个文件,打的二进制包就会按照这个文件中的符号顺序进行生成对应的mach-O
。
如何获取启动运行的函数呢
hook objc_msgSend
:我们知道,函数的本质是发送消息,在底层都会来到objc_msgSend
,但是由于objc_msgSend
的参数是可变
的,需要通过汇编获取,对开发人员要求较高。而且也只能拿到OC 和 swift中@objc
后的方法静态扫描:扫描
Mach-O
特定段和节里面所存储的符号以及函数数据
Clang插桩
:即批量hook
,可以实现100%符号
覆盖,即完全获取swift、OC、C、block
函数
Clang插桩
llvm
内置了一个简单的代码覆盖率检测(SanitizerCoverage)
。它在函数级、基本块级和边缘级
插入对用户定义函数的调用
。我们这里的批量hook
,就需要借助于SanitizerCoverage
。流程如下:
- 开启
SanitizerCoverage
-
OC
项目,需要在:在Build Settings
里的“Other C Flags”
中添加 -fsanitize-coverage=func,trace-pc-guard
- 如果是
Swift
项目,还需要额外在“Other Swift Flags”
中加入-sanitize-coverage=func
和-sanitize=undefined
-
- 重写方法
-
__sanitizer_cov_trace_pc_guard_init
方法- 参数1
start
是一个指针,指向无符号int类型
,4个字节
,相当于一个数组的起始位置
,即符号的起始位置(是从高位往低位读) - 参数2
stop
,由于数据的地址是往下
读的(即从高往低读
,所以此时获取的地址并不是stop真正的地址,而是标记的最后的地址,读取stop
时,由于stop占4个字节
,stop
真实地址 =stop打印的地址-0x4)
*__sanitizer_cov_trace_pc_guard
方法
- 参数1
-
#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;
/*
- start:起始位置
- stop:并不是最后一个符号的地址,而是整个符号表的最后一个地址,最后一个符号的地址=stop-4(因为是从高地址往低地址读取的,且stop是一个无符号int类型,占4个字节)。stop存储的值是符号的
*/
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.
}
/*
可以全面hook方法、函数、以及block调用,用于捕捉符号,是在多线程进行的,这个方法中只存储pc,以链表的形式
- guard 是一个哨兵,告诉我们是第几个被调用的
*/
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check.
//获取PC
/*
- PC 当前函数返回上一个调用的地址
- 0 当前这个函数地址,即当前函数的返回地址
- 1 当前函数调用者的地址,即上一个函数的返回地址
*/
void *PC = __builtin_return_address(0);
char PcDescr[1024];
//__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
//创建结构体!
SYNode * node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//加入结构!
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
- 获取所有符号并写入文件
{
//定义数组
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (YES) {//一次循环!也会被HOOK一次!!
SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info = {0};
dladdr(node->pc, &info);
// printf("%s \n",info.dli_sname);
NSString * name = @(info.dli_sname);
free(node);
BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
//是否去重??
[symbolNames addObject:symbolName];
/*
if ([name hasPrefix:@"+["]||[name hasPrefix:@"-["]) {
//如果是OC方法名称直接存!
[symbolNames addObject:name];
continue;
}
//如果不是OC直接加个_存!
[symbolNames addObject:[@"_" stringByAppendingString:name]];
*/
}
//反向数组
// symbolNames = (NSMutableArray<NSString *>*)[[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:@"hank.order"];
//文件内容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
拷贝文件,放入指定位置,并配置路径
一般将该文件放入主项目路径下,并在Build Settings -> Order File
中配置./hank.order
注:
Build Settings -> Other C Flags
的如果配置的是-fsanitize-coverage=trace-pc-guard
,没有func
,在while循环部分
会出现死循环
,原因是while循环也会被__sanitizer_cov_trace_pc_guard
中的guard哨兵
检测到,通过汇编可以查看到流程-
第一次是
bl
是touchBegin
-
第二次
bl
是因为while 循环
。 即只要是跳转
,就会被hook
,即有bl、b
的指令,就会被hook