手动目录
- Main之前
- Main 之后
- 二进制重排
系统默认加载方式
1)、查看PageFault 次数
2)、查看系统默认的链接符号
指定加载方式
随着App的不管更新、功能增加等,app工程会原来越大,需要在启动就加载的功能模块也可能越来越多,这个是很耗时的,App的启动时间过久,多用户是很不友好的。所以App的启动优化是一个非常有必要的。
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函数之前,有哪些是可以 做 优化的:
- 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加载的时候,会将虚拟内存映射到物理内存,这个时候,物理内存大概是这样的
App先加载部分进入内存,当App使用某一功能的时候,发现在物理内存里面没有,这个时候会发生
缺页中断(PageFault)
---- 这个时候会先阻塞进程,先将虚拟内存加入物理内存中去。
加载的原则就是哪里有空的,加载哪里,没有空的,覆盖掉其他的。(这也是为什么当我们App开的比较多的时候,最开始打开的App会被重新启动加载)。
还有一个问题:当App编译好之后,他的虚拟内存的地址就固定了,这样会很容易黑客攻击,为了解决这个问题 ,就出现了ASLR - 地址空间布局随机化(就是在每个内存前面 加一个随机偏移值)。
因为内存加载是进行分页加载。那么我要先加载那些,后加载那些 。能不能自己进行指定?答案是可以的。Xcode支持指定符号进行加载。
系统默认加载方式
查看PageFault 次数
我们借助 系统自带调试工具:Xcode -> Instruments -> 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 设置改。
编译之后找到 编译好的.app 工程 -> Products —> xxx.app
Show In Finder
按照这个目录去找
Intermediates.noindex -> 工程名.build -> Debug-iphoneos(跑的机器不同,这个路径也不同) -> 工程名.bulid —> 工程名-LinkMap-normal-arm64.txt
打开这个文件 往下翻 找到这样的地方# Symbols:
# Symbols:
# Address Size File Name
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文件
打开这个文件,里面就是指定的加载内容。
那么我们自己如何使用这个功能
指定符号顺序
1、 创建一个 .order 文件 (filename.order) 并放入工程目录下
2、在Build Settings 下搜索 order File (下图一) ,输入 路径 ./filename.order (因为我放在了根目录 下)
这样 Xcode就会按照我们指定的符号去加载到内存。
获取启动需要的符号
说了那么多,我知道怎么用了,但是我要如何获取启动时,需要重排的符号?
我们使用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
extern void __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.
}
extern void __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
步骤四的操作:
不能用数组直接操作,那么换个方式:原子队列
//原子队列
static OSQueueHead 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, ^{
Class class= 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];
});
}
//原子队列
static OSQueueHead 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);
BOOL isObjc = [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 = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"fileName.order"];
NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",funcStr);
}
#ifdef DEBUG
void __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文件。