启动优化
热启动
App已经在内存中,处在后台状态中,在次点击图标启动App
冷启动: 首次启动
dyld阶段
加载App的可执行文件,同时会递归加载所有依赖库的动态库
加载到内存之后,就通知Runtime进行下一步处理
Runtime阶段
调用map_images函数对可执行文件的内容进行解析和处理。
在load_images函数中调用call_load_methods,以调用所有Class和Category
的+load方法。
进行各种objc结构的初始化(例如objc类的注册,初始化类对象等等)
调用C++静态初始化器以及被_attribute_((constructor))修饰的函数
runtime负责将上面的内容初始化成`objc定义的结构体,然后dyld就会调用
main函数
main函数阶段
main函数
-->UIApplicationMain函数
-->didFinishLaunchingWithOptions方法
冷启动优化
pre-main 阶段
在`Edit Scheme -> Run -> Arguments ->Environment Variables`点击
+添加环境变量 `DYLD_PRINT_STATISTICS` 设为 `1`),然后运行
pre-main阶段
`dylib loading` time
(动态库耗时)
主要是加载动态库
`rebase/binding` time
(偏移修正/符号绑定耗时)
rebase(偏移修正)
因物理地址是随机配置的,实际需要:
正确的内存地址 = ASLR地址 + 偏移值
binding(绑定)
将内存中地址(真实地址)与符号进行绑定,是`dyld`做的,也称为`动态库符
号绑定`),
一句话概括:`绑定就是给符号赋值的过程`
ObjC setup` time
(OC类注册的耗时)
OC类越多,越耗时
initializer` time(执行load和构造函数的耗时)
dyld阶段优化
减少动态库,合并一些动态库,定期清理不必要的动态库
减少Objc类、Category的数量,减少Selector数量,定期清理不再需要的Class
和Category
减少C++虚函数的数量,因为虚函数会导致额外的虚表的存在
如果是Swift尽量使用struct
二进制重排
如果是swift,尽量使用`struct`
Runtime阶段优化
+initialize方法+dispatch_once的组合来取代所有的
_attribute_((constructor))、C++静态构造器、Objc的+load方法
main阶段优化
- 【第一类】初始化第三方sdk
- 【第二类】app运行环境配置
- 【第三类】自己工具类的初始化等
在不影响用户体验的前提下,尽可能讲一些操作延迟,不要全都放在
didFinishLaunchingWithOptions方法中
建议主要有以下几点:
- `减少启动初始化的流程`,能懒加载的懒加载,能延迟的延迟,能放后台初始
化的放后台,尽量不要占用主线程的启动时间
- 优化代码逻辑,`去除非必须的代码逻辑`,减少每个流程的消耗时间
- 启动阶段能`使用多线程`来初始化的,就使用多线程
- 尽量`使用纯代码`来进行UI框架的搭建,尤其是主UI框架,例如
UITabBarController。尽量避免使用Xib或者SB,相比纯代码而言,这种更耗时
- 删除废弃类、方法
启动优化:二进制重排
问:为什么要进行二进制重排?
进程访问一个虚拟内存page,而对应的物理内存不存在时,会触发`缺页中断
(Page Fault)`,因此阻塞进程。此时就需要先加载数据到物理内存,然后再
继续访问。这个对性能是有一定影响的
虚拟内存页Page Fault 是一页一页访问的,
但是启动时刻需要调用的方法,处于不同的Page导致的
iOS系统缺页时,还会对其做一次`签名验证`,增加耗时
问:什么是二进制重排?
`导致Page Fault次数过多的根本原因是启动时刻需要调用的方法,处于不同的
Page导致的`。因此,我们的优化思路就是:`将所有启动时刻需要【调用的方
法,排列在一起,即放在一个页中】,这样就从多个Page Fault变成了一个Page
Fault`。这就是二进制重排的`核心原理`
简单理解就是将启动调用的方法顺序执行
下面介绍两个名词: Link Map 和 Id
Link Map
是iOS编译过程的中间产物,记录了二进制文件的布局
在Xcode的`Build Settings`里开启`Write Link Map File` 查看生成产物:
包含三部分
Object Files: 生成二进制用到的link单元的路径和文件编号
Sections: 记录Mach-O每个Segment/section的地址范围
Symbols:按顺序记录每个符号的地址范围
ld
`ld`是Xcode使用的链接器
.order 文件
我们可以通过在`Build Settings -> Order File`配置一个后缀为order的文件路
径
order文件中,会将所需要的符号按照顺序写在里面
改变order文件方法顺序以此达到优化的目的
问: 如何获取启动运行的函数呢?
1、hook objc_msgSend`:我们知道,函数的本质是发送消息,在底层都会来
到`objc_msgSend`,但是由于objc_msgSend的参数是可变的,需要通过`汇
编`获取,对开发人员要求较高。而且也只能拿到`OC` 和 swift中`@objc` 后
的方法
2、静态扫描`:扫描 `Mach-O` 特定段和节里面所存储的符号以及函数数据
3、Clang插桩`:即批量hook,可以实现100%符号覆盖,即完全获取`swift、
OC、C、block`函数
显然 Clang 插桩 是唯一的办法
Clang 插桩
llvm内置了一个简单的代码覆盖率检测
(`SanitizerCoverage`)
【第一步:配置】
开启 `SanitizerCoverage`
OC项目
`在 Build Settings` 里的 “`Other C Flags`” 中添加 `-fsanitize-
coverage=func,trace-pc-guard`
不要配置成: `-fsanitize-coverage=trace-pc-guard` 循环场景会有问题
Swift项目
还需要额外在 “`Other Swift Flags`” 中加入`-sanitize-coverage=func` 和
`-sanitize=undefined`
或者通过`podfile`来配置
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['OTHER_CFLAGS'] = '-fsanitize-
coverage=func,trace-pc-guard'
config.build_settings['OTHER_SWIFT_FLAGS'] = '-sanitize-
coverage=func -sanitize=undefined'
end
end
end
【第二步:重写方法】
新建一个OC文件`CJLOrderFile`,重写两个方法:
代码:(可展开)
//原子队列,其目的是保证写入安全,线程安全
static OSQueueHead queue = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体,以链表的形式
typedef struct {
void *pc;
void *next;
}CJLNode;
/*
- 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;
if (start == stop || *start) return;
printf("INIT: %p - %p\n", start, stop);
for (uint32_t *x = start; x < stop; x++) {
*x = ++N;
}
}
/*
可以全面hook方法、函数、以及block调用,用于捕捉符号,是在多线程进行
的,这个方法中只存储pc,以链表的形式
- guard 是一个哨兵,告诉我们是第几个被调用的
*/
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return;//将load方法过滤掉了,所以需要注释掉
//获取PC
/*
- PC 当前函数返回上一个调用的地址
- 0 当前这个函数地址,即当前函数的返回地址
- 1 当前函数调用者的地址,即上一个函数的返回地址
*/
void *PC = __builtin_return_address(0);
//创建node,并赋值
CJLNode *node = malloc(sizeof(CJLNode));
*node = (CJLNode){PC, NULL};
//加入队列
//符号的访问不是通过下标访问,是通过链表的next指针,所以需要借用
offsetof(结构体类型,下一个的地址即next)
OSAtomicEnqueue(&queue, node, offsetof(CJLNode, next));
}
【第三步:获取所有符号并写入文件】
`while循环`从队列中取出符号,处理非OC方法的前缀,存到数组中
处理:
- 数组`取反`,因为入队存储的顺序是反序的
- 数组`去重`,并移除本身方法的符号
- 将数组中的符号转成字符串并写入到`cjl.order`文件中
代码:(可展开)
extern void getOrderFile(void(^completion)(NSString *orderFilePath)){
collectFinished = YES;
__sync_synchronize();
NSString *functionExclude = [NSString stringWithFormat:@"_%s",
__FUNCTION__];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.01 *
NSEC_PER_SEC)),
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//创建符号数组
NSMutableArray<NSString *> *symbolNames = [NSMutableArray
array];
//while循环取符号
while (YES) {
//出队
CJLNode *node = OSAtomicDequeue(&queue, offsetof(CJLNode,
next));
if (node == NULL) break;
//取出PC,存入info
Dl_info info;
dladdr(node->pc, &info);
// printf("%s \n", info.dli_sname);
if (info.dli_sname) {
//判断是不是OC方法,如果不是,需要加下划线存储,反之,则直接
存储
NSString *name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-
["];
NSString *symbolName = isObjc ? name : [@"_"
stringByAppendingString:name];
[symbolNames addObject:symbolName];
}
}
if (symbolNames.count == 0) {
if (completion) {
completion(nil);
}
return;
}
//取反(队列的存储是反序的)
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:functionExclude];
//将数组变成字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSLog(@"Order:\n%@", funcStr);
//字符串写入文件
NSString *filePath = [NSTemporaryDirectory()
stringByAppendingPathComponent:@"cjl.order"];
NSData *fileContents = [funcStr
dataUsingEncoding:NSUTF8StringEncoding];
BOOL success = [[NSFileManager defaultManager]
createFileAtPath:filePath contents:fileContents attributes:nil];
if (completion) {
completion(success ? filePath : nil);
}
});
}
【第四步:在`didFinishLaunchingWithOptions`方法最后调用】
code
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 生成order文件路径
getOrderFile(^(NSString *orderFilePath) {
NSLog(@"OrderFilePath:%@", orderFilePath);
});
return YES;
}
将获取到的order文件放到项目跟路径中
并在`Build Settings -> Order File`中配置`./xxx.order`
二进制重排-end