前言
在APP启动过程详解+优化(二进制重排)
一文中了解了由于缺页中断
导致启动耗时,我们可以编译的时候根据我们.order
方法进行排列,但是我们项目比较大的话,找到方法进行排列就比较困难。而这篇文章就是解决这个问题,拭目以待吧。
准备工作
1. Clang插桩配置
LLVM
内置一个简单的代码覆盖仪表
(SanitizerCoverage
)。它将用户定义的函数插入函数、基本块和边缘级别的调用。提供了这些回调的默认实现,并实现简单的覆盖报告和可视化。Clang代码覆盖文章一文有详细的说明,如下:
1.1 配置
如果开发的是oc
项目的话,在 Build Settings
里的 Other C Flags
中添加如下配置:
-fsanitize-coverage=trace-pc-guard
在任意一个控制器或者文件添加
文档说明的方法
实现,编译通过。如下:运行后打印出方法的实现,如下:
__sanitizer_cov_trace_pc_guard_init
其中的start
和stop
代表是方法的个数起始和结束的个数,进行for
循环,当起始地址的值和终止的相同时return
。如下:这里
0e
表示14
个符号,我们添加个 方法 和block
,如下:十六进制
10
表示16
,那么添加一个方法和block
相当于添加了两个符号进去了。
补充:
我们如果swift
项目或者混编
的话,在 Build Settings
里的 Other Swift Flags
中添加如下配置:
-sanitize-coverage=func
-sanitize=undefined
1.2 原理
我们使用惯用的手法打开汇编断点查看如下:
汇编断点中可以看出
main
函数后会调用__sanitizer_cov_trace_pc_guard
,然后继续追踪断点,如下:发现
[AppDelegate application:didFinishLaunchingWithOptions:]
函数会调用__sanitizer_cov_trace_pc_guard
。
经过多次的断点跟踪,后续发现在实现我们的方法后都会调用__sanitizer_cov_trace_pc_guard
,相当于系统在编译的时候会给我们项目中方法添加hook
,进行标记,定位我们的方法。我们是通过Other c Flags
添加的标记,所以肯定是在编译期做这个插入代码的动作
。
2. 具体实现
之前分析了只要调用我们自身的方法就会调用系统为我们添加的hook
,
__builtin_return_address(0)
这个函数返回的是上一个函数的地址
,也就是调用者
,这个PC
就是上一个函数的地址,表示第0
行插入的__sanitizer_cov_trace_pc_guard
。我们取出这个地址的信息,我们通过Dl_info
来保存该方法的信息。
Dl_info info;
dladdr(PC, &info);
导入#import <dlfcn.h>
,Dl_info
的组成,包括路径
,方法名
,地址
。如下:
那么知道了信息的保存,就可以运行项目查看方法的名字,如下:
打印的时候没有
+load
方法,我们打断点发现确实进入了,但是guad
为0
,因此写入的时候去除这个判断。如下:2.1 原子操作保存方法名
我们添加1个方法进行获取方法名,我们在存储的时候,多线程调用方法,这个hook
也会在多线程,这个时候写入操作的话,会造成线程不安全
,因此我们采用原子操作
。
我们导入#import <libkern/OSAtomic.h>
定义原子队列
和定义符号结构体
,如下:
在
hook
中写入方法,如下:这里添加
next
节点地址也就是PC
的信息,方便我们取出的时候判断是否时最后一个
,最后一个没有下个节点信息的
。
在touchesBegan
中添加for循环
,如下:
结果
死循环
了,hook
把我们的for循环也捕获了
,导致了表一直插入进入死循环,我们修改下标记,如下:
-fsanitize-coverage=func,trace-pc-guard
如果项目中存在
swift
代码的话,或者block
等则需要判断,如下:然后进行
添加
和反向遍历
,并去除本身调用的方法
,如下:最后写入
order
文件,如下:运行并打印结果,按照执行的顺序排列,如下:
2.3 链接orderFile
写入在OrderFile
中进行链接,编译后方法的顺序就是我们启动时执行的顺序
,如下:
3. 总结
通过官方对方法的hook
,定位到实现的方法,存入数组,最后按.order
文件进行读取,从而减少page fault的次数
,提高启动速度
。写入的具体代码:
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct {
void * pc;
void * next;
} KBNode;
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
// if (!*guard) return;
void *PC = __builtin_return_address(0);
// Dl_info info;
// dladdr(PC, &info);
// NSLog(@"%s\n,\n",info.dli_sname);
KBNode *node = malloc(sizeof(KBNode));
*node = (KBNode){PC,NULL};//赋值,强转,next表示下个节点的地址
OSAtomicEnqueue(&symbolList, node, offsetof(KBNode, next));//原子列表写入方法,并把下个节点的地址写入。
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//定义数组
NSMutableArray<NSString *> * symbleNames = [NSMutableArray array];
while (YES) {
KBNode *node = OSAtomicDequeue(&symbolList, offsetof(KBNode, next));
if (node == NULL) {
break;//没有的话结束
}
Dl_info info;
dladdr(node->pc, &info);
// printf("%s\n",info.dli_sname);
NSString * name = @(info.dli_sname);//转字符串
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbleNames addObject:symbolName];
}
//添加
NSEnumerator * em = [symbleNames reverseObjectEnumerator];
NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbleNames.count];
NSString * name;
while (name = [em nextObject]) {
if (![funcs containsObject:name]) {//数组没有name
[funcs addObject:name];
}
}
//去掉自己!
[funcs removeObject:[NSString stringWithFormat:@"%s",__func__]];
//写入文件
//1.编程字符串
NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"test.order"];
NSData * file = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:file attributes:nil];
NSLog(@"%@",funcStr);
}