App启动时间优化之二进制重排
1、启动时间的定义
- 点击app图标到首页数据加载完毕
- 点击app图标到launch界面完全消失的第一帧
我们这里按照第二种来去定义应用的启动时间。
由于启动动画时长为400ms,所以一般情况下app的启动最佳时间是400ms内
下面直接进入正题,其他概念方面的东西不多做赘述。
2、二进制重排
看了很多大佬的文章,抖音大佬主要讲了二进制重排的大概原理和一些实现的思路,主要采用的是静态扫描的方式去获取函数符号,文章比较粗略。戳我👉🏻
本记录主要是通过此文章进行的实践戳戳戳👉🏻这篇文章的作者包括概念性的东西都讲述的很详细。
我们不墨迹直接开始操作!
1、systemTrace工具使用
- 1、先打开XCode的Instrument找到工具system trace
- 2、选择真机->all process 全部进程,为了避免手机内应用的内存缓存,先把应用删除,并clean项目,再run systemTrace,然后再运行Xcode的app项目。等项目运行起来启动页结束出现应用的第一个页面的时候,把systemTrace停止运行。
- 3、找到运行的项目,点下一级,再找到MainThread,选择summary:Virtual Memory,就可以看到下方的File Backed Page In就是缺页中断的次数。
2、查看工程的符号顺序
程序在编译的时候,会有一个默认的符号顺序的列表,这个列表包含了项目中所有类的函数的逻辑地址,内存分页就是按照该内存地址进行排列。
二进制重排的最终目的其实就是改变这个列表的排序,让我们在程序启动的时候需要调用的函数的逻辑地址顺序排列起来。
首先我们先通过Xcode的配置去获取这个符号列表。
-
1、build Setting 中搜索 Link m找到 Write Link Map File 默认为NO,此处设置为YES。
-
2、Clean项目,然后再run。Success之后我们找到项目目录中如下位置。
-
3、根据下图的目录中找到后缀为.txt的文件。我们打开它。
-
4、找到# Symbols:
-
5、然后我们对照Xcode,Target中的Build Phases点开Compile Sources
-
对比4点的图和5点的图,我们发现符号列表中函数的排列顺序 就是按照CompileSources类的顺序来排列的。
好的,既然我们知道了Xcode编译产生的mapFile的原样了。我们接下来就是需要找到app在点击运行的时候到app第一个页面出来的时候调用了哪些函数,并将它重新排列在这个txt里面,使它的逻辑地址连续,从而让他们在同一个内存分页中连续。
3、Clang静态插桩
我们直接上代码,原理可以看之前提到的原文戳戳戳大佬原文👉🏻
-
1、开始我们还是先配置XCode,这个文件放的就是我们需要重排列的符号列表,XCode会自动将文件内的内容重新排列在之前我们生成的符号列表txt文件的前方顺序排列。
-
2、第一点只是声明,但是没有实体文件,我们需要手动创建一下,创建lb.order ,我们cd到项目根目录下命令好执行 touch lb.order 生成对应的lb.order文件。
-
3、build Setting 中直接搜索 Other C Flags 找到Apple Clang - Custom Compiler Flags 添加以下代码配置
-fsanitize-coverage=func,trace-pc-guard
- 4、添加以下代码,并在appDelegate最后调用。
.h
@interface ClangInsertStaticPile : NSObject
+ (void)startWriteToFileOfClangInsertStaticPile;
@end
.m
#import "ClangInsertStaticPile.h"
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
@implementation ClangInsertStaticPile
+ (void)load{
}
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)startWriteToFileOfClangInsertStaticPile{
NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
while (true) {
//offsetof 就是针对某个结构体找到某个属性相对这个结构体的偏移量
SymbolNode * node = OSAtomicDequeue(&symboList, offsetof(SymbolNode, 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];
//去重
if (![symbolNames containsObject:symbolName]) {
[symbolNames addObject:symbolName];
}
}
//取反
NSArray * symbolAry = [[symbolNames reverseObjectEnumerator] allObjects];
NSLog(@"%@",symbolAry);
//将结果写入到文件
NSString * funcString = [symbolAry componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lb.order"];
NSData * fileContents = [funcString dataUsingEncoding:NSUTF8StringEncoding];
BOOL result = [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
if (result) {
NSLog(@"%@",filePath);
}else{
NSLog(@"文件写入出错");
}
}
//原子队列
static OSQueueHead symboList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
void * pc;
void * next;
}SymbolNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//if (!*guard) return; // Duplicate the guard check.
void *PC = __builtin_return_address(0);
SymbolNode * node = malloc(sizeof(SymbolNode));
*node = (SymbolNode){PC,NULL};
//入队
// offsetof 用在这里是为了入队添加下一个节点找到 前一个节点next指针的位置
OSAtomicEnqueue(&symboList, node, offsetof(SymbolNode, next));
}
@end
- 5、成功生成lb.order之后我们去找到这个文件
选中项目下载包
下载下来的包右键显示包内容
把这个lb.order的内容拷贝到项目目录中的lb.order里面就完成了。
- 6、验证结果
clean 一下再跑一下项目,我们再按最开始的方式去看一下 # Symbols的符号顺序,发现我们已经重排成功了!我们对比一下:
再用system Trace看一下对比一下:
4、总结
由于我的项目工程并不大,并且该二进制重排方式仅仅只能优化本体项目的分页内容,所以其实优化效果并不明显,我们的二进制重排其实并不彻底。
通过配置工程,DYLD_PRINT_STATISTICS 可以看到pre-maind的启动时间
其实优化的方式多种多样,我们也可以从动态库入手,对于私有动态库,可以才用合并动态库的方式进行优化等。对于类的初始化方法,我们可以少使用load方法,尽可能使用initializer在类使用到的时候再进行初始化等等方式。
如果进行彻底的二进制重排需要对第三方的framework也进行上述重排方式,一个一个framework进行操作会比较复杂且耗时,这次实践就没有做第三方的重排。静态插桩二进制重排仅是其中一种方式,本文是通过大佬的文章进行实操做的一个记录,也是一个学习过程的记录。