APP启动分为pre-main和mian两个过程,首先我们需要进行如下图设置,设置DYLD_PRINT_STATISTICS为1,这样我们就可以打印出来APP在启动的时候所花费的时间。
下面是各个阶段的消耗时间:
1. dylib loading time
这个时间优化,也就是dylib链接动态库的优化,可以减少动态库个数(指自己写的库),苹果给我们建议是我们自己写的动态库不要超过6个是最好的。
2.ObjC setup time
这个是注册objc所有类所需时间,在mach-0里读取所有文件的类,把它们会放到一张表里面。我们可以安装一个fui,通过终端输入 fui find命令可以打印出没有被使用的类,我们把这些没使用的类进行一些删除。还可以通过工具检测是否工厂里有没有使用的图片。
3. rebase/binding time
binding就是一个符号绑定,其会把一个函数具体实现地址和对应函数符号进行链接。比如printf函数打印,其内部是通过一个binging链接到printf具体函数实现和printf这个符号,然后进行打印。
rebase是一个进行指针的修复,这里有个虚拟内存的概念,在早期时候都是通过物理内存,这样会出现内存不够用,不安全。但是虚拟内存也会不安全,因为其地址都是从0开始,为了解决这个问题用到了ASLR(地址空间随机话),
4. initializer time
初始化时间优化,一边我们再load里不要添加耗时操作,因为load加载会在mian函数之前。
Total pre-main time: 2.0 seconds (100.0%)
dylib loading time: 232.43 milliseconds (11.4%)
rebase/binding time: 1.0 seconds (53.4%)
ObjC setup time: 224.46 milliseconds (11.0%)
initializer time: 486.86 milliseconds (23.9%)
slowest intializers :
libSystem.B.dylib : 61.13 milliseconds (3.0%)
libprotobuf.dylib : 55.00 milliseconds (2.7%)
Test : 185.69 milliseconds (9.1%)
二进制重排
每个page fault消耗时间虽然很少,但是多个时间就会很多。所以这个是我们优化的重点。App在执⾏的过程中会存在⼤量的Page Fault,⼀个Page Fault
的耗时很少,但是当⼤量的Page Fault存在时,就会影响到代码的执⾏速度。同理,在App启动的时候,就可能会出现⼤量的Page Fault。⼆进制重排就是把在启动过程中需要使⽤到的符号,重新排列在⼀个或者⼏个Page⾥⾯,减少Page Fault的次数,从⽽达到减少启动时间的⽬的。
在build phrase里我们可以重新排列调用文件顺序,但是这样不是一个好办法。
可以通过设置.order文件,因为系统在启动的时候是会先读.order文件进行配置启动。首先通过终端创建一个lg.order文件。然后我们可以看到就是按我们.order文件进行排序的。
那我们如何知道哪些函数在启动的时候使用需要放到前面,这个就需要clang插桩技术,首先我们开启clang插桩,然后需要添加几个头文件#include <stdint.h>,#include <stdio.h>,#include <sanitizer/coverage_interface.h>。
在viewController里也添加了如下的两个方法:
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;
NSLog(@"%d",N);
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// NSLog(@"%s",__func__);
if (!*guard) return;
void *PC = __builtin_return_address(0);
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,0};
//插入结构体
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, abc));
// printf("%s\n",info.dli_sname);
}
上面运行后会打印如下,也就是stop和start的打印,下面30代表是的30个方法。下面是4个字节的存储01 00 00 00,其存的值就是1,2,3,4,5,6,7,8。通过
1.__sanitizer_cov_trace_pc_guard_init这个方法也就知道了在项目中用到的符号(方法)的个数。
2.系统每调用一个方法,都会回调这个void __sanitizer_cov_trace_pc_guard(uint32_t *guard) 这个方法,我们发现这个方法会到各个方法调用之前会被调用,那么就可以在这里获取到启动时候调用了哪些方法。
INIT: 0x1009e1750 0x1009e17c8
(lldb) x 0x1009e1750
0x1009e1750: 01 00 00 00 02 00 00 00 03 00 00 00 04 00 00 00 ................
0x1009e1760: 05 00 00 00 06 00 00 00 07 00 00 00 08 00 00 00 ................
(lldb) x 0x1009e17c8
0x1009e17c8: 00 00 00 00 00 00 00 00 1e 00 00 00 00 00 00 00 ................
0x1009e17d8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
2022-06-13 16:18:47.368235+0800 TraceDemo[43903:514812] 30
下面是获取一个函数的名称以及地址:
Dl_info info;
dladdr(node->pc, &info);
NSString *name = @(info.dli_sname);
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;
下面dli_fbase是文件地址,dli_sname是名字也就是符号,dli_saddr符号的地址。其实我们只需要dli_sname就可以。
下面是dli_sname的打印,也就是启动后的程序所运行的函数,其中对于swift也有效,s9TraceDemo9SwiftTestC05swiftD0yyFZTo这些方法就是swift的,因为swift有自带混淆。
-[AppDelegate application:didFinishLaunchingWithOptions:]
-[SceneDelegate window]
-[SceneDelegate setWindow:]
-[SceneDelegate scene:willConnectToSession:options:]
-[ViewController viewDidLoad]
_$s9TraceDemo9SwiftTestC05swiftD0yyFZTo
_$s9TraceDemo9SwiftTestC05swiftD0yyFZ
_$ss27_finalizeUninitializedArrayySayxGABnlF
_$sSa12_endMutationyyF
_$ss5print_9separator10terminatoryypd_S2StFfA0_
_$ss5print_9separator10terminatoryypd_S2StFfA1_
-[SceneDelegate sceneWillEnterForeground:]
-[SceneDelegate sceneDidBecomeActive:]
-[ViewController touchesBegan:withEvent:]
通过如下操作可以进行二进制重排:可以直接生成.oder文件,这里是把二进制符号文件存储在symbolList里符号列表里。
//原子队列 线程安全 先进后出 (/*只能保存结构体*/不用讲)
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
typedef struct {
void * pc;
int abc;
} SYNode;
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;
NSLog(@"%d",N);
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
void *PC = __builtin_return_address(0);
SYNode *node = malloc(sizeof(SYNode));
*node = (SYNode){PC,0};
//插入结构体
OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, abc));
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
//创建数组
NSMutableArray *symbleNames = [NSMutableArray array];
while (YES) {
SYNode *node = OSAtomicDequeue(&symbolList, offsetof(SYNode, abc));//取出symbolList里的方法符号
if (node == NULL) {
break;;
}
Dl_info info;
dladdr(node->pc, &info);
//转为OC字符串方便操作
NSString *name = @(info.dli_sname);
//OC方法 直接添加到数组
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbleName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbleNames addObject:symbleName];
}
NSEnumerator *em = [symbleNames reverseObjectEnumerator];//去重
//新数组存储
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
NSString *name;
while (name = [em nextObject]) {
if (![funcs containsObject:name]) {
[funcs addObject:name];
}
}
//生成.order文件
//数组转为字符串
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
//文件路径
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"lg.order"];
//文件内容
NSData *file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
//写入文件
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
//获取沙盒主目录路径
NSLog(@"%@",NSHomeDirectory());
NSLog(@"%@",funcStr);
}
二进制重排总结
-
在二进制重排里我们首先需要进行设置:
2.然后其会调用__sanitizer_cov_trace_pc_guard_init(这里可以打印出启动调用了几个方法) 和void __sanitizer_cov_trace_pc_guard(这里就是启动时候每调用一个方法就会回调这个)这两个方法,然后生成.order文件。
3.在生成了.order文件之后,需要进行配置处理,放了之后的话编译器就会按照.order的顺序进行排列了。