App启动优化

  • App的启动过程

    App的启动通常是指从用户点击App开始到AppDelegatedidFinishLaunching方法执行完成为止,通常又将启动分为冷启动和热启动。html

    • 冷启动
      冷启动: 是指App启动前它的进程不在系统里,须要系统分配一个进程给它启动的状况,这是一次完成的启动(通常启动优化都是优化冷启动的过程)
    • 热启动
      热启动: 是指App在冷启动后将App退后台,App的进程还在系统里,内存中海油App的数据的状况下,再次启动App的过程,这个过程作的事情也很是少
  • App启动优化

    上文也说了通常启动优化主要优化的是冷启动的过程,热启动作的事情也很是少。因此这里只讲解冷启动过程的优化。冷启动过程又被分为main函数执行以前和main函数执行以后node

    • main函数执行以前
      操做系统加载App可执行文件到内存,执行一系列的加载&连接工做,能够经过添加添加环境变量DYLD_PRINT_STATISTICS来查看main函数执行以前都作了什么,同时也能够看出对应消耗的时间 image.png 不难发现main函数执行以前主要作了如下几种事情
      • 动态库的加载
        对应的是dylib loading time能够发现加载时间为48.41毫秒
        优化建议 :
        这里主要的优化建议是减小动态库的加载,苹果公司建议更少的使用动态库,而且建议动态库的数量较多的时候,尽可能将多个动态库合并,数量上苹果公司最多支持6个非系统动态库的合并
      • 偏移修正和符号绑定
        对应的是rebase/binding time,耗时9.18毫秒
        • 偏移修正
          任何App生成的二进制文件中的方法、函数都会有个地址,而这个地址是相对于当前二进制文件中的偏移地址,可是到了运行时系统会随机生成一个数值添加到二进制文件的头部(ASLR安全机制下文中会有讲解),因此此时函数、方法的地址就是 随机分配的数值+偏移地址 这个过程就是偏移修正
        • 符号绑定
          动态库不像是静态库,静态库实在编译时期就将对应使用到的代码一块儿打包生成了mach-o文件,因此此时使用到的静态库的方法、函数其实就和自定义的方法、函数差很少了,可以直接获取到对应的地址,可是动态库在编译阶段是不会被打包进mach-o文件的,可是此时又用到了动态库中的方法,例如用到了NSLog方法,此时就会生成一个!NSLog 符号此时这个符号会随机指向一个地址,当运行时,此时动态库被加载到内存,此时就能够拿到动态库对应的方法、函数的地址,因此此时就须要将!NSLog这个符号绑定到相应的地址上去(dyld作的),这个过程就叫作符号绑定
      • 类的注册
        对应的是ObjC setup time,耗时10.86毫秒
        优化建议
        删除启动后不会去使用的类
      • 执行load和构造函数
        对应的是initializer time,耗时110.79毫秒
        优化建议
        减小使用load方法相应的能够将load中的实现放在+initialize()方法中去,应为通常一个load方法的执行须要耗时4毫秒,并且若是类中实现了load那么相对应类的加载就要提早到read_image方法中去执行,若是没有实现load类的加载则会方法第一次发送消息的时候加载,
    • main函数执行以后
      这个阶段主要是指main函数执行开始到首屏渲染完成方法执行完毕。 这个阶段主要作的工做包括:
      • 第三方SDK初始化
      • 自定义工具类初始化
      • 首屏数据的加载
      • 首屏渲染的一些计算
      这个地方的优化建议主要有一下几点
      1. 只处理首屏渲染相关的任务,其余非首屏的业务例如初始化、注册监听、配置文件的读取等等都放在首页渲染完成以后去作,固然也能够开辟一个线程去处理这些事情。尽可能不要占用主线程
      2. 本身的业务逻辑的优化,已经废弃的不须要用的逻辑代码、方法、函数都删除掉,减小每一个流程的耗时
      3. 启动时期的页面尽可能避免使用xib、storyboard(中间会有个转换的过程也是须要耗时的)UI的主框架尽可能使用纯代码
  • 二进制重排基础知识

    上文主要是针对特定的阶段作一些优化处理,除了删除的优化方案还有一种优化,就是二进制重排,在讲解二进制重排以前先将几个概念性的东西:ios

    1. 物理内存
      就是运行内存,是指计算机上安装的内存,通俗的将其实就是内存条的大小。
      早期的操做系统没有虚拟内存,程序寻址用的都是物理地址,因此没启动一个程序开辟一个进程都要相应的分配一段物理内存给这个程序,这就形成了以下几个问题:
      1. 当物理内存被分配完成的时候此时其余程序就不能再被加载到内存(也就是不能运行),此时就须要等待其余程序退出释放内存,此时才能运行新的程序
      2. 程序指令都是在物理内存上操做的,那么我这个进程就能够修改其余进程的数据,甚至会修改内核地址空间的数据
      针对以上的问题也就引出了虚拟内存
    2. 虚拟内存
      指的是把硬盘中的一部分空间用来当作内存使用
      进程和物理内存之间增长一个中间层,这个中间层就是所谓的虚拟内存,主要用于解决当多个进程同时存在时,对物理内存的管理。提升了CPU的利用率,使多个进程能够同时、按需加载。因此虚拟内存其本质就是一张虚拟地址和物理地址对应关系的映射表.每一个进程都有一个独立的虚拟内存,其地址都是从0开始,大小是4G固定的。
      进程开始要访问一个地址,它可能会经历下面的过程:
      1. 每次我要访问地址空间上的某一个地址,可是进程间是没法互相访问的,保证了进程间数据的安全(一个进程只能访问给定的这篇虚拟内存的地址)。都须要把地址翻译为实际物理内存地址
      2. 全部进程共享这整一块物理内存,每一个进程只把本身目前须要的虚拟地址空间映射到物理内存上
      3. 每一个虚拟内存会划分一个一个页存储(页的大小在iOS中是16K,其余的是4K),进程须要知道哪些地址空间上的数据在物理内存上,哪些不在(可能这部分存储在磁盘上),还有在物理内存上的哪里,这就须要经过页表来记录
      4. 页表的每个表项分两部分,第一部分记录此页是否在物理内存上,第二部分记录物理内存页的地址(若是在的话)
      5. 当进程访问某个虚拟地址的时候,就会先去看页表,若是发现对应的数据不在物理内存上,就会发生缺页异常
      6. 缺页异常的处理过程,操做系统当即阻塞该进程,并将硬盘里对应的页换入内存,而后使该进程就绪,若是内存已经满了,没有空地方了,那就找一个页覆盖,至于具体覆盖的哪一个页,就须要看操做系统的页面置换算法是怎么设计的了。
      以下图所示,虚拟内存与物理内存间的关系 未命名文件(34).jpg 若是物理内存被占满,此时又有新的页须要被加载进来,此时新页就会吧长时间没有使用的页覆盖掉
    3. ASLR
      应为虚拟内存的起始地址与大小都是固定的,这意味着,当咱们访问时,其数据的地址也是固定的,这会致使咱们的数据很是容易被破解,为了解决这个问题,因此苹果为了解决这个问题,在iOS4.3开始引入了ASLR技术,其实现原理就是在虚拟内存的头部随机加上一块地址,这样每次启动时虚拟地址的其实址就不同,因此在程序启动的时候须要作偏移修正。
  • 二进制重排缘由

    从上文的知识中能够知道,ios程序在加载到虚拟内存的时候会被分红不少不少页,若是此时访问的虚拟地址的一个page,对应的物理地址不存在,则会缺页异常,此时会阻塞进程将这一页加载到物理内存而后在访问。这里能够经过instrumentsSystem Trace来查看你的项目的缺页异常的数量以下: image.png 步骤:先点击启动->首页加载完成后暂停->而后找到你的项目找到主线程 image.png发现启动以前有两百多个缺页异常,此时咱们再看项目在编译时期的默认排列顺序,此时咱们写一个简单的demo以下图: image.png就是写了几个简单的方法,而后项目中选择Build-setting搜索link map而后配置image.png此时会发现对应配置的文件夹中生成了对应的link-map文件,image.png发现方法、函数等都是按照在文件中的实现顺序来的,而文件的顺序是按照comple source中的顺序来的如图: image.png这种状况就形成了每一个页有可能只有一个方法是有用的,其余方法、函数等都不是在启动阶段调用的,这就形成了在启动时期缺页异常的数量会不少,也就形成了启动时间变长的状况。这也就是须要进行二进制重排的缘由算法

  • 二进制重排原理

    上文分析了二进制重排的缘由,就是应为页中空间的浪费没有充分利用每一个页的空间形成缺页异常数量增多,二进制重排的原理其实就是将启动阶段用到的方法、函数所有排在最前面,这样就能充分利用每一个页的空间,与此同时也下降了缺页异常的数量。以下图所示: 未命名文件(35).jpg
    明显减小了一大半的缺页异常的数量swift

  • 二进制重排实践

    经过上面的原理分析能够知道,若是作二进制重排只须要改变编译时期方法、函数等的排列顺序就行。其本质就是就是对启动加载的符号进行从新排列数组

    • 修改排列顺序的方法
      Xcode是用的连接器叫作ld,ld有一个参数叫Order File , 咱们能够经过这个参数配置一个 order文件的路径 .
      咱们能够经过在Build Settings -> Order File配置一个后缀为order的文件路径。在这个order文件中,将所须要的符号按照顺序写在里面,在项目编译时,会按照这个文件的顺序进行加载,以此来达到咱们的优化,因此二进制重排的关键点在于Order File文件的生成
    • 获取Order File文件的方法
      1. 若是项目不大的状况下本身也能够根据项目本身找到启动阶段要运行的方法、函数,本身编写Order File文件。
      2. hook objc_msgSend,可是因为objc_msgSend的参数是可变的,须要经过汇编获取,使用门槛比较高。并且也只能拿到OC和swift中@objc后的方法
      3. 静态扫描:扫描 Mach-O 特定段和节里面所存储的符号以及函数数据
      4. Clang插桩:即批量hook,能够实现100%符号覆盖,即彻底获取swift、OC、C、block函数
    • Clang插桩
      llvm内置了一个简单的代码覆盖率检测(SanitizerCoverage)。它在函数级、基本块级和边缘级插入对用户定义函数的调用,相应文档
      具体步骤:
      1. 配置开启SanitizerCoverage,在build setting中搜索Other C Flags,以下图image.png若是是OC项目则添加-fsanitize-coverage=func,trace-pc-guard,若是是swift项目则添加-sanitize-coverage=func-sanitize=undefined
      2. 添加hook方法
        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.
           }
        
           void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
             //guard 是一个哨兵,告诉咱们是第几个被调用的
             // 这个地方 是过滤掉了load方法,因此这里须要注释掉
             if (!*guard) return;
               /*
                - PC 当前函数返回上一个调用的地址
                - 0 当前这个函数地址,即当前函数的返回地址
                - 1 当前函数调用者的地址,即上一个函数的返回地址
               */
             void *PC = __builtin_return_address(0);
             char PcDescr[1024];
             printf("guard: %p %x PC %s\n", guard, *guard, PcDescr);
           }
        
        复制代码
        主要的方法在于__sanitizer_cov_trace_pc_guard方法,在这里咱们能够取到对应方法的地址,为何方法执行以前会先调用__sanitizer_cov_trace_pc_guard方法呢,可经过断点调试查看,在一个方法或者函数的起始处大断点,再看汇编代码以下图: image.png image.png 发如今方法执行以前插入了__sanitizer_cov_trace_pc_guard方法,全部的函数执行都会限制性__sanitizer_cov_trace_pc_guard方法,在block前面也打个断点发现 image.pngblock执行前也会被插入__sanitizer_cov_trace_pc_guard方法,继续查看swift-oc混编是swift方法是否会被hook image.png image.png也会被hook,因此也验证了clang插桩的方法能覆盖全部方法、函数。
      3. 获取符号 上述hook方法中咱们知道能够拿到当前方法或者函数的地址,拿到地址以后咱们能够经过dladdr方法去除对应方法或者函数的信息具体代码以下图: image.png image.png image.png 发现dli_sname就是咱们想要的符号,接下来的操做主要就是把这些符号存储下来而后生成order而后工程再配置对应的Order file就算完成了。
      4. 输出order文件
        上文中已经能够拿到符号了,最后的工做就是输出order文件。
        具体思路:咱们能够在__sanitizer_cov_trace_pc_guard将函数地址信息存储下来而后给app添加一个点击屏幕的监听事件,等到首屏加载完毕说明启动完成全部所须要加载的方法也就加载完成,此时咱们再在这个方法遍历地址信息,输出符号。
        我这里借用的链表存储,因此先要创建一个节点以下图: image.png 而后再经过OSQueueHead建立原子队列,其目的是保证读写安全。
        image.png 经过OSAtomicEnqueue方法将node入队,经过链表的next指针能够访问下一个符号 image.png 此刻地址的储存完成下一步就是读取写入order文件:具体代码以下
        -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
        {
            //定义数组
            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:@"-["];
                //须要注意若是不是OC方法须要添加下划线
                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];
                }
            }
            [funcs removeObject:[NSString stringWithFormat:@"%s",__FUNCTION__]];
            //数组转成字符串
            NSString * funcStr = [funcs componentsJoinedByString:@"\n"];
            //字符串写入文件
            //文件路径
            NSString * filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tudou.order"];
            //文件内容
            NSData * fileContents = [funcStr dataUsingEncoding:NSUTF8StringEncoding];
            [[NSFileManager defaultManager] createFileAtPath:filePath contents:fileContents attributes:nil];
        }
        复制代码
        运行完成发现生成了order文件 image.png
      5. Xcode配置order文件 如图配置文件 image.png
      6. 查看二进制重排结果 最后一样的查看生成的link map文件:
        没有二进制重排以前: image.png 发现是按照文件按照方法的顺序来的。
        二进制重排以后:
        image.png 发现此时就是按照咱们的order文件的顺序来的
相关文章
相关标签/搜索