app冷启动三个阶段以及优化方案

2021-07-16  本文已影响0人  嘿_原来你也在这里

app冷启动概括为3大阶段:

1、动态链接库, 启动app时,dyld会装载app的可执行文件,同时会递归加载所有依赖的动态库,进行 rebase 指针调整和 bind 符号绑定 装载完毕会通知Runtime

2、runtime  1)调用map—images进行可执行文件的解析和处理 2)调用load——methods,调用所有class和category的load方法  3)进行objc结果的初始化  4)c++静态初始化器和attribute修饰的函数

3、main() 函数执行后

dylib loading time:

加载动态链接库所耗的时间(每个库本身都有依赖关系,苹果公司建议使用更少的动态库,并且建议在使用动态库的数量较多时,尽量将多个动态库进行合并。数量上,苹果公司建议最多使用 6 个非系统动态库。)

rebase/binding time:

ASLR:全称为 Address Space Layout Randomization,地址空间布局随机化,它将进程的某些内存空间地址进行随机化来增大入侵者预测目的地址的难度,从而降低进程被成功入侵的风险。

mach-o文件正式采用了ASLR技术,每次启动时mach-o内存地址不一样。

rebase time:修正偏移的时间  因为ASLR技术,每个函数的实际地址是mach-o地址+方法的偏移地址

binding time:当引用外部函数时,比如NSLog,在编译时无法得到其内存地址,因为它不在当前进程中,它存储在iOS系统的共享缓存空间中(Foundation)。所以调用外部函数时,iOS系统在你的可执行文件中添加一个符号,等到运行时,由系统去绑定符号,找到真正的外部函数

objC setup time: 

这一步主要做了以下操作

注册Objc类 (class registration)

把category的定义插入方法列表 (category registration)

保证每一个selector唯一 (selctor uniquing)

优化方法:

减少class(类),selector(选择子)以及category(分类)这类元数据的数量(使用AppCode分析未使用的代码,可以看出有大量优化空间)

减少C++虚函数数量

使用swift stuct(其实本质上就是为了减少符号的数量)

 initializer time:

以上三步属于静态调整,都是在修改——DATA segment中的内容,而这里则开始动态调整,开始堆栈中写入内容,在这里的工作有以下几点:

用+ initialize方法代替+load方法

使用 dispatch_one() pthread_once() std::once() 代替 C/C++ __ atribute__((constructor))(__ attribute__((constructor))用法解析

减少静态构造函数

不要在初始化方法中调用 dlopen(),对性能有影响。因为 dyld 在 App 开始前运行,由于此时是单线程运行所以系统会取消加锁,但 dlopen() 开启了多线程,系统不得不加锁,这就严重影响了性能,还可能会造成死锁以及产生未知的后果。所以也不要在初始化器中创建线程。

总结一下

APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库

并由runtime负责加载成objc定义的结构

所有初始化工作结束后,dyld就会调用main函数

接下来就是UIApplicationMain函数,AppDelegate的application:didFinishLaunchingWithOptions:方法

优化:

dyld阶段:

1)减少动态库,合并一些动态库(定期清理不必要的动态库)

2)减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)

3)+load() 方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize() 方法替换掉。因为,在一个 +load() 方法里,进行运行时方法替换操作会带来 4 毫秒的消耗。不要小看这 4 毫秒,积少成多,执行 +load() 方法对启动速度的影响会越来越大。

4)控制 C++ 全局变量的数量。

main() 函数执行后

main() 函数执行后的阶段,指的是从 main() 函数执行开始,到 appDelegate 的 didFinishLaunchingWithOptions 方法里首屏渲染相关方法执行完成。

首页的业务代码都是要在这个阶段,也就是首屏渲染前执行的,主要包括了:首屏初始化所需配置文件的读写操作;首屏列表大数据的读取;首屏渲染的大量计算等。

功能级别的启动优化

优化的思路是: main() 函数开始执行后到首屏渲染完成前只处理首屏相关的业务,其他非首屏业务的初始化、监听注册、配置文件读取等都放到首屏渲染完成后去做。

1)在不影响用户体验的前提下,尽可能将一些操作延迟。不要都放在finishLaunching中

2)按需加载

方法级别的启动优化

检查首屏渲染完成前主线程上有哪些耗时方法,将没必要的耗时方法滞后或者异步执行。通常情况下,耗时较长的方法主要发生在计算大量数据的情况下,具体的表现就是加载、编辑、存储图片和文件等资源。

对 App 启动速度的监控,主要有两种手段:

第一种方法是,定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时。

Xcode 工具套件里自带的 Time Profiler ,采用的就是这种方式。这种方式的优点是,开发类似工具成本不高,能够快速开发后集成到你的 App 中,以便在真实环境中进行检查。说到定时抓取,就会涉及到定时间隔的长短问题。定时间隔设置得长了,会漏掉一些方法,从而导致检查出来的耗时不精确;而定时间隔设置得短了,抓取堆栈这个方法本身调用过多也会影响整体耗时,导致结果不准确。这个定时间隔如果小于所有方法执行的时间(比如 0.002 秒),那么基本就能监控到所有方法。但这样做的话,整体的耗时时间就不够准确。一般将这个定时间隔设置为 0.01 秒。这样设置,对整体耗时的影响小,不过很多方法耗时就不精确了。但因为整体耗时的数据更加重要些,单个方法耗时精度不高也是可以接受的,所以这个设置也是没问题的。总结来说,定时抓取主线程调用栈的方式虽然精准度不够高,但也是够用的。

第二种方法是,对 objc_msgSend 方法进行 hook 来掌握所有方法的执行耗时。hook objc_msgSend可以查看Facebook 开源了一个库,这个库叫 fishhookfishhook底层原理直通车。

只靠 fishhook 就能够搞定 objc_msgSend 的 hook 了吗?当然还不够。我前面也说了,objc_msgSend 是用汇编语言实现的,所以我们还需要从汇编层面多加点料。具体耗时检测的完整代码可查看链接,在需要检测耗时时间的地方调用 [SMCallTrace start],结束时调用 stop 和 save 就可以打印出方法的调用层级和耗时了。你还可以设置最大深度和最小耗时检测,来过滤不需要看到的信息。了这样一个检查方法耗时的工具,你就可以在每个版本开发结束后执行一次检查,统计总耗时以及启动阶段每个方法的耗时,有针对性地观察启动速度慢的问题。如果你在线上做个灰度开关,还可以监控线上启动慢的一些特殊情况。

参考:

https://time.geekbang.org/column/article/85331

上一篇下一篇

猜你喜欢

热点阅读