App的启动通常是指从用户点击App开始到AppDelegate
的didFinishLaunching
方法执行完成为止,通常又将启动分为冷启动和热启动。html
上文也说了通常启动优化主要优化的是冷启动的过程,热启动作的事情也很是少。因此这里只讲解冷启动过程的优化。冷启动过程又被分为main
函数执行以前和main
函数执行以后node
main
函数执行以前DYLD_PRINT_STATISTICS
来查看main
函数执行以前都作了什么,同时也能够看出对应消耗的时间 main
函数执行以前主要作了如下几种事情
dylib loading time
能够发现加载时间为48.41毫秒rebase/binding time
,耗时9.18毫秒
ASLR
安全机制下文中会有讲解),因此此时函数、方法的地址就是 随机分配的数值+偏移地址 这个过程就是偏移修正mach-o
文件,因此此时使用到的静态库的方法、函数其实就和自定义的方法、函数差很少了,可以直接获取到对应的地址,可是动态库在编译阶段是不会被打包进mach-o
文件的,可是此时又用到了动态库中的方法,例如用到了NSLog
方法,此时就会生成一个!NSLog
符号此时这个符号会随机指向一个地址,当运行时,此时动态库被加载到内存,此时就能够拿到动态库对应的方法、函数的地址,因此此时就须要将!NSLog
这个符号绑定到相应的地址上去(dyld
作的),这个过程就叫作符号绑定ObjC setup time
,耗时10.86毫秒initializer time
,耗时110.79毫秒load
方法相应的能够将load
中的实现放在+initialize()
方法中去,应为通常一个load
方法的执行须要耗时4毫秒,并且若是类中实现了load
那么相对应类的加载就要提早到read_image
方法中去执行,若是没有实现load
类的加载则会方法第一次发送消息的时候加载,main
函数执行以后上文主要是针对特定的阶段作一些优化处理,除了删除的优化方案还有一种优化,就是二进制重排,在讲解二进制重排以前先将几个概念性的东西:ios
从上文的知识中能够知道,ios程序在加载到虚拟内存的时候会被分红不少不少页,若是此时访问的虚拟地址的一个page,对应的物理地址不存在,则会缺页异常,此时会阻塞进程将这一页加载到物理内存而后在访问。这里能够经过instruments
的System Trace
来查看你的项目的缺页异常的数量以下: 步骤:先点击启动->首页加载完成后暂停->而后找到你的项目找到主线程
发现启动以前有两百多个缺页异常,此时咱们再看项目在编译时期的默认排列顺序,此时咱们写一个简单的demo以下图:
就是写了几个简单的方法,而后项目中选择
Build-setting
搜索link map
而后配置此时会发现对应配置的文件夹中生成了对应的
link-map
文件,发现方法、函数等都是按照在文件中的实现顺序来的,而文件的顺序是按照
comple source
中的顺序来的如图: 这种状况就形成了每一个页有可能只有一个方法是有用的,其余方法、函数等都不是在启动阶段调用的,这就形成了在启动时期缺页异常的数量会不少,也就形成了启动时间变长的状况。这也就是须要进行二进制重排的缘由算法
上文分析了二进制重排的缘由,就是应为页中空间的浪费没有充分利用每一个页的空间形成缺页异常数量增多,二进制重排的原理其实就是将启动阶段用到的方法、函数所有排在最前面,这样就能充分利用每一个页的空间,与此同时也下降了缺页异常的数量。以下图所示:
明显减小了一大半的缺页异常的数量swift
经过上面的原理分析能够知道,若是作二进制重排只须要改变编译时期方法、函数等的排列顺序就行。其本质就是就是对启动加载的符号进行从新排列。数组
Xcode
是用的连接器叫作ld
,ld
有一个参数叫Order File
, 咱们能够经过这个参数配置一个 order
文件的路径 .Build Settings
-> Order File
配置一个后缀为order
的文件路径。在这个order
文件中,将所须要的符号按照顺序写在里面,在项目编译时,会按照这个文件的顺序进行加载,以此来达到咱们的优化,因此二进制重排的关键点在于Order File
文件的生成Order File
文件的方法
Order File
文件。hook
objc_msgSend
,可是因为objc_msgSend
的参数是可变的,须要经过汇编获取,使用门槛比较高。并且也只能拿到OC和swift中@objc
后的方法Mach-O
特定段和节里面所存储的符号以及函数数据Clang
插桩:即批量hook,能够实现100%符号覆盖,即彻底获取swift、OC、C、block
函数Clang
插桩llvm
内置了一个简单的代码覆盖率检测(SanitizerCoverage
)。它在函数级、基本块级和边缘级插入对用户定义函数的调用,相应文档SanitizerCoverage
,在build setting
中搜索Other C Flags
,以下图-fsanitize-coverage=func,trace-pc-guard
,若是是swift项目则添加-sanitize-coverage=func
和-sanitize=undefined
hook
方法 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) {
//guard 是一个哨兵,告诉咱们是第几个被调用的
// 这个地方 是过滤掉了load方法,因此这里须要注释掉
if (!*guard) return;
/*
- PC 当前函数返回上一个调用的地址
- 0 当前这个函数地址,即当前函数的返回地址
- 1 当前函数调用者的地址,即上一个函数的返回地址
*/
void *PC = __builtin_return_address(0);
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
复制代码
主要的方法在于__sanitizer_cov_trace_pc_guard
方法,在这里咱们能够取到对应方法的地址,为何方法执行以前会先调用__sanitizer_cov_trace_pc_guard
方法呢,可经过断点调试查看,在一个方法或者函数的起始处大断点,再看汇编代码以下图: __sanitizer_cov_trace_pc_guard
方法,全部的函数执行都会限制性__sanitizer_cov_trace_pc_guard
方法,在block前面也打个断点发现 block
执行前也会被插入__sanitizer_cov_trace_pc_guard
方法,继续查看swift-oc
混编是swift
方法是否会被hook
hook
,因此也验证了clang插桩的方法能覆盖全部方法、函数。hook
方法中咱们知道能够拿到当前方法或者函数的地址,拿到地址以后咱们能够经过dladdr
方法去除对应方法或者函数的信息具体代码以下图: dli_sname
就是咱们想要的符号,接下来的操做主要就是把这些符号存储下来而后生成order而后工程再配置对应的Order file
就算完成了。__sanitizer_cov_trace_pc_guard
将函数地址信息存储下来而后给app
添加一个点击屏幕的监听事件,等到首屏加载完毕说明启动完成全部所须要加载的方法也就加载完成,此时咱们再在这个方法遍历地址信息,输出符号。OSQueueHead
建立原子队列,其目的是保证读写安全。OSAtomicEnqueue
方法将node
入队,经过链表的next
指针能够访问下一个符号 -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//定义数组
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:@"-["];
//须要注意若是不是OC方法须要添加下划线
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
//反向数组
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:@"tudou.order"];
//文件内容
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}
复制代码
运行完成发现生成了order文件