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 <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。

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


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<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:

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容