App的启动速度对用户体验会有一定的影响,因此,为追求用户体验,启动速度的优化必然是App开发过程中,不可或缺的一个环节。那么我们需要先分析App在启动过程中都做了些什么事?
一、APP加载慢背后原理
1.虚拟内存和物理内存
早期计算机没有虚拟地址,一旦加载都会全部加载到内存中,而且进程都是按顺序排列的,这样别的进程只需要把自己的地址加一些就能访问到别的进程这样就很不安全。
现在软件发展的比硬件快,软件占用的内存越来越大,这就导致计算机的内存不够用,当开启多个软件时候,如果内存不够用就只能等待,只有等前面的软件关掉后才能加载打开,这就是早期计算机有时候为啥只有把前面的软件关掉才能打开新软件的原因。用户使用软件时候并不是使用到全部内存,只会使用到一部分,如果软件一打开就把软件全部加载到内存中,这样会很浪费内存空间。
基于上面原因虚拟内存技术出现了,软件打开后,软件自己以为有一大片内存空间,但实际上是虚拟的,而虚拟内存和物理内存是通过一张表来关联的
2.内存使用率问题
内存分页管理,映射表不能以字节为单位,是以页为单位。在应用加载时候不会把所有数据放内存中,因为数据是懒加载。
当进程访问虚拟地址时候,首先看页表,如果发现该页表数据为0,说明该页面数据未在物理地址上,这个时候系统会阻塞该进程,这个行为就叫做页中断(page Fault),也叫缺页异常,然后将磁盘中对应页面的数据加载到内存中,然后让虚拟内存指向刚加载的物理内存,将数据加载到内存中时候,如果有空的内存空间,就放空的内存空间中,如果没有的话,就会去覆盖其他进程的数据,具体怎么覆盖操作系统有一个算法,这样永远都会保证当前进程的使用,这就是灵活管理内存。
总结:
当我们向操作系统申请内存时,操作系统并不是直接分配给我们物理内存,而是只标记当前进程拥有该段内存,当真正使用这段内存时才会分配。这种延迟分配物理内存的方式就通过 page fault 机制来实现的。page fault 机制又会造成缺页异常,导致启动时间增加。
二、App启动分析
1.App启动分为 冷启动 和 热启动
冷启动:点击 App 启动前,它的进程不在系统里,需要系统新创建一个进程分配给它的情况。这是一次完整的启动过程
热启动:App 在冷启动后,用户将App 退到后台,即在App的进程还在系统里的情况下,用户重新启动进入 App 的过程,这个过程做的事情非常少,启动速度非常快。
因此,我们主要针对 App 冷启动进行优化。
一般而言,App 启动时间,指的是从用户点击 App 开始,到用户看到第一个界面之间的时间,总结来说:App 的启动主要包括三个阶段:
1.main() 函数执行前
2.main() 函数执行后
3.首屏渲染完成后
2.冷启动都做了什么?
- dylib loading:加载可执行文件(App 的.o 文件的集合),加载动态链接库
- rebase/binding:对动态链接库进行 rebase 指针调整和 bind 符号绑定;
- Objc setup:Objc 运行时的初始化处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;
- initializer:初始化,包括了执行 +load() 方法attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。
3.冷启动常规优化
减少动态库加载:每个库本身都有依赖关系,苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司建议最多使用 6 个非系统动态库。
减少加载启动后不会去使用的类或者方法。
+load()方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize()方法替换掉。因为,在一个 +load() 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗。不要小看这 4 毫秒,积少成多,执行 +load() 方法对启动速度的影响会越来越大。
控制 C++ 全局变量的数量。
当我们做了以上工作,对 pre-main 的时间有所优化之后,如果还想再进行优化,那就需要使用 LLVM 为我们提供的优化方式:二进制重排
三、System Trace调试
首先优化,要先学会调试,只有调试才能发现需要优化的地方
首先我们打开项目command + i打开Instruments调试工具
选择System Trace,这个软件可以看到我们项目中每个线程的数据
点击开始后这里我们搜索Main thread,选择我们的app,然后点击Main thread ,再到下面选择Main Thread –> Virtual Memory(虚拟内存)
这里面File Backed Page In就是page fault的次数。
当我们把APP杀死后里面再启动,结果发现File Backed Page In这个值变得很小,说明APP就算杀死后,在启动不是冷启动,还是有一部数据在系统的缓存中。
要做到真正的冷启动,我们可以把APP杀掉后启动多个手机里面的APP,然后再启动APP,发现File Backed Page In又变得很大。
说明虚拟内存是在系统中的,当系统内存不够的时候,其他APP会覆盖老的APP的虚拟内存。
二进制重拍是在链接阶段生成的,重排之后生成可执行文件,所以我们只能在编译阶段来优化,而无法对已生成的ipa进行优化。
四、二进制重排
1.重排原理
App启动时首先的调用的几个方法,会分布在虚拟内存的各个⻚面中, 执行这些方法时,需要从读取到物理内容中,就会产生多次 page fault
如果能将启动阶段需要的读取代码集中排布,将这些方法全都放到相邻的区域中,我们读取这些方法可能就只需要极少的 page fault 次数。可以减少不必要的 page fault 时间。达到优化启动时间的效果。
2.重排实现(流程复杂参考链接二进制重排实现)
苹果其实已经给我们提供了这个机制。
2.1 Order File
- 实际上 二进制重排就是对即将生成的可执行文件重新排列,即它发生在链接阶段。
- 首先,Xcode 用的链接器叫做 ld ,ld 有一个参数叫 Order File,我们可以通过这个参数配置一个 后缀名 为order的文件路径。在这个 order 文件中,将你需要的符号按顺序写在里面。当工程 build 的时候,Xcode 会读取这个文件,打的二进制包就会按照这个文件中的符号顺序进行生成对应的 mach-O。
2.2 查看项目符合顺序
可以设置 Write Link Map File 来设置是否输出,默认是 no。Link Map 是编译期间产生的 ,( ld 的读取二进制文件顺序默认是按照 Compile Sources 里的顺序 ),它记录了二进制文件的布局。
修改 Write Link Map File 为 YES,然后clean项目并重新编译
Products -> show in finder,上上层文件夹,然后找到一个xxxxx-LinkMap-normal-arm64.txt 的txt文件。
2.3 Clang插桩
2.4 补充
- swift / OC 混编工程问题
通过如上方式适合纯 OC 工程获取符号。由于 swift 的编译器前端是自己的 swift 编译前端程序,因此配置稍有不同。搜索 Other Swift Flags,添加两条配置即可:-sanitize-coverage=func、 -sanitize=undefined。swift类同样可以通过这个方式获取。
- cocoapod 工程问题
cocoapod 工程引入的库,会产生多 target,我们在主target添加的配置是不会生效的,我们需要针对需要的target做对应的设置。
对于直接手动导入到工程里的 sdk,不管是 静态库 .a 还是 动态库,会默认使用主工程的设置,也就是可以拿到符号的。
- 本文作者: Grx
- 本文链接: https://ruixiaoguo.github.io/Grx.github.io/Grx.github.io/2021/06/30/iOS启动优化之二进制重排/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!