首先我们需要知道iOS在启动会发生什么?
启动优化时间段
在苹果官方,将app的启动时间分为两个阶段
T1: pre-main 阶段,即main()函数之前,操作系统加载app可执行文件到内存中,然后执行一系列的加载&链接工作,最后执行到App的main() 函数
即我们常说的加载Mach-O文件的过程
T2: main()函数之后,即从main()开始,到appDelegate的didFinishLaunchingWithOptions方法执行完毕前这段时间,主要是构建第一个界面,并完成渲染。
而,从用户点击App图标开始到用户能看到App主界面内容为止这个过程,即T1+T2。
接下来简单聊聊分别针对这两个时间段我们能做哪些优化
pre-main 阶段优化
通过上图,我们可以看到,pre-main阶段,app主要做的是dyld加载操作, 其次代码区主要在做init和+load函数的事情
首先来看下加载Mach-O.
我们可以通过xcode打印时间来查看,哪些时间是占用比较多的
检测方法
获得 main() 方法执行前的耗时比较简单,通过 Xcode 自带的测量方法既可以。将 Xcode 中 Product -> Scheme -> Edit scheme -> Run -> Environment Variables 将环境变量 DYLD_PRINT_STATISTICS 或 DYLD_PRINT_STATISTICS_DETAILS 设为 1 即可获得执行每项耗时:
// example
// DYLD_PRINT_STATISTICS
Total pre-main time: 383.50 milliseconds (100.0%)
dylib loading time: 254.02 milliseconds (66.2%)
rebase/binding time: 20.88 milliseconds (5.4%)
ObjC setup time: 29.33 milliseconds (7.6%)
initializer time: 79.15 milliseconds (20.6%)
slowest intializers :
libSystem.B.dylib : 8.06 milliseconds (2.1%)
libMainThreadChecker.dylib : 22.19 milliseconds (5.7%)
AFNetworking : 11.66 milliseconds (3.0%)
TestDemo : 38.19 milliseconds (9.9%)
// DYLD_PRINT_STATISTICS_DETAILS
total time: 614.71 milliseconds (100.0%)
total images loaded: 401 (380 from dyld shared cache)
total segments mapped: 77, into 1785 pages with 252 pages pre-fetched
total images loading time: 337.21 milliseconds (54.8%)
total load time in ObjC: 12.81 milliseconds (2.0%)
total debugger pause time: 307.99 milliseconds (50.1%)
total dtrace DOF registration time: 0.07 milliseconds (0.0%)
total rebase fixups: 152,438
total rebase fixups time: 2.23 milliseconds (0.3%)
total binding fixups: 496,288
total binding fixups time: 218.03 milliseconds (35.4%)
total weak binding fixups time: 0.75 milliseconds (0.1%)
total redo shared cached bindings time: 221.37 milliseconds (36.0%)
total bindings lazily fixed up: 0 of 0
total time in initializers and ObjC +load: 43.56 milliseconds (7.0%)
libSystem.B.dylib : 3.67 milliseconds (0.5%)
libBacktraceRecording.dylib : 3.41 milliseconds (0.5%)
libMainThreadChecker.dylib : 21.19 milliseconds (3.4%)
AFNetworking : 10.89 milliseconds (1.7%)
TestDemo : 2.37 milliseconds (0.3%)
total symbol trie searches: 1267474
total symbol table binary searches: 0
total images defining weak symbols: 34
total images using weak symbols: 97
看完时间后我们再来具体看看做了些什么? 以及我们在这个过程怎么优化点.
加载可执行文件
加载 Mach-O 格式文件,既 App 中所有类编译后生成的格式为 .o 的目标文件集合。
dyld 加载 dylib 会完成如下步骤:
分析 App 依赖的所有 dylib。
找到 dylib 对应的 Mach-O 文件。
打开、读取这些 Mach-O 文件,并验证其有效性。
在系统内核中注册代码签名。
对 dylib 的每一个 segment 调用 mmap()。
系统依赖的动态库由于被优化过,可以较快的加载完成,而开发者引入的动态库需要耗时较久。
优化点:
1.减少动态库数量可以加减少启动闭包创建和加载动态库阶段的耗时,官方建议动态库数量小于 6 个。
- 推荐的方式是动态库转静态库,因为还能额外减少包大小。另外一个方式是合并动态库,但实践下来可操作性不大。最后一点要提的是,不要链接那些用不到的库(包括系统),因为会拖慢创建闭包的速度。
Rebase和Bind操作
由于使用了ASLR 技术,在 dylib 加载过程中,需要计算指针偏移得到正确的资源地址。 Rebase 将镜像读入内存,修正镜像内部的指针,消耗 IO 性能;Bind 查询符号表,进行外部镜像的绑定,需要大量 CPU 计算。
优化点:
下线代码可以减少 Rebase & Bind & Runtime 初始化的耗时。那么如何找到用不到的代码,然后把这些代码下线呢?可以分为静态扫描和线上统计两种方式。我们可以定期扫描一下项目里面的无用方法,这里需要将方法找出来,然后推进大家负责的模块,去删除.
Objc setup, Initializers, 加载资源文件
进行 Objc 的初始化,包括注册 Objc 类、检测 selector 唯一性、插入分类方法等。
往应用的堆栈中写入内容,包括执行 +load 方法、调用 C/C++ 中的构造器函数(用 attribute((constructor)) 修饰的函数)、创建非基本类型的 C++ 静态全局变量等。
优化点:
+load 迁移
+load 除了方法本身的耗时,还会引起大量 Page In,另外 +load 的存在对 App 稳定性也是冲击,因为 Crash 了捕获不到。
合并功能类似的类和扩展(Category)
由于Category的实现原理,和ObjC的动态绑定有很强的关系,所以实际上类的扩展是比较占用启动时间的。尽量合并一些扩展,会对启动有一定的优化作用。不过个人认为也不能因为它占用启动时间而去逃避使用扩展,毕竟程序员的时间比CPU的时间值钱,这里只是强调要合并一些在工程、架构上没有太大意义的扩展。
通过减少IO操作量级优化 - 压缩资源图片
压缩图片为什么能加快启动速度呢?因为启动的时候大大小小的图片加载个十来二十个是很正常的,图片小了,IO操作量就小了,启动当然就会快了。
其次因为我的项目是OC-Swift的,由于OC有运行时,会比较耽误时间(各种runtime消息转发), 我们在把OC的类,尽量重写到Swift去,因为Swift静态语言的原因,编译完成就可以检测出没有调用的函数,优化删除后是可以减少二进制文件大小的,从而直接从体积上减少了加载时间. 建议大家在发版后的空闲时期,可以来做一些重构的工作
mian时间段的优化
main阶段的启动优化, 在main()函数之后的didFinishLaunchingWithOptions方法里执行了各种业务,有很多业务不是一定要在这里执行,我们可以延迟加载,防止影响启动时间。
在didFinishLaunchingWithOptions方法里我们一般做一下逻辑:
初始化第三方sdk
配置App运行需要的环境
自己的一些工具类的初始化 等等
Application Initializaiton
这个阶段主要是生命周期方法的回调,也正是开发者最熟悉的部分。
调用 UIApplicationDelegate 的 App 生命周期方法:
application:willFinishLaunchingWithOptions:
application:didFinishLaunchingWithOptions:
在这个阶段,开发者可以做的优化:
推迟和启动时无关的工作
Senens 之间共享资源
Fisrt Frame Render
这个阶段主要做了创建、布局和绘制视图的工作,并把准备好的第一帧提交给渲染层渲染。会频繁调用以下几个函数:
loadView
viewDidLoad
layoutSubviews
我们在闪屏期间需要多关注首页的这些方法加载时间,提前加载,或者异步处理耗时操作,从而来优化启动时间
最后, 聊一下怎么检测你的优化成功
使用 Instruments 分析和优化 App 启动过程
当知道如何优化之后,我们需要针对我们的启动过程进行分析。Xcode 11 的 Instruments 为此新增了一个 App launch 模板,让开发者可以更好的分析自己 App 的启动速度。
运行后可以看到各个阶段的具体时间,根据数据进行优化,还能看到耗时的函数调用。
以上是自己在工作中总结的一些方法,希望对大家有用.
参考资料:
深入探索 iOS 启动速度优化
https://juejin.cn/post/6844904127068110862
高德 APP 启动耗时剖析与优化实践(iOS 篇)
https://www.infoq.cn/article/xjb3cysclphv5sh5923q
抖音品质建设 - iOS启动优化《原理篇》
https://mp.weixin.qq.com/s/3-Sbqe9gxdV6eI1f435BDg
抖音品质建设 - iOS启动优化《实战篇》
https://mp.weixin.qq.com/s/ekXfFu4-rmZpHwzFuKiLXw
iOS 优化篇 - 启动优化之Clang插桩实现二进制重排
https://mp.weixin.qq.com/s/UlMAvuLuTcWgd3qkEAHYMA
今日头条 iOS 客户端启动速度优化
https://juejin.cn/post/6844903649416577037
马蜂窝 iOS App 启动治理:回归用户体验
https://juejin.cn/post/6844903841410842638