应用启动分为冷启动和热启动;
冷启动指:在内存中不包含相关数据,必须从磁盘载入到内存中。
热启动指:在打开应用程序时,在内存中存在部分程序数据,使得程序数据不用全部载入磁盘。
测试app启动分两个阶段,由main函数作为一个分界点,main之前时pre-main,系统反馈。
main函数之后是靠自己业务去进行优化,而启动优化,主要解决main函数之前。
通过在edit scheme
中添加环境变量DYLD_PRINT_STATISTICS
,就可以打印出项目的所有耗时时间。
Total pre-main time: 90.14 milliseconds (100.0%)
//动态库加载
dylib loading time: 122.85 milliseconds (136.2%)
//偏移修正耗时,绑定耗时,就是给符号赋值的过程;
//ASLR安全机制随机值,偏移修正耗时,安全随机值+偏移值 = 运行到内存的地址。
rebase/binding time: 126687488.8 seconds (278005995.4%)
//oc类注册耗时,oc类越多越耗时,swift没有这个耗时
ObjC setup time: 11.53 milliseconds (12.7%)
//构造函数,load耗时
initializer time: 66.23 milliseconds (73.4%)
slowest intializers :
libSystem.B.dylib : 4.77 milliseconds (5.2%)
libBacktraceRecording.dylib : 6.92 milliseconds (7.6%)
libobjc.A.dylib : 2.21 milliseconds (2.4%)
CoreFoundation : 1.89 milliseconds (2.1%)
libMainThreadChecker.dylib : 41.72 milliseconds (46.2%)
libLLVMContainer.dylib : 2.40 milliseconds (2.6%)
//主程序耗时
Demo : 2.00 milliseconds (2.2%)
在main函数之后的优化,有以下几点建议:
减少启动初始化流程,能懒加载就懒加载,去除不需要的类
加载尽量使用多线程,将cpu性能发挥出来
启动时刻页面尽量用存代码来写。
二进制重排和clang插桩
配置文件:
1、在build setting
中搜索Other c flag
,添加-fsanitize-coverage=trace-pc-guard
配置
2、导入头文件和实现两个函数:
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
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;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
// void *PC = __builtin_return_address(0);
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
配置完就可以了,执行程序打印的是以下信息:
INIT: 0x10698c4b0 0x10698c4e8
guard: 0x10698c4c0 5 PC
guard: 0x10698c4b4 2 PC
guard: 0x10698c4b8 3 PC pob\200\377�
guard: 0x10698c4dc c PC
guard: 0x10698c4e0 d PC \240\217'\351\376�
guard: 0x10698c4dc c PC �
guard: 0x10698c4dc c PC �
guard: 0x10698c4c4 6 PC `\220'\351\376�
guard: 0x10698c4dc c PC
guard: 0x10698c4dc c PC 0\224'\351\376�
guard: 0x10698c4dc c PC
guard: 0x10698c4b0 1 PC \350\273\356\206\377�
guard: 0x10698c4d4 a PC
guard: 0x10698c4cc 8 PC
start是起始位置,通过x 0x10698c4b0
可以获取它的内存地址信息;
stop是终止位置,但是要获取stop的内存地址信息,需要对0x10698c4e8
地址进行减去0x4
。读结尾数需要往前走4个字节.
读取stop信息时,最开始位为0e,表示14位,当添加一个方法,就变为0f。
因此,无论是函数,方法,都能获取到。
下面通过一段代码来了解一下这两个c函数的作用:看代码:
void test(){
blcok1();
}
void(^blcok1)(void) = ^(void){
};
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;
}
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
if (!*guard) return;
// void *PC = __builtin_return_address(0);
char PcDescr[1024];
printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
test();
}
通过执行代码,点击屏幕,会发现guard
会打印三次,也就是说,在touchBegin
方法中,调用了test,test中调用了block,一共执行了三个方法,而控制太也打印了三个方法。因此可以得到一个结论,它hook到了所有的方法。
那么怎么确定是谁的方法??
拿到所有的符号,guard
通过在__sanitizer_cov_trace_pc_guard
方法中的void *PC = __builtin_return_address(0);
代码,其中__builtin_return_address
是返回地址信息;
在经过执行到__builtin_return_address
的下一步,查看PC的值,可以得到PC的地址就是上一个执行函数的地址。
那么重点来了,要拿到方法的符号,就要引入一个库#import<dlfcn.h>
;
通过在__sanitizer_cov_trace_pc_guard
方法中PC下面添加下面的方法:
Dl_info info;
dladdr(PC, &info);
printf("fname:%s\n,fbase:%s\n,sname:%s\n,saddr:%s\n\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
当点击屏幕,就会打印三个方法:
fname:/Users/pengwenxi/Library/Developer/CoreSimulator/Devices/1F9DE621-C300-4CF5-BB43-F978DB5EEC30/data/Containers/Bundle/Application/A7D74872-F86B-4C5C-8BF3-29819DFBC3C7/二进制重排和Clang插桩.app/二进制重排和Clang插桩
,fbase:\317\372\355\376�
,sname:-[ViewController touchesBegan:withEvent:]
,saddr:UH\211\345H\203\354@̍�-{
fname:/Users/pengwenxi/Library/Developer/CoreSimulator/Devices/1F9DE621-C300-4CF5-BB43-F978DB5EEC30/data/Containers/Bundle/Application/A7D74872-F86B-4C5C-8BF3-29819DFBC3C7/二进制重排和Clang插桩.app/二进制重排和Clang插桩
,fbase:\317\372\355\376�
,sname:test
,saddr:UH\211\345H\215=\311|
fname:/Users/pengwenxi/Library/Developer/CoreSimulator/Devices/1F9DE621-C300-4CF5-BB43-F978DB5EEC30/data/Containers/Bundle/Application/A7D74872-F86B-4C5C-8BF3-29819DFBC3C7/二进制重排和Clang插桩.app/二进制重排和Clang插桩
,fbase:\317\372\355\376�
,sname:blcok1_block_invoke
,saddr:UH\211\345H\203\354 H\215�\231|
可以看到,其中的sname就是方法名,既然拿到了方法名,那就就剩下生成order文件,将所有符号写入order文件。
下面需要利用#import <libkern/OSAtomic.h>
原子队列来增加线程安全。
创建存储符号的结构体和定义原子队列:
@implementation ViewController
//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
void *pc;
void *next;
}Node;
在__sanitizer_cov_trace_pc_guard
方法中创建结构体和加入结构体:
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
//我怎么知道是谁??
//拿到所有的符号 guard
if (!*guard) return;
//当前函数返回上一个调用的地址
void *PC = __builtin_return_address(0);
//创建结构体
Node *node = malloc(sizeof(Node));
*node = (Node){PC,NULL};
//加入结构
OSAtomicEnqueue(&symbolList, node, offsetof(Node, next));
// char PcDescr[1024];
// printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
// Dl_info info;
// dladdr(PC, &info);
// printf("fname:%s\n,fbase:%s\n,sname:%s\n,saddr:%s\n\n",info.dli_fname,info.dli_fbase,info.dli_sname,info.dli_saddr);
}
在touchesBegan
中实现方法的hook:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
while (YES) {
Node *node = OSAtomicDequeue(&symbolList, offsetof(Node, next));
if(node == NULL){
break;
}
Dl_info info = {0};
dladdr(node->pc, &info);
printf("%s \n",info.dli_sname);
}
}
在最后就会出现一个bug,那就是一直只hook一个方法,因为循环一次就会hook一次,而方法中一直进行while循环,clang只要有跳转,就会被hook:
解决方法:
在build setting
中的other c flag
中修改为:-fsanitize-coverage=func,trace-pc-guard
。
就正常hook所有函数了:
而上面的代码,还缺少load方法不能hook,要hookload
方法,需要将__sanitizer_cov_trace_pc_guard
方法中的if (!*guard) return;
注释掉,就能够hookload
方法了。
接下来是将所有方法存储到数组并且反序存储,并且去除重复代码。
NSString *name = @(info.dli_sname);
BOOL isObjc = [name hasPrefix:@"+["] || [name hasPrefix:@"-["];
NSString *symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
[symbolNames addObject:symbolName];
NSEnumerator *enumerator = [symbolNames reverseObjectEnumerator];
//创建一个新数组
NSMutableArray *funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
NSString *name;
//去重
while (name = [enumerator nextObject]) {
if(![funcs containsObject:name]){
//数组中不包含name
[funcs addObject:name];
}
}
NSLog(@"%@",funcs);
执行结果:
那么hook完所有方法,那么就只剩下将方法写入order文件。
NSString *funcStr = [funcs componentsJoinedByString:@"\n"];
NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"WX.order"];
NSData *fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
[[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
NSLog(@"%@",NSTemporaryDirectory());
执行完之后,就有一个order文件,在里面可以修改方法执行顺序:
在你修改完之后,就可以进行使用了;
首先我们需要查看未重排的方法执行顺序;
在build setting
中搜索link map
,将Write Link Map File
设置为YES,编译之后,在项目路径下的Intermediates.noindex
文件中的.build
中找到后缀为LinkMap-normal-x86_64.txt
的文件打开;在文件中就能看到方法执行顺序:
下图是默认执行顺序:
配置order文件:
在build setting
中搜索order file
,将order文件的路径填进去;
再清除完缓存重新编译后,再去看LinkMap-normal-x86_64.txt
文件:
可以看到,已经重排成功:
还有一点就是关于Swift的重排,首先创建一个swift文件,需要桥接,里面创建一个函数,在viewcontroller中调用;
那么还需要在build setting
中搜索other seift flag
,输入-sanitize-coverage=func
和-sanitize=undefined
;
那么执行程序之后,同样可以hook到swift方法: