App启动优化

极客时间戴铭学习笔记

App启动干了什么

一般分为冷启动和热启动两种
冷启动指, App点击启动前, 进程不在系统里, 需要系统新创建一个进程分配给该App, 这是一个完整的启动过程
热启动指, App在冷启动后用户将app退到后台, 在App的进程还在系统里用户重新进入的过程

冷启动阶段

三个阶段: main函数执行前、main之后、首屏渲染完成后

main()执行前

加载可执行文件(App的.o文件的集合)
加载动态链接库, 进行rebase指针调整和bind符号绑定
Objc运行时的初始化处理, 包括Objc相关类的注册、category注册、selector唯一性检查等
初始化, 包括了执行+load()方法、attribute((constructor))修饰的函数调用、创建C++静态全局变量

相应优化方法

减少动态库加载. 每个库本身都有依赖关系, 苹果公司建议使用更少的动态库, 并且建议在使用动态库的数量较多时, 尽量将多个动态库进行合并. 数量上, 最多可以支持6个非系统动态库合并为一个
减少加载启动后不会去使用的类或方法
+load()方法里的内容可以放到首屏渲染完成后再执行, 或者用+initialize()方法替换掉, 因为, 在一个+load()方法里,进行运行时方法替换操作会带来4毫秒的消耗
控制C++全局变量的数量

main()函数执行后

main()函数执行后的阶段, 指从main()函数执行开始, 到appDelegate的didFinishLaunchingWithOptions方法里首屏渲染执行完成
首页的业务代码都是在这个阶段, 也就是首屏渲染前执行的, 主要包括了:
首屏初始化所需配置文件的读写操作;
首屏列表大数据的读取;
首屏渲染的大量计算等.

相应优化方法

从功能上保留首屏渲染必要的初始化功能, 启动必要的初始化功能,
其他使用时才需要初始化的可以优化到之后的时间里执行

首屏渲染完成后

非首屏其他业务服务模块的初始化、监听的注册、配置文件的读取.
从函数上看, 这个阶段指的是截止到didFinishLaunchingWithOptions方法作用域内执行首屏渲染之后的所有方法执行完成.
这个优化的优先级排在最后

功能级别的启动优化

跟前面有点重复, main()函数开始执行后到首屏渲染完成前只处理首屏相关的业务, 其他非首屏业务的初始化、监听注册、配置文件读取等放到首屏渲染完成后去做

方法级别的启动优化

某些方法大量调用会增加耗时.

监控启动速度两种方案

  1. 定时抓取主线程上的方法调用堆栈, 计算一段时间里各个方法的耗时,比如Time Profiler
  2. 对objc_msgSend方法进行hook来掌握所有方法的执行耗时, 通过监听所有的Objective-C里方法执行来实现
    objc_msgSend方法执行的逻辑是: 先获取对象对应类的信息, 再获取方法的缓存, 根据方法的selector查找函数指针, 经过异常错误处理后, 最后跳到对应函数的实现
    使用fishhook的方法来实现

基于静态库插桩的二进制重排启动优化 (手淘的最新实践)

  1. App 启动时PageFault的性能分析
  2. 静态库插桩重排方案的技术原理

App启动和PageFault

当我们向操作系统申请内存时, 操作系统并不是直接分配给我们物理内存, 而是只标记当前进程拥有该段内存, 当真正使用这段内存时才会分配. 这种延迟分配物理内存的方式就通过page fault机制来实现的. 当我们访问一个内存地址时, 如果该地址非法, 或者我们对其没有访问权限, 或者该地址对应的物理内存还未分配, cpu都会生成一个page fault, 进而执行操作系统的page fault handler. 如果是因为还未分配物理内存, 操作系统会立即分配物理内存给当前进程, 然后重试产生这个page fault的内存访问指令
来自淘系技术官方

App在启动时, 需要执行各种函数, 我们需要读取TEXT段代码到物理内存中, 这个过程会发生缺页中断, 由于启动时所需要执行的代码分布在TEXT段的各个部分, 会读取很多页面, 导致启动时Page Fault数量非常多. 与直接访问物理内存不同, page fault过程大部分由软件完成, 消耗时间比较久, 所以是影响启动性能的一个关键指标.
手淘启动时首先调用的几个方法会分布在虚拟内存的各个页面中, 执行这些方法时, 需要从读取到物理内存中, 就会产生多次page fault.
如果能将启动阶段需要的读取代码集中排布, 将这些方法全都放到相邻的区域中, 我们读取这些方法可能就只需要极少的page fault次数. 可以减少不必要的page fault时间.达到优化启动时间的效果.
重排前后的函数在页面的布局对比


来自淘系技术官方

重排方案

如何获取方法的执行顺序

为了生存order_file, 我们需要确定应用启动时方法的执行顺序.
抖音通过静态扫描和运行时Trace等方法确定order_file, 该方案无法覆盖initialize、block和C++通过寄存器的间接函数调用
Facebook分享过通过llvm插桩的确定order_file的方案, 需要使用源码重新打包.

静态库插桩

我们编译过的静态库由.o文件组成, 我们可以对.o中的函数代码进行修改, 在每个函数的开头插入调用我们指定记录函数的指令

生成order file

linkmap记录了连接过程中的相关信息. 其中包含链接用到的symbol相关信息.通过pc address减去slide得到的地址, 我们可以在linkmap中找到对应的symbol

address = pc - slide. // 因为ASLR, APP 可执行文件随机载入的原因,需要处理一下偏移
量。
我们需要将之前记录的地址转换成对应的符号, 为了真实还原线上的执行环境, 在app中简单的记录了pc地址和image的偏移量. 通过解析linkmap, 获取函数的地址区间, 得到距离address最近的symbol, 生成order_file

更改符号的排列顺序

默认情况下, Id链接器会按照链接的顺序将各个.o文件的数据重新布局生成可执行文件.Id链接器提供-order-file选项操控数据排列的顺序.在Xcode中可以通过Order File选项指定符号排序文件.

重排之后效果很不错

推荐阅读更多精彩内容