iOS启动优化

#热启动与冷启动

  • 冷启动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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值