启动优化--二进制重排
启动优化
- 启动时间:从用户点击app图标开始到 AppDelegate 的didFinishLaunching
- 冷启动: 内存中不包含app相关数据的启动,一般我们可以通过重启手机来实现冷启动
1.热启动: 是指杀掉app进程后,数据仍然存在时的启动
- 启动优化 -> T1 + T2 需要启动优化的部分
1.T1: pre-main阶段, 即main函数之前, 操作系统加载APP可执行文件到内存,执行一系列加载&链接等工作 -> dyld加载过程.
2.main函数之后, 即从main函数开始, 到Appdelegate到didFinishLaunching方法执行完成为止, 主要是构建第一个界面,并完成渲染.
main函数之前的部分
Edit Scheme -> Run -> Arguments -> Environment Variables -> 添加'DYLD_PRINT_STATISTICS'设为1
image.png
pre-main字段说明
- dylib loading time(动态库耗时)
- rebase/binding time(偏移修正/符号绑定耗时)
- rebase(偏移修正):任何一个app生成的二进制文件,在二进制文件内部所有的方法、函数调用,都有一个地址,这个地址是在当前二进制文件中的偏移地址。一旦在运行时刻(即运行到内存中),每次系统都会随机分配一个ASLR(Address Space Layout Randomization,地址空间布局随机化)地址值(是一个安全机制,会分配一个随机的数值,插入在二进制文件的开头),例如,二进制文件中有一个 test方法,偏移值是0x0001,而随机分配的ASLR是0x1f00,如果想访问test方法,其内存地址(即真实地址)变为 ASLR+偏移值 = 运行时确定的内存地址(即0x1f00+0x0001 = 0x1f01)
- binding(绑定):,例如NSLog方法,在编译时期生成的mach-o文件中,会创建一个符号!NSLog(目前指向一个随机的地址),然后在运行时(从磁盘加载到内存中,是一个镜像文件),会将真正的地址给符号(即在内存中将地址与符号进行绑定,是dyld做的,也称为动态库符号绑定),一句话概括:绑定就是给符号赋值的过程
- ObjC setup time (OC类注册的耗时):OC类越多,越耗时
- initializer time(执行load和构造函数的耗时)
优化建议:
- 尽量少用外部动态库,苹果官方建议自定义的动态库最好不要超过6个,如果超过6个,需要合并动态库
- OC类越多越耗时
- 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数
- 如果是swift,尽量使用struct
main函数阶段的优化
didFinishLaunching方法中,主要是执行了各种业务,有很多并不是必须在这里立即执行的,这种业务我们可以采取延迟加载,防止影响启动时间。
didFinishLaunching中业务主要类型
- 【第一类】初始化第三方sdk
- 【第二类】app运行环境配置
- 【第三类】自己工具类的初始化等
main函数阶段的优化建议:
减少启动初始化的流程
,能懒加载的懒加载,能延迟的延迟,能放后台初始化的放后台,尽量不要占用主线程的启动时间- 优化代码逻辑,
去除非必须的代码逻辑
,减少每个流程的消耗时间 - 启动阶段能使用
多线程
来初始化的,就使用多线程 - 尽量使用纯代码来进行UI框架的搭建,尤其是主UI框架,例如UITabBarController。尽量避免使用Xib或者SB,相比纯代码而言,这种更耗时
- 删除废弃类、方法
二进制重排原理
原理:
当进程访问一个虚拟内存page,而对应的物理内存不存在时,会触发缺页中断(Page Fault),因此阻塞进程。此时就需要先加载数据到物理内存,然后再继续访问。这个对性能是有一定影响的。
基于Page Fault,我们思考,App在冷启动过程中,会有大量的类、分类、三方等需要加载和执行,此时的产生的Page Fault所带来的的耗时是很大的。
查看当前项目的缺页终端
- cmd + i 性能分析, 需要让子弹飞一会儿
- 选择System Trace
- 如下图配置查看缺页中断次数
image.png
从上面的Page Fault的次数以及加载顺序,可以发现其实导致Page Fault次数过多的根本原因是启动时刻需要调用的方法,处于不同的Page导致的。因此,我们的优化思路就是:将所有启动时刻需要调用的方法,排列在一起,即放在一个页中,这样就从多个Page Fault变成了一个Page Fault。这就是二进制重排的核心原理
查看文件执行顺序
1.
Build Setting -> Write Link Map File设置为YES,
如下图配置
image.png
- CMD+B编译demo,然后在对应的路径下查找 link map文件,如下所示,可以发现 类中函数的加载顺序是从上到下的,而文件的顺序是根据Build Phases -> Compile Sources中的顺序加载的
- Link Map是iOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File
1.Object Files 生成二进制用到的link单元的路径和文件编号
2.Sections 记录Mach-O每个Segment/section的地址范围
3.Symbols 按顺序记录每个符号的地址范围
ld
ld是Xcode使用的链接器,有一个参数order_file,我们可以通过在Build Settings -> Order File配置一个后缀为order的文件路径。在这个order文件中,将所需要的符号按照顺序写在里面,在项目编译时,会按照这个文件的顺序进行加载,以此来达到我们的优化 -> 二进制重排的本质就是对启动加载的符号进行重新排列.
获取启动运行的函数呢
- hook objc_msgSend:我们知道,函数的本质是发送消息,在底层都会来到objc_msgSend,但是由于objc_msgSend的参数是可变的,需要通过汇编获取,对开发人员要求较高。而且也只能拿到OC 和 swift中@objc 后的方法
- 静态扫描:扫描 Mach-O 特定段和节里面所存储的符号以及函数数据
- Clang插桩:即批量hook,可以实现100%符号覆盖,即完全获取swift、OC、C、block函
Clang 插桩
llvm内置了一个简单的代码覆盖率检测(SanitizerCoverage)。它在函数级、基本块级和边缘级插入对用户定义函数的调用。我们这里的批量hook,就需要借助于SanitizerCoverage。
- 使用AppOrderFiles
- 在 Build Settings 里的 “Other C Flags” 中添加
-fsanitize-coverage=func,trace-pc-guard
1.如果是Swift项目,还需要额外在 “Other Swift Flags” 中加入-sanitize-coverage=func 和 -sanitize=undefined
//当然通过pod导入的, 可以在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
源码解析
//原子队列,其目的是保证写入安全,线程安全 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)); }
获取所有符号并写入文件
注意:只要是汇编中的跳转都会被hook, 既有b,bl的指令都会被hook
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); } }); }
配置order文件
通过上面的方法生成order文件后, 配置
-> Build Setting -> Order File -> ./xxx.order