冷启动和热启动
当用户按下home键时,iOS的APP不会立马被kill掉,还会存活一段时间,在这个时间之内,用户再进入APP,APP几乎不需要做什么,就可以还原到退出时的状态。这个称之为热启动。
与之对应的冷启动,就是内存中不包含APP的相关数据,必须从磁盘中载入到内存。重启手机后,就是绝对的冷启动。
APP的启动优化主要分为两个阶段:
-
pre-main(main()函数之前) main()函数之后
pre-main
苹果提供了一个方法,可以监测main()函数之前的耗时。
在Xcode中,Project->Scheme->Edit Scheme -> Run -> Arguments -> Environment Variables,添加DYLD_PRINT_STATISTICS。

command+R运行,控制台就会多出这样一份信息。

这里用的是我之前一个没有优化过的项目,选用的是release环境,我们可以看到pre-main time非常久,总共达到了933.64ms。
-
dylib loading time:动态库的载入 -
rebase/binding time:偏移修正和符号绑定 -
ObjC setup time:OC类注册耗时,swif类要比OC快 -
initializer time:执行+load和构造函数的耗时 -
slowest intializers:最慢的,举了一些例子 -
libglInterpose.dylib:系统库,和调试相关 - 最后一个是咱们的主程序耗时
偏移修正:在二进制文件中,方法、函数调用都有一个地址,这个地址是在这个二进制文件当中的偏移地址。但是我们的程序在
运行中,系统会给我们分配一个ASLR随机地址值,插入到二进制文件的开头,函数方法的地址就发生了变化。
符号绑定:比如我们的NSLog,是foundation框架中的方法,编译的时候并不知道它的真实地址,真实地址是在运行时绑定的,这就是符号绑定。
与之对应的优化方法有这些:
1. 减少动态库的使用
官方建议自定义动态库不要多于6个,但实际使用6个可能远远不够,这时候就要考虑动态库的合并了。
2. 减少不必要的OC类,尽量不使用+load方法
减少加载后可能不会使用的类或者方法,尽量不要写+load()方法,可以用+initialize()、dispatch_once()替换+load()
3. 减少C++构造函数和静态对象
尽量不要写 __attribute__((constructor))的C函数,也尽量不要用到C++的静态对象
main函数之后
main函数之后到第一个页面渲染完成,这之中尽量不要做耗时的操作,如果一定要做,那么请使用多线程去完成。
启动时需要加载展示的页面,最好不要用storyboard、xib,因为storyboard要经过一层代码解析和转换,再渲染。
二进制重排
由于现在系统采用的都是虚拟地址加物理地址,启动时大量的page fault也会耗时。通过二进制重排,把启动时需要访问的内存,集中在一个page里面,减少page fault的次数,这就是二进制重排的核心原理。
检测工具
page fault的次数可以通过Instruments -> System Trace查看

-
选择我们要监测的应用运行
-
首屏渲染完成停止
搜索main thread,选择Summary:Virtual Memory查看虚拟内存情况
我们可以看到page fault的次数是1022次,总共耗时278.38ms,最长的一次是4.83ms。
二进制重排实现
二进制文件的顺序是根据Build Phases -> Compile Sources里的顺序来的,每个文件里面按照从上到下的顺序。

-
link map
通过link map可以查看方法写入二进制的顺序。
搜索link map,将Write Link Map File修改为YES,编译一次就可以在上面的path路径中拿到link map文件。 -
通过配置
.order文件,可以改变文件的链接顺序,可以在Xcode中配置
下面是objc源码中的.order文件,可以参考这个来写。
如果orde_file中的符号实际不存在会怎么样?
Id会忽略这些符号,如果提供了link选项-order_file_statistics,会以warning的形式把这些没找到的符号打印在日志里。
前面的准备工作做好了,下面最重要的是如何获取启动时加载的方法?
要获取APP启动时加载的方法,首先想到的是hook掉objc_msgSend,使用fishhook,但由于objc_msgSend是可变参数,需要使用汇编,而且不能覆盖所有符号。下面提供一个clang插桩方式,hook一切符号的方式。
clang插桩
-
在Xcode中添加
-fsanitize-coverage=trace-pc-guard
-
编译会报两个错误
说明只要我们添加了-fsanitize-coverage=trace-pc-guard,他就会自动调用___sanitizer_cov_trace_pc_guard_init和___sanitizer_cov_trace_pc_guard这两个函数
官方文档给出了这两个函数的例子:

extern "C" 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.
}
这里面的start为0,stop为启动时函数调用结束的数目。通过stop - start可以得出方法函数的调用次数。
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return; // Duplicate the guard check. 屏蔽了load方法
void *PC = __builtin_return_address(0);//当前函数返回到上一个调用的地址 0 表示当前函数会回到哪里 1 表示上一个函数会回到哪里
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
这个函数,在每一次调用方法时都会进来,guard里包含了一些方法地址和信息。

只要通过
trace_pc_guard就可以拿到APP启动时调用的所有方法,现在只要我们通过guard信息拿到方法的reciver和SEL,就可以进行order_file的编写。
- 打上图示断点
这时候进来的是main函数,查看左边堆栈可知
打印发现PC的地址和main函数的地址一样
PC是函数返回地址,当调用完trace_pc_guard,会继续执行原函数,PC只是返回原函数地址,并不是原函数的起始地址。
这里我们需要借助dlopen动态链接库
#import <dlfcn.h>
其中有一个Dl_info结构体
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
在trace_pc_guard加上以下几行代码

断点运行

可以看到main函数的名字输出了。
-
fname:文件地址 -
fbase:文件地址 -
sname:函数名称 -
saddr:函数起始地址
这样我们通过dladdr()函数就拿到了我们想要的内容。
符号的存储
- 自定义一个原子队列,将数据存入队列
//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
- 自定义一个结构体
SYNode用来存储PC数据
//定义符号结构体
typedef struct {
void *pc;
void *next;
}SYNode;
队列是一个单链表,next用来指向下一个元素位置
- 自定义一个获取队列中数据的函数
readSymbolList()
void readSymbolList() {
while (YES) {
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
printf("%s \n",info.dli_sname);
}
}
我们在App首屏界面中的touchesBegan调用它,读取队列中的数据。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
readSymbolList();
}
-
运行程序,查看
readSymbolList()打印结果
发现程序进入可死循环,究其原因是因为while循环也会被hook掉,每进行一次循环就会调用一次trace_pc_guard -
进入循环后打上断点
-
查看汇编
此时的地址是0x104a24808 -
进入
trace_pc_guard打上断点
-
断点进来后,查看PC
返回地址为0x0000000104a24870
-
回到
readSymbolList()的汇编
找到0x104a24870这个地址看到是一个跳转,跳转到0x104a24838这个地址 -
查看
0x104a24838
这正是第一个断点的位置所在,如此就陷入了死循环,程序在readSymbolList()和trace_pc_guard中一直循环跳转。
死循环解决方法
clang插桩的时候,设置让循环不会调用trace_pc_guard方法

在上面的value中加入
func,这样就只会hook方法。
-
再次运行
程序启动时调用的方法成功打印。注意这里是一个倒序的,因为是单链表。
上述的打印还存在一下几个问题:
1. 顺序是颠倒的
2. 没有+load方法
3. 方法重复
4. 如果是函数,需要在前面加_
-
解决
load方法
去除这一行代码 处理并写入
order
void writeOrderFile() {
//定义数组
NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
while (YES) {
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSString *name = @(info.dli_sname);
//如果是OC方法 直接存
if ([name hasPrefix:@"+["] || [name hasPrefix:@"-["]) {
[symbolNames addObject:name];
continue;
}
//如果不是
[symbolNames addObject:[@"_" stringByAppendingString:name]];
}
//取反 去重
NSEnumerator *enumerator = [symbolNames reverseObjectEnumerator];
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//去除本身这个函数
[funcs removeObject:[NSString stringWithFormat:@"_%s",__func__]];
//写入order_file
//组装成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
//文件路劲
NSString *filePath = [NSString stringWithFormat:@"%@%@",NSTemporaryDirectory(),@"example.order"];
//文件内容
NSData *fileData = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
//写入
BOOL isSuccess = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
if (isSuccess) {
NSLog(@"order_file 写入成功");
}else{
NSLog(@"order_file 写入失败");
}
}
-
拿到
order文件,配置到Xcode的order file路径
-
再次运行App,查看启动耗时
优化后
优化前
可以看到差不多pre-main时间减少了200ms。


page fault数目从1000多减到了54。
- 完整代码
#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) {
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) {
// if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
//创建结构体
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,NULL};
//加入结构体
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
void writeOrderFile() {
//定义数组
NSMutableArray<NSString *> *symbolNames = [NSMutableArray array];
while (YES) {
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
if (node == NULL) {
break;
}
Dl_info info;
dladdr(node->pc, &info);
NSString *name = @(info.dli_sname);
free(node);
//如果是OC方法 直接存
if ([name hasPrefix:@"+["] || [name hasPrefix:@"-["] || [name hasPrefix:@"_"]) {
[symbolNames addObject:name];
continue;
}
//如果不是
[symbolNames addObject:[@"_" stringByAppendingString:name]];
}
//取反 去重
NSEnumerator *enumerator = [symbolNames reverseObjectEnumerator];
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
while (name = [enumerator nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//去除本身这个函数
[funcs removeObject:[NSString stringWithFormat:@"_%s",__func__]];
//写入order_file
//组装成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
//文件路劲
NSString *filePath = [NSString stringWithFormat:@"%@%@",NSTemporaryDirectory(),@"example.order"];
//文件内容
NSData *fileData = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
//写入
BOOL isSuccess = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileData attributes:nil];
if (isSuccess) {
NSLog(@"order_file 写入成功");
}else{
NSLog(@"order_file 写入失败");
}
}
Swift
需要在Other Swift Flags配置一下参数




















