iOS-OC启动优化

应用启动分为冷启动和热启动;
冷启动指:在内存中不包含相关数据,必须从磁盘载入到内存中。
热启动指:在打开应用程序时,在内存中存在部分程序数据,使得程序数据不用全部载入磁盘。
测试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配置

iShot2020-11-23 15.30.17.png

2、导入头文件和实现两个函数:

#include 
#include 
#include 


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。

因此,无论是函数,方法,都能获取到。


iShot2020-11-23 15.51.16.png

下面通过一段代码来了解一下这两个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 *)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
通过在__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 原子队列来增加线程安全。

创建存储符号的结构体和定义原子队列:

@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 *)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:

iShot2020-11-23 17.05.53.png

解决方法:
build setting中的other c flag中修改为:-fsanitize-coverage=func,trace-pc-guard

就正常hook所有函数了:

iShot2020-11-23 17.14.18.png

而上面的代码,还缺少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);

执行结果:


iShot2020-11-24 14.32.11.png

那么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文件,在里面可以修改方法执行顺序:


iShot2020-11-24 14.41.57.png

在你修改完之后,就可以进行使用了;

首先我们需要查看未重排的方法执行顺序;
build setting中搜索link map,将Write Link Map File设置为YES,编译之后,在项目路径下的Intermediates.noindex文件中的.build中找到后缀为LinkMap-normal-x86_64.txt的文件打开;在文件中就能看到方法执行顺序:
下图是默认执行顺序:

iShot2020-11-24 14.50.38.png

配置order文件:
build setting中搜索order file,将order文件的路径填进去;

再清除完缓存重新编译后,再去看LinkMap-normal-x86_64.txt文件:
可以看到,已经重排成功:

iShot2020-11-24 14.54.01.png

还有一点就是关于Swift的重排,首先创建一个swift文件,需要桥接,里面创建一个函数,在viewcontroller中调用;
那么还需要在build setting中搜索other seift flag,输入-sanitize-coverage=func-sanitize=undefined
那么执行程序之后,同样可以hook到swift方法:

iShot2020-11-24 15.14.32.png

你可能感兴趣的:(iOS-OC启动优化)