启动优化 热启动 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