iOS-App启动优化


手动目录
  • Main之前
  • Main 之后
  • 二进制重排
    系统默认加载方式
    1)、查看PageFault 次数
    2)、查看系统默认的链接符号
    指定加载方式
随着App的不管更新、功能增加等,app工程会原来越大,需要在启动就加载的功能模块也可能越来越多,这个是很耗时的,App的启动时间过久,多用户是很不友好的。所以App的启动优化是一个非常有必要的。
App的启动分为:热启动、冷启动
一般我们说的启动优化 是指冷启动
启动优化,从2方面入手:Main函数之前、Main函数之后。
Main函数之前 主要是由系统决定。
Main函数之后:由用户的加在内容决定。
Main之前 如何检测Main函数之前的启动时间?
添加一个环境变量:DYLD_PRINT_STATISTICS

iOS-App启动优化
文章图片
添加环境变量
重新启动 打印出Main启动时间
Total pre-main time: 704.32 milliseconds (100.0%)// 总共启动时间 dylib loading time: 174.75 milliseconds (24.8%)// 动态库加载时间 rebase/binding time:36.61 milliseconds (5.1%)// 修复内部指针地址(ASLR 随机偏移值)/外部符号绑定(DYLD去做的) ObjC setup time: 170.39 milliseconds (24.1%)// OC类注册的耗时时间 initializer time: 322.29 milliseconds (45.7%)// load 的时间 slowest intializers ://启动最耗时的内容 libSystem.B.dylib :10.14 milliseconds (1.4%)// 系统库 libMainThreadChecker.dylib :85.19 milliseconds (12.0%)//系统库 (App名称) : 411.80 milliseconds (58.4%)// 主程序

从上面的耗时来分析
在Main函数之前,有哪些是可以 做 优化的:
  • 1、库的加载
    系统库经过优化处理的,本身加载就 很快。
    自己倒入的库: 苹果给出的建议是不超过6个。如果超过6个,可以采用合并的方式。
  • 2、减少不必要的类 、资源图片等。
    比如随着版本更新迭代,有些类、图片等被弃用了的。
    可以使用工具检测没有用到的类
    这个操作相对来说优化的成效不高。有人说 减少2w个类,启动时间只少了800 毫秒。
  • 3、能不在Load里面做的操作,就不要在Load操作。
  • 4、二进制重排
    这个主要是正对binding阶段的操作。
Main之后 Main 之后的时间 自己用计时器打印。推荐一个工具:BLStopwatch打点计时器。 里面也链接了一篇作者自己的关于Main之后启动优化的文章 一次立竿见影的启动时间优化。
// 打点计时器用法
//在didFinishLaunchingWithOptions 里面加入计时操作 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [[BLStopwatch sharedStopwatch] start]; //一些列操作 [[BLStopwatch sharedStopwatch] splitWithDescription:@"didFinishLaunchingWithOptions"]; return YES; }// 在其他启动阶段操作的地方 [[BLStopwatch sharedStopwatch] refreshMedianTime]; //一些列操作 [[BLStopwatch sharedStopwatch] splitWithDescription:@"这是进行了某一个操作"]; // 显示第一个界面的地方进行操作 [[BLStopwatch sharedStopwatch] refreshMedianTime]; //一些列操作 [[BLStopwatch sharedStopwatch] splitWithDescription:@"第一个界面显示耗时"]; 、 [[BLStopwatch sharedStopwatch] stopAndPresentResultsThenReset ]; // 停止计时并打印所有的计时。

根据打印的时间,可以做相应的优化
Main 之后的优化
1、能懒加载的就懒加载
2、发货CPU的性能(多线程初始化)
3、启动阶段的尽量不要用Xib、stroyboard。 Xib、storyboard都是需要进行xml解析,相对纯代码来讲,是比较耗时的。
网上关于Main之后的优化内容比较多。包括上面提到的 打点计时器作者的那片文章一次立竿见影的启动时间优化。
二进制重排 这篇文章主要讲Main之前的启动优化。 二进制重排
在19年 抖音团队的一篇关于二进制重排火了。我们这篇文章主要讲如何操作
关于二进制重排,需要先了解虚拟内存物理内存
早期的计算机都是使用物理内存来处理:一次全部把App加载进入内存。这也就是出现了当内存满了之后,在打开一个应用,就会报错。而且也不安全(因为 全部都加载进入内存,而且是连续的,当拿到内存地址开始的位置,其他的内存信息,都可以通过内存偏移来拿到)。
虚拟内存就是为了解决这些问题的,先加载必须的信息,其他的信息当你用到的时候再在物理内存上去分配。
虚拟地址于物理地址 中间通过一张映射表(页表)进行管理。由硬件mmu(CPU里的一个单元)来管理。
我们在Xcode里面打印出来的地址 都是虚拟地址,这个时候的地址是连续的。但是实际地址需要通过映射表(页表)去寻址。 在物理地址上可能是不连续的
物理内存是分页的(在iOS设备 一页16K,Mac 一页4K)。当App加载的时候,会将虚拟内存映射到物理内存,这个时候,物理内存大概是这样的

iOS-App启动优化
文章图片
物理内存分页 App先加载部分进入内存,当App使用某一功能的时候,发现在物理内存里面没有,这个时候会发生
缺页中断(PageFault) ---- 这个时候会先阻塞进程,先将虚拟内存加入物理内存中去。
加载的原则就是哪里有空的,加载哪里,没有空的,覆盖掉其他的。(这也是为什么当我们App开的比较多的时候,最开始打开的App会被重新启动加载)。
还有一个问题:当App编译好之后,他的虚拟内存的地址就固定了,这样会很容易黑客攻击,为了解决这个问题 ,就出现了ASLR - 地址空间布局随机化(就是在每个内存前面 加一个随机偏移值)。
因为内存加载是进行分页加载。那么我要先加载那些,后加载那些 。能不能自己进行指定?答案是可以的。Xcode支持指定符号进行加载。
系统默认加载方式 查看PageFault 次数 我们借助 系统自带调试工具:Xcode -> Instruments -> System Trace

iOS-App启动优化
文章图片
System Trace 步骤一: 将应用安装到手机上
步骤二:打开System Trace 清空筛选条件,输入 Main Thread (下图一)
步骤三:点击start ,等状态变成黑色方形图标 点击停止 (下图二)
步骤四:选中Main Thread 选择主线程 - > Summary : Virtual Memory(下图三)
查看缺页中断次数:

iOS-App启动优化
文章图片
图一 iOS-App启动优化
文章图片
图二 iOS-App启动优化
文章图片
图三 这样就看到了缺页中断的次数(File Backed Page In):这里是2747次。总耗时:814ms。
(注意:如果不是第一次启动。尽量多的点开其他应用,把物理内存中的page 尽量清空)。
后台退出App,在进行一次 操作,这个时候,File Backed page In 可能就很小(几十----一百多)。
查看系统默认的链接符号 直接在Xcode 设置改。

iOS-App启动优化
文章图片
修改Link map 编译之后找到 编译好的.app 工程 -> Products —> xxx.app Show In Finder

iOS-App启动优化
文章图片
找到相应目录
按照这个目录去找
Intermediates.noindex -> 工程名.build -> Debug-iphoneos(跑的机器不同,这个路径也不同) -> 工程名.bulid —> 工程名-LinkMap-normal-arm64.txt
打开这个文件 往下翻 找到这样的地方# Symbols:
# Symbols: # AddressSizeFileName 0x100007F1C 0x000001F0[1] -[OneClass mj_newValueFromOldValue:property:] 0x10000810C 0x000000CC[1] +[OneClass mj_objectClassInArray] 0x1000081D8 0x0000002C[1] -[OneClass buyPrice] 0x100008204 0x00000034[1] -[OneClass setBuyPrice:] ........0x100008238 0x0000002C[2] -[TwoClass deliveryType] 0x100008264 0x00000048[2] -[TwoClass setDeliveryType:] 0x1000082AC 0x00000030[2] -[TwoClass isHot] .......

我们发现 系统默认是按照 Bulid Phases -> Compile Source 里面的类的顺序去排列符号的。
但是一般来说,我们启动的时候,需要的类并不完全都在前面,这样就导致不必要的 缺页中断的发生。
指定加载方式 指定加载符号order文件 其实在下载系统的源码中,就有这样的配置。只不过之前没太注意。
打开下载的源码,在目录下就可以看到一个 libobjc.order文件

iOS-App启动优化
文章图片
源码里的order文件
打开这个文件,里面就是指定的加载内容。
那么我们自己如何使用这个功能
【iOS-App启动优化】指定符号顺序
1、 创建一个 .order 文件 (filename.order) 并放入工程目录下
2、在Build Settings 下搜索 order File (下图一) ,输入 路径 ./filename.order (因为我放在了根目录 下)
这样 Xcode就会按照我们指定的符号去加载到内存。
iOS-App启动优化
文章图片
添加order支持 获取启动需要的符号 说了那么多,我知道怎么用了,但是我要如何获取启动时,需要重排的符号?
我们使用Clang 插桩的方式 【官方网站】,它可以捕获所有的方法、block、函数的调用。
他的作用就是:在编译的时候 ,在每个函数、方法、block 调用的时候,插入一个 __sanitizer_cov_trace_pc_guard
类似这样
- (void)callTask { __sanitizer_cov_trace_pc_guard() ; // 插入这行代码 //你要操作的任务 }

获取符号列表步骤
  • 步骤一:With -fsanitize-coverage=trace-pc-guard the compiler will insert the following code on every edge。给compiler添加一个参数 (下图一)
  • 步骤二:在合适的位置写入它指定的函数。
  • 步骤三:根据打印的出来的地址,根据方法地址,找到方法符号。
  • 步骤四:保存找到的符号。
    因为编译插入的函数可能是在子线程,所以不能直接用数组来保存。
iOS-App启动优化
文章图片
图一 ---- 添加配置
步骤二中的函数
// 新建一个AppDelegate 分类:AppDelegate+Hook externvoid __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. }externvoid __sanitizer_cov_trace_pc_guard(uint32_t *guard) { if (!*guard) return; // Duplicate the guard check.void *PC = __builtin_return_address(0); // 拿到被调用函数的地址。 char PcDescr[1024]; //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr)); 这一个函数在官网中没有说明,我们屏蔽掉 printf("guard: %p %x PC %s\n", guard, *guard, PcDescr); }

在这里面拿到了被调用方法的地址,我们根据地址拿符号。
步骤三中的代码
#import // 引入系统库void *PC = __builtin_return_address(0); //在这个位置插入下面的代码 //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; Dl_info info; // 所有信息都在这个结构体里面 dladdr(PC, &info); NSLog(@"dli_fname : %s\ndli_fbase : %p\ndli_sname : %s\ndli_saddr : %p\n\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr); // 其中一个打印的内容 dli_fname : /Users/xxxx/Library/Developer/CoreSimulator/Devices/C09425DB-F468-4BB4-BE33-42845DFEAE07/data/Containers/Bundle/Application/D14C571E-54D1-4ABD-85B5-3CE16A46744C/我的App名.app/我的App名 dli_fbase : 0x10af7c000 dli_sname : -[UIView(SDLayoutExtention) sd_equalWidthSubviews] dli_saddr : 0x10afb3bd0

步骤四的操作:
不能用数组直接操作,那么换个方式:原子队列
//原子队列 staticOSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT; //定义符号结构体 typedef struct { void *pc; void *next; }SYNode; void __sanitizer_cov_trace_pc_guard(uint32_t *guard) { //if (!*guard) return; // Duplicate the guard check.不屏蔽的话打印出来load方法 /*精确定位 哪里开始 到哪里结束!在这里面做判断写条件!*/void *PC = __builtin_return_address(0); SYNode *node = malloc(sizeof(SYNode)); *node = (SYNode){PC,NULL}; //进入 OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next)); // NSLog(@"dli_fname : %s\ndli_fbase : %p\ndli_sname : %s\ndli_saddr : %p\n\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr); }

原子队列先进后出,我们需要对保存的符号进行 去翻、去重、重组(Block、函数 符号不完整)。
最后完成的一个完整类:#import "AppDelegate+Hook.h"
#import "AppDelegate+Hook.h"#import "Aspects.h"#import #import @implementation AppDelegate (Hook)+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{Classclass= NSClassFromString(@"SU_UnionHomeVC"); [class aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionBefore usingBlock:^(id info) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [self methodSymbolList]; }); } error:NULL]; }); }//原子队列 staticOSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT; //定义符号结构体 typedef struct { void *pc; void *next; }SYNode; #ifdef DEBUG#endif+ (void)methodSymbolList {NSMutableArray * 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); BOOLisObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["]; NSString * symbolName = isObjc ? name: [@"_" stringByAppendingString:name]; [symbolNames addObject:symbolName]; } //取反 NSEnumerator * emt = [symbolNames reverseObjectEnumerator]; //去重 NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count]; NSString * name; while (name = [emt nextObject]) { if (![funcs containsObject:name]) { [funcs addObject:name]; } } //干掉自己! [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]]; //将数组变成字符串 NSString * funcStr = [funcscomponentsJoinedByString:@"\n"]; NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"fileName.order"]; NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding]; [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil]; NSLog(@"%@",funcStr); }#ifdef DEBUGvoid __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)); //printf("fname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n", //info.dli_fname, //info.dli_fbase, //info.dli_sname, //info.dli_saddr); // }#endif@end

最后取出methodSymbolList 方法里面打印的路径里面的文件, 替换掉根目录下的.order文件。

    推荐阅读