一. 发动剖析:main阶段和pre-main阶段

1.1 概括

  • pre-main:发动之前,包括dylb加载,类的注册,符号绑定,load办法等
  • main之后:从main到第一个节面彻底显示,削减同步耗时操作,把一些能够延迟加载的操作,做成异步操作(多线程初始化,这儿需求对事务逻辑进行筛选,不是一切的事务都适;这儿发动时所需求用到的节面等,不要运用xib,xib会解析成代码加载,多了一步操作)
  • 所以,今日分享的首要是针对pre-main阶段的发动优化

1.2 pre-main阶段时刻剖析

  1. 检查发动时刻:Edit Scheme -Run – Arguments – Environment Variables 添加DYLD_PRINT_STATISTICS参数,值设置为1

iOS启动优化:二进制重排

  1. 各个部分阶段所花费的时刻剖析

iOS启动优化:二进制重排

dylib-loading:动态库加载时刻:除体系库以外,主张动态库数量在6个以下
rebase/binding: 批改偏移指针ASLR(为了安全,随机一个偏移指针,拜访内存运用 0xfff + ASLR),绑定外部符号
Objc settup:类注册时刻:
    这儿假如做优化,那么能够经过
        1. 削减OC的类来完成(20000个类,削减800ms),
        2. swift的功率要更高
        3. 要经常把弃用的类删去,例如事务实验运用今后,及时把不需求的无用类删去
initializer:load办法的时刻,所以咱们尽量防止在load中履行耗时操作
  1. 优化主张,在以往来说,很少有对premain阶段进行优化,一是因为没有太多有效的技能能对这个阶段进行优化,其次便是premain阶段即便优化,也是毫秒级别的优化,相比较事务层优化来说,优化幅度肯定是要远远不如的,可是从上一年起,这一块鼓起一个比较火的技能,运用二进制重排来针对premain阶段进行有效的优化,这方面的优化属于纯技能优化,不会对事务发生任何影响

二. 二进制重排

2.1 什么叫做二进制重排

  • 在介绍二进制重排之前,需求先了解一下虚拟内存以及内存的分页办理

  • 2.1.1 虚拟内存
    • 在程序开发过程中,对开发者而言,内存是连续的,这一块连续的内存实际上便是虚拟内存
    • 在早期的计算机中,程序是直接运行在物理内存上的,程序运行时会把其悉数加载到内存中,只要程序所需的内存不超过计算机剩余内存就不会出现问题。
    • 但因为程序是能够直接拜访物理内存的,这也带来了内存数据的不安全性
    • 为了确保不同运用程序之间内存数据的安全以及互不影响,添加运用功率
    • 后边在物理内存之上添加了一个中间层,让程序经过虚拟内存去直接拜访物理内存,这样对每个程序而言,内存都相当于被自己独占了,而且每个进程看到内存都是共同的
2.1.2 内存分页
  • 有了虚拟内存之后,每次程序的运行都会被悉数加载到虚拟内存中,但实际上,每次运行并不需求用不到程序的悉数逻辑,比如app发动时,实际上只需求掉用部分办法即可发动,其他不相干的逻辑并不需求
  • 为了进步内存运用率,才有了内存分页一说,把一段连续的虚拟内存地址组成的一页,映射到物理内存上对应的一页,这样只需求将少部分代码加载到虚拟内存中,而且映射到物理内存即可
  • 页面置换:当进程A,从虚拟内存页表中映射到物理内存当中,假如发现当前页没有在物理内存中找到,会发生缺页中止,操作体系阻塞当前进程,将磁盘中的进程A 对应的数据加载到内存中,而且将进程A的虚拟内存映射到物理内存;
  • 在加载进程A 对应的数据到内存中时:假如有物理内存中有空的,那么直接放 1 页 上去,假如内存现已满了,那么找一页去覆盖(找不活跃的进程去覆盖)
  • macos 4K = 1页, iOS=16K, 终端:PAGESIZE指令

iOS启动优化:二进制重排

iOS启动优化:二进制重排

2.1.3 二进制重排

举例:

假如我现在有一个书架,书架有很多层,每一层都放了很多本书,可是我想找一套书全集7本,碰巧这7本书因为某种原因放在不同的层,那么咱们假如需求找到这一套书,是不是就需求从7层挨个悉数找一下,最终才能得到一套;那么之后呢,咱们肯定就想着把哈利波特一套书放到固定某一层去,假如咱们经常要用到,咱们也可能会把这7本书本放到最显眼的一层,这样就能够一下子找到。

同理:

  • app在发动时就会从磁盘加载数据到内存中履行,
  • 假如发动时需求的办法分别在内存中不同的N页,那么就相当于需求把N页悉数映射到物理内存上,而且iOS体系在加载分页时还会做签名校验,这样就会大大添加了缺页中止的时刻(抖音给的数据:每一个缺页中止耗时0.6-0.8 ms)
  • 这些缺页中止在app运行时基本无法感知,可是当发动时,需求掉用很多办法的时分,跟着缺页中止数量的大大添加,就会发生一定的影响
  • 这个时分,咱们会想,那假如把发动时所需求用到的办法,悉数放在前面 M 页, 那么就能够大大削减缺页中止的次数,进步发动速度,这就时今日所介绍的二进制重排技能
2.1.4 检查符号次序的办法

1. iOS默许的分页次序时依照编译次序来的,也便是咱们在compiler sources中的文件次序

  1. 咱们能够经过Build Settings – Write Link Map File – YES 翻开设置
  2. 编译之后,经过文件途径找到 Demo1-LinkMap-normal-x86_64文件
  3. 能够发现,实际上的符号次序是和编译时文件次序符合的

iOS启动优化:二进制重排

iOS启动优化:二进制重排

iOS启动优化:二进制重排

2.2 二进制重排的方针

假定咱们N个文件当中,每一个文件都有一个func办法会在发动时进行掉用,假定这个办法名为func3,那么下图便是原始次序和方针次序,咱们把一切发动需求用到的符号办法悉数放到前面几页,来防止很多的缺页中止
例如现在发动时需求掉用20个办法,可是这20个办法分别在不同的分页,那么发动时就需求pagefault 20次,但实际上这20页中除了20个办法外的其他办法发动时都不需求,这时咱们把一切发动需求加载的办法,放在前面1或2或N页,这样加载的时分,只需求从前面2页加载,能够大大削减pagefault次数,优化发动时刻

iOS启动优化:二进制重排

检查缺页中止的次数
检查 pagefault:instrument -> system trace -> 输入 Main Thread过滤 ->summary virtual memory -> page in 

2.3 二进制重排的办法

  • 苹果官方供给了一个能够修正符号次序的办法,创立一个demo.order文件,在buildsetting – order file中装备order文件的途径,这样在打包时,会依照order file文件中的符号次序进行重排
  • 这个技能在oc750的源码中也能看到

iOS启动优化:二进制重排

  • 咱们能够手动创立一个order文件,然后写入自界说的符号次序,编译看一下结果
  • ps:order文件中即便编写的符号不存在,也不会报错的,能够定心写

iOS启动优化:二进制重排

iOS启动优化:二进制重排

address:代码的地址
size:代码大小,跟着代码改变会变动
file:代表在第几个文件
name:符号称号

2.4 问题

  • 二进制重排技能首要就这么多,但这儿有一个问题,咱们不行能去一行行去把一切发动时分用到的办法挨个敲一遍,咱们也没法知道发动时到底调用了哪些办法,所以这儿咱们就需求一个自动化的能够批量获取发动时调用的一切符号以及符号调用次序

三. 解决办法:自动化获取符号以及次序

3.1 先驱计划:

  • 运用fishhook去动态hook一切的办法,hook体系办法objc_msgsend 拿到第二个参数selector
  • 但时这种办法存在一定的问题,例如initiallize hook不到, 部分block hook不到,C++和swift也hook不到

3.2 终极计划:运用Clang进行编译器静态插桩

3.2.1 新增装备
  • other C flags : -fsanitize-coverage=func,trace-pc-guard
  • other swift flags: -sanitize-coverage=func
    • -sanitize=undefined
3.2.2 添加全局相应办法
  • __sanitizer_cov_trace_pc_guard_init

  • __sanitizer_cov_trace_pc_guard

    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) { // if (!*guard) return; // Duplicate the guard check. void *PC = __builtin_return_address(0);

    /// 进行符号搜集
    SYNode *node = malloc(sizeof(SYNode));
    *node = (SYNode){PC,NULL};
    //进入
    OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));
    /// 日志打印
    // 运用Dl_info,获取PC的相关信息
    Dl_info info;
    dladdr(PC, &info);
    printf("fname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n",
           info.dli_fname,
           info.dli_fbase,
           info.dli_sname,
           info.dli_saddr);
    

    }

3.2.3 针对办法是否能够hook悉数办法进行验证
  • 首要经过在__sanitizer_cov_trace_pc_guard办法中,添加log,判断出,改办法确实hook了项目中的一切办法
  • 添加initialize办法
  • 添加block等
  • 下图是:原始项目,添加了一个initialize办法,添加了block后的三次发动log比照
  • 阐明,改办法确实能够hook到一切的办法

iOS启动优化:二进制重排
iOS启动优化:二进制重排
iOS启动优化:二进制重排

3.2.4 办法中经过对__builtin_return_address进行处理,拿到被hook的办法的相关信息
  • 经过 Dl_info结构体拿到一切的函数信息和符号称号

    void *PC = __builtin_return_address(0);
    Dl_info info;
    dladdr(PC, &info);
    printf("\n-----------------------");
    printf("fname:%s \nfbase:%p \nsname:%s \nsaddr:%p\n",
           info.dli_fname,
           info.dli_fbase,
           info.dli_sname,
           info.dli_saddr);
    

ps:汇编当中办法A经过bl进行跳转办法B的时分,会把履行完之后下次履行的地址放到x30寄存器中,办法B中遇到ret指令时,会去x30寻找而且履行

  •   -(void)demo_func {
          // 履行clang静态注入的办法
          //__sanitizer_cov_trace_pc_guard
          // 履行完之后,回到demo_func继续改办法的后续履行
      }
    

    所以咱们能够相信__builtin_return_address办法回来的值,便是被hook的办法地址

  • 添加log后,咱们能够看到打印日志

sname:

+[Test2

load]

sname:``+[Test1 load]

sname:main

3.2.5 到这儿停止,咱们现已拿到了发动时所需求的一切办法,接下来只需求对这些信息进行过滤写入文件即可
  • 这儿需求留意的是,因为这个hook办法是有可能在子线程履行的,所以咱们做办法搜集的时分需求考虑到线程安全,

  • 这儿运用 OSQueueHead 搭配 OSAtomicEnqueue进行信息的搜集,当然也能够用其他办法

    //原子行列 static OSQueueHead symbolList = OS_ATOMIC_QUEUE_INIT; //界说符号结构体 typedef struct { void *pc; void *next; }SYNode;

    /// 进行符号搜集 SYNode *node = malloc(sizeof(SYNode)); *node = (SYNode){PC,NULL}; //进入 OSAtomicEnqueue(&symbolList, node, offsetof(SYNode, next));

  • 最终咱们能够在app的第一个界面进行viewdidappear的时分,履行一下详细解析的办法即可,拿到demo1.order文件

  • 之后能够删去相关的插桩装备,翻开order文件读取即可

iOS启动优化:二进制重排

四. 总结:

  • 之所以运用二进制重排对项目进行发动优化,首要仍是在于对于事务的无侵入

  • 就小说项目来说,优化幅度大概在100-200ms左右,详细看项目实际情况

  • 该办法能够适用于OC以及Swift项目均能够,其中假如需求对swift进行插桩,那么需求装备swift flags即可

    other swift flags: -sanitize-coverage=func -sanitize=undefined