启动优化

热启动与冷启动

  • 冷启动App点击启动前,此时App的进程还不在系统里,内存中不包含app相关数据,需要系统新创建一个进程分配给App
  • 热启动App冷启动后用户将App退回后台,此时App的进程还在系统里,数据仍然存在,用户重新返回App的过程。

APP冷启动完整流程

冷启动的整个过程是指从用户唤起 App开始到 AppDelegate 中的 didFinishLaunchingWithOptions 方法执行完毕为止,并以执行main()函数的时机为分界点,分为pre-mainmain()两个阶段。

pre-main阶段

pre-main阶段,即main函数之前,操作系统加载App可执行文件到内存,执行一系列的加载&链接等工作,简单来说,就是dyld加载过程

2251862-eb6d2c7572f89693.jpg

  • dylib loading time(动态库耗时):主要是加载动态库
  • rebase/binding time(偏移修正/符号绑定耗时):进行rebase指针调整和bind符号绑定
    • rebase:任何一个app生成的二进制文件,所有的方法和函数调用都对应一个地址,这个地址就是当前二进制文件中的偏移地址,为了安全,通常会随机分配一个ASLR随机值,只有ASLR+偏移地址才是方法或函数对内存的真实地址
    • binding:将编译期创建的符号运行时地址进行绑定,一般是dyld执行,也可以叫动态库符号绑定
      *ObjC setup time(OC类注册的耗时):ObjC相关Classcategory注册、selector唯一性检查等,OC类越多,时间越长
      *initializer time(执行load和构造函数的耗时):+load()方法、attribute修饰的函数调用、创建C++静态全局变量等
pre-main优化方案
  • 减少外部动态库加载,官方建议自定义的动态库不要超过6个,如果超过则需要合并动态库
  • 减少OC类
  • 将不必须在+load方法中做的事情延迟到+initialize中,尽量不要用C++虚函数

main函数

main函数执行后的阶段,指的是:从 main函数执行开始,到appDelegatedidFinishLaunchingWithOptions方法里首屏渲染相关方法执行完成。
即,从main函数执行到设置self.window.rootViewController执行完成的阶段。

didFinishLaunching中的业务主要分为三个类型

  • 【第一类】初始化第三方sdk
  • 【第二类】app运行环境配置
  • 【第三类】自己工具类的初始化等
main函数阶段优化方案
  • 尽量使用纯代码来进行UI框架的搭建,尤其是主UI框架,例如UITabBarController。尽量避免使用Xib或者SB,相比纯代码而言,这种更耗时
  • 将耗时操作滞后或异步处理。
    通常的耗时操作有:网络加载、编辑、存储图片和文件等资源,不要占用主线程时间
  • main函数执行后到首屏渲染完成前,只处理首屏渲染相关业务。
    首屏渲染外的其他功能放到首屏渲染完成后去初始化
  • 减少启动初始化的流程,能懒加载的懒加载,能延迟的延迟,能放后台初始化的放后台,尽量不要占用主线程的启动时间
  • 启动阶段能使用多线程来初始化的,就使用多线程
  • 删除废弃类、方法

二进制重排

对用户而言,使用App时第一个直接体验就是启动 App 时间,而启动时期会有大量的类、分类、三方等等需要加载和执行,此时多个 Page Fault 所产生的的耗时往往是不能小觑的,下面我们就通过二进制重排来优化启动耗时。

虚拟内存

  • 进程物理内存之间增加一个中间层,这个中间层就是所谓的虚拟内存,主要用于解决当多个进程同时存在时,对物理内存的管理。提高了CPU的利用率,使多个进程可以同时、按需加载。所以虚拟内存其本质就是一张虚拟地址和物理地址对应关系的映射表

  • 每个进程都有一个独立的虚拟内存,其地址都是从0开始,大小是4G固定的,每个虚拟内存又会划分为一个一个的页(页的大小在iOS中是16K,其他的是4K),每次加载都是以页为单位加载的,进程间是无法互相访问的,保证了进程间数据的安全性

  • 一个进程中,只有部分功能是活跃的,所以只需要将进程中活跃的部分放入物理内存,避免物理内存的浪费

  • 当CPU需要访问数据时,首先是访问虚拟内存,然后通过虚拟内存去寻址,即可以理解为在表中找对应的物理地址,然后对相应的物理地址进行访问

  • 如果在访问时,虚拟地址的内容未加载到物理内存,会发生缺页异常(pagefault),将当前进程阻塞掉,此时需要先将数据载入到物理内存,然后再寻址,进行读取。这样就避免了内存浪费

page Fault 调试

打开Instruments,选择 System Trace
选择真机,选择工程,选择启动(启动前最好重启手机,清除缓存数据,确保是冷启动状态),当页面加载出来的时候,停止

二进制重排实践

二进制重排就是对即将生成的可执行文件重新排列,即它发生在链接阶段。
首先理解几个名词

  • Link Map:LinkmapiOS编译过程的中间产物,记录了二进制文件的布局,需要在Xcode的Build Settings里开启Write Link Map File,Link Map主要包含三部分:
    *Object Files 生成二进制用到的link单元路径和文件编号
    • Sections记录Mach-O每个Segment/section地址范围
    • Symbols 按顺序记录每个符号的地址范围
  • ld :ld 有一个参数叫 Order File,通过Build Settings -> Order File配置一个 后缀名 为order的文件路径。在这个order文件中,将你需要的符号按顺序写在里面。当工程build 的时候Xcode 会读取这个文件,打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach-O

如何获取启动运行的函数呢

  • hook objc_msgSend:我们知道,函数的本质是发送消息,在底层都会来到objc_msgSend,但是由于objc_msgSend参数是可变的,需要通过汇编获取,对开发人员要求较高。而且也只能拿到OC 和 swift中@objc 后的方法

  • 静态扫描:扫描 Mach-O特定段和节里面所存储的符号以及函数数据

  • Clang插桩:即批量hook,可以实现100%符号覆盖,即完全获取swift、OC、C、block函数

Clang插桩

llvm内置了一个简单的代码覆盖率检测(SanitizerCoverage)。它在函数级、基本块级和边缘级插入对用户定义函数的调用。我们这里的批量hook,就需要借助于SanitizerCoverage。流程如下:

  • 开启SanitizerCoverage
    • OC项目,需要在:在 Build Settings里的 “Other C Flags”中添加 -fsanitize-coverage=func,trace-pc-guard
    • 如果是Swift项目,还需要额外在 “Other Swift Flags”中加入-sanitize-coverage=func-sanitize=undefined
  • 重写方法
    • __sanitizer_cov_trace_pc_guard_init方法
      • 参数1start 是一个指针,指向无符号int类型4个字节,相当于一个数组的起始位置,即符号的起始位置(是从高位往低位读)
      • 参数2 stop,由于数据的地址是往下读的(即从高往低读,所以此时获取的地址并不是stop真正的地址,而是标记的最后的地址,读取stop时,由于stop占4个字节stop真实地址 = stop打印的地址-0x4)
        *__sanitizer_cov_trace_pc_guard方法
#include <stdint.h>
#include <stdio.h>
#include <sanitizer/coverage_interface.h>
#import <dlfcn.h>
#import <libkern/OSAtomic.h>
//定义原子队列
static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT;
//定义符号结构体
typedef struct{
    void *pc;
    void *next;
} SYNode;
/*
 - start:起始位置
 - stop:并不是最后一个符号的地址,而是整个符号表的最后一个地址,最后一个符号的地址=stop-4(因为是从高地址往低地址读取的,且stop是一个无符号int类型,占4个字节)。stop存储的值是符号的
 */
void __sanitizer_cov_trace_pc_guard_init(uint32_t *start,
                                                    uint32_t *stop) {
  static uint64_t N;  // Counter for the guards.
  if (start == stop || *start) return;  // Initialize only once.
  printf("INIT: %p %p\n", start, stop);
  for (uint32_t *x = start; x < stop; x++)
    *x = ++N;  // Guards should start from 1.
}
/*
 可以全面hook方法、函数、以及block调用,用于捕捉符号,是在多线程进行的,这个方法中只存储pc,以链表的形式
 
 - guard 是一个哨兵,告诉我们是第几个被调用的
 */
void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
  if (!*guard) return;  // Duplicate the guard check.
//获取PC
    /*
     - PC 当前函数返回上一个调用的地址
     - 0 当前这个函数地址,即当前函数的返回地址
     - 1 当前函数调用者的地址,即上一个函数的返回地址
    */
  void *PC = __builtin_return_address(0);
  char PcDescr[1024];
  //__sanitizer_symbolize_pc(PC, "%p %F %L", PcDescr, sizeof(PcDescr));
  printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);

 //创建结构体!
   SYNode * node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    
    //加入结构!
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
}
  • 获取所有符号并写入文件
{
    //定义数组
    NSMutableArray<NSString *> * symbolNames = [NSMutableArray array];
    
    while (YES) {//一次循环!也会被HOOK一次!!
       SYNode * node = OSAtomicDequeue(&symbolList, offsetof(SYNode, next));
        
        if (node == NULL) {
            break;
        }
        Dl_info info = {0};
        dladdr(node->pc, &info);
//        printf("%s \n",info.dli_sname);
        NSString * name = @(info.dli_sname);
        free(node);
        
        BOOL isObjc = [name hasPrefix:@"+["]||[name hasPrefix:@"-["];
        NSString * symbolName = isObjc ? name : [@"_" stringByAppendingString:name];
        //是否去重??
        [symbolNames addObject:symbolName];
        /*
        if ([name hasPrefix:@"+["]||[name hasPrefix:@"-["]) {
            //如果是OC方法名称直接存!
            [symbolNames addObject:name];
            continue;
        }
        //如果不是OC直接加个_存!
        [symbolNames addObject:[@"_" stringByAppendingString:name]];
         */
    }
    //反向数组
//    symbolNames = (NSMutableArray<NSString *>*)[[symbolNames reverseObjectEnumerator] allObjects];
    NSEnumerator * enumerator = [symbolNames reverseObjectEnumerator];
    
    //创建一个新数组
    NSMutableArray * funcs = [NSMutableArray arrayWithCapacity:symbolNames.count];
    NSString * name;
    //去重!
    while (name = [enumerator nextObject]) {
        if (![funcs containsObject:name]) {//数组中不包含name
            [funcs addObject:name];
        }
    }
    [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
    //数组转成字符串
    NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
    //字符串写入文件
    //文件路径
    NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"hank.order"];
    //文件内容
    NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
    [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
}

  • 拷贝文件,放入指定位置,并配置路径
    一般将该文件放入主项目路径下,并在Build Settings -> Order File中配置./hank.order

  • 注:Build Settings -> Other C Flags的如果配置的是-fsanitize-coverage=trace-pc-guard,没有func,在while循环部分会出现死循环,原因是while循环也会被__sanitizer_cov_trace_pc_guard中的guard哨兵检测到,通过汇编可以查看到流程

  • 第一次是bltouchBegin

    2251862-3ab80a2744e85668.jpg

  • 第二次bl是因为while 循环。 即 只要是跳转,就会被hook,即有bl、b的指令,就会被hook

    2251862-b09a285f136e195d.jpg

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

推荐阅读更多精彩内容