手动目录随着App的不管更新、功能增加等,app工程会原来越大,需要在启动就加载的功能模块也可能越来越多,这个是很耗时的,App的启动时间过久,多用户是很不友好的。所以App的启动优化是一个非常有必要的。
- Main之前
- Main 之后
- 二进制重排
系统默认加载方式
1)、查看PageFault 次数
2)、查看系统默认的链接符号
指定加载方式
App的启动分为:
热启动、冷启动
。一般我们说的启动优化 是指冷启动
启动优化,从2方面入手:Main函数之前、Main函数之后。
Main函数之前 主要是由系统决定。
Main函数之后:由用户的加在内容决定。
Main之前 如何检测Main函数之前的启动时间?
添加一个环境变量:
DYLD_PRINT_STATISTICS
文章图片
添加环境变量
重新启动 打印出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函数之前,有哪些是可以 做 优化的:Main之后 Main 之后的时间 自己用计时器打印。推荐一个工具:BLStopwatch打点计时器。 里面也链接了一篇作者自己的关于Main之后启动优化的文章 一次立竿见影的启动时间优化。
- 1、库的加载
系统库经过优化处理的,本身加载就 很快。
自己倒入的库: 苹果给出的建议是不超过6个。如果超过6个,可以采用合并的方式。- 2、减少不必要的类 、资源图片等。
比如随着版本更新迭代,有些类、图片等被弃用了的。
可以使用工具检测没有用到的类
这个操作相对来说优化的成效不高。有人说 减少2w个类,启动时间只少了800 毫秒。- 3、能不在Load里面做的操作,就不要在Load操作。
- 4、二进制重排
这个主要是正对binding阶段的操作。
// 打点计时器用法
//在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 之后的优化网上关于Main之后的优化内容比较多。包括上面提到的 打点计时器作者的那片文章一次立竿见影的启动时间优化。
1、能懒加载的就懒加载
2、发货CPU的性能(多线程初始化)
3、启动阶段的尽量不要用Xib、stroyboard。 Xib、storyboard都是需要进行xml解析,相对纯代码来讲,是比较耗时的。
二进制重排 这篇文章主要讲Main之前的启动优化。
二进制重排
。在19年 抖音团队的一篇关于二进制重排火了。我们这篇文章主要讲如何操作
关于二进制重排,需要先了解
虚拟内存
和物理内存
。早期的计算机都是使用物理内存来处理:一次全部把App加载进入内存。这也就是出现了当内存满了之后,在打开一个应用,就会报错。而且也不安全(因为 全部都加载进入内存,而且是连续的,当拿到内存地址开始的位置,其他的内存信息,都可以通过内存偏移来拿到)。
虚拟内存就是为了解决这些问题的,先加载必须的信息,其他的信息当你用到的时候再在物理内存上去分配。
虚拟地址于物理地址 中间通过一张映射表(页表)进行管理。由硬件mmu(CPU里的一个单元)来管理。
我们在Xcode里面打印出来的地址 都是
虚拟地址,这个时候的地址是连续的
。但是实际地址需要通过映射表(页表)去寻址。 在物理地址上可能是不连续的
,物理内存是分页的(在iOS设备 一页16K,Mac 一页4K)。当App加载的时候,会将虚拟内存映射到物理内存,这个时候,物理内存大概是这样的
文章图片
物理内存分页 App先加载部分进入内存,当App使用某一功能的时候,发现在物理内存里面没有,这个时候会发生
缺页中断(PageFault)
---- 这个时候会先阻塞进程,先将虚拟内存加入物理内存中去。加载的原则就是哪里有空的,加载哪里,没有空的,覆盖掉其他的。(这也是为什么当我们App开的比较多的时候,最开始打开的App会被重新启动加载)。
还有一个问题:当App编译好之后,他的虚拟内存的地址就固定了,这样会很容易黑客攻击,为了解决这个问题 ,就出现了ASLR - 地址空间布局随机化(就是在每个内存前面 加一个随机偏移值)。
因为内存加载是进行分页加载。那么我要先加载那些,后加载那些 。能不能自己进行指定?答案是可以的。Xcode支持指定符号进行加载。
系统默认加载方式 查看PageFault 次数 我们借助 系统自带调试工具:Xcode -> Instruments -> System Trace
文章图片
System Trace 步骤一: 将应用安装到手机上
步骤二:打开System Trace 清空筛选条件,输入 Main Thread (下图一)
步骤三:点击start ,等状态变成黑色方形图标 点击停止 (下图二)
步骤四:选中Main Thread 选择主线程 - > Summary : Virtual Memory(下图三)
查看缺页中断次数:
文章图片
图一
文章图片
图二
文章图片
图三 这样就看到了缺页中断的次数(File Backed Page In):这里是2747次。总耗时:814ms。
(注意:如果不是第一次启动。尽量多的点开其他应用,把物理内存中的page 尽量清空)。
后台退出App,在进行一次 操作,这个时候,File Backed page In 可能就很小(几十----一百多)。
查看系统默认的链接符号 直接在Xcode 设置改。
文章图片
修改Link map 编译之后找到 编译好的.app
工程 -> Products —> xxx.app
Show In Finder文章图片
找到相应目录
按照这个目录去找
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文件
文章图片
源码里的order文件
打开这个文件,里面就是指定的加载内容。
那么我们自己如何使用这个功能
【iOS-App启动优化】指定符号顺序这样 Xcode就会按照我们指定的符号去加载到内存。
1、 创建一个 .order 文件 (filename.order) 并放入工程目录下
2、在Build Settings 下搜索 order File (下图一) ,输入 路径 ./filename.order (因为我放在了根目录 下)
文章图片
添加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添加一个参数 (下图一)
- 步骤二:在合适的位置写入它指定的函数。
- 步骤三:根据打印的出来的地址,根据方法地址,找到方法符号。
- 步骤四:保存找到的符号。
因为编译插入的函数可能是在子线程,所以不能直接用数组来保存。
文章图片
图一 ---- 添加配置
步骤二中的函数
// 新建一个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文件。