极客时间 | App 启动速度怎么做优化与监控?| 读书笔记
戴老师上面讲的如果是生活中使用高频,短暂的应用,如果启动时间太长,确实会丢失掉特别多的用户. 当然对于国内想增加开屏广告之类的应用来说, 此处优化倒是没有那么迫切,但身为程序员还对要对这方面要多一些了解, 以备不时之需.
--- 首先贴出极客时间原文地址 ----
02 | App 启动速度怎么做优化与监控?
戴老师上面讲的如果是生活中使用高频,短暂的应用,如果启动时间太长,确实会丢失掉特别多的用户. 当然对于国内想增加开屏广告之类的应用来说, 此处优化倒是没有那么迫切,但身为程序员还对要对这方面要多一些了解, 以备不时之需. 当然为了避免付费课程的版权问题,这里只是对原文涉及到的部分知识点进行总结, 不会原文粘贴. 同时我为了避免一家之言,查找了多家对于 应用启动有看法的文章, 详情见 参考, 当然最有说服力的仍然是 Optimizing App Startup Time - WWDC2016 独家了.
应用启动
首先来讲,应用启动这个过程,它分为两类
- 热启动 :应用进程已经启动,只是从后台到应用前台的切换启动过程.
- 冷启动 : 指的是应用第一次创建应用进程,进驻系统内运行.
热启动阶段
大部分应用这个过程时长相差不大. 是否还有优化空间, 应该也有,比如进入前台,一般有些应用会进行数据更新等操作, 只要不阻塞主线程就好.
冷启动阶段
此过程一般因业务不同,时长差异会较大, 而大部分优化也是从这里着手.
它主要分为3个阶段
- 进入
main
方法之前 - 执行
main
方法之后 - 用户能首次看到应用界面的时间点
main 方法之前
说这个的时候, 先来一张图, 很多博客都有这里, 不知道来源到底是哪里, 不过总之对于 应用启动这回事, 这个图很有专家范, 暂时先放上这个图, 如有侵权,请联系删除.
dyld: Dynamic Linking On OS X
这篇文章对这个过程有着不错的解释. 孙源大神翻译如下
从 kernel 留下的原始调用栈引导和启动自己
将程序依赖的动态链接库递归加载进内存,当然这里有缓存机制
non-lazy 符号立即 link 到可执行文件,lazy 的存表里
Runs static initializers for the executable
找到可执行文件的 main 函数,准备参数并调用
程序执行中负责绑定 lazy 符号、提供 runtime dynamic loading services、提供调试器接口
程序main函数 return 后执行 static terminator
某些场景下 main 函数结束后调 libSystem 的 _exit 函数
简化了解这个过程也是类似 戴铭老师 说的那4个过程
main
函数之前 此时基本是加载应用所需二进制类文件的阶段, 大部分和业务没有任何关系.
- 其一 加载解析可执行文件, 也就是戴老师所说的所有
.O
文件, 目前对于Mach-O
文件知之甚少, 根据下面参考文章中描述, 个人猜测 打包生成的 可执行文件 只是对于所有的.O
文件进行了完成的链接过程而已, 并不是生成一个.O
文件, bang 文章中提到, 可以根据LinkMap
记录文件,统计每个.O
文件的大小. - 其二
load_dylinker
动态链接器 加载程序中需要使用到的 动态库. 之前使用CocoaPods
工具构建iOS
组件化之路时, 最开始CocoaPods
对静态库支持不好, 使得项目中 模块都是以 动态库的方式引入. 根据这个启动顺序, 也就使得 应用冷启动时间变长了很多.
otool -L `ProjectName`
根据 孙源
博客上写的, 可以根据上面的指令查看 应用下所有 link
的 framework
其中 libobjc
即我们知道的 objc
和 runtime
.
- 其三
ObjC Runtime
的一些初始操作, 比如 ObjC Runtime 需要维护一张映射类名与类的全局表 - 其四 初始化
Initializers
, 比如我们常说的+load
方法 (这里要注意, 父类, 子类, 分类的+ load
方法顺序, 有些坑就在这里,尤其是分类的加载顺序. 其是按继承层级依次调用Class
的+load
方法和其Category
的+load
方法). 这个+load
方法目前不建议使用, 而通常建议使用另一个+initialize
, 也就是延迟加载. 其他的初始化,比如一些C++
全局变量等.- 苹果官方推荐使用
Swift
,因为 它没有initializer
,且语法更简洁,体积会更小 (ABI
已经稳定了嘛)
- 苹果官方推荐使用
而对此的优化, 也就是常见的分析应用中哪些 动态库参与了, 尝试减少动态库的数量, 比如 戴铭老师 提到的合并动态库, Cocoapods
目前也支持了组件声明静态库的导入方式. 又比如 避免不必要的 +load
方法调用, 或者更换成 +initialize
调用. 但是要注意有些功能是只能在 +load
中调用的,所以一定要注意!!!!!! 不要为了优化而导致业务上的问题.
main 方法之后
未完待续 ...
TIPS
合并多个库为一个
合并多个动态库文件为一个
Merge dylibs to speed up app startup time
There isn’t a way to merge dynamic libraries once they’re built. To produce a merged library you need to add the source for both libraries to a single dynamic library target. This can be quite tricky depending on how the complex the built system is for each library.
苹果员工回复说,一旦将动态库打包就无法进行合并操作, 只能将两个动态库源码合并到一个动态库 target
中打包才可以.
在 Stack Overflow
中有个回答, 说是 lipo -create path/yourFramework1 path/yourFramework2 -output path/yourFramework
iOS merge several framework into one
经测试
fatal error: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/lipo: libvips.42.dylib and libwebp.7.dylib have the same architectures (x86_64) and can't be in the same fat output file
此命令合并时,其架构不能相同 ,我测试多次都无法合并不同的动态库!!!! 应该如 苹果技术开发者所说的没有实现方式~
合并多个静态库为一个 查看👇脚本
combine_static_libraries.sh
static initializer analysis
自 iOS 11
之类增加了分析静态初始化工作的分析功能, 想要优化这块时间的可以参考 wwdc 2017 - App Startup Time: Past, Present, and Future
示例中展示了初始化方法中一个严重延迟的方法
移除或者改造后可以极大提升体验.
全程英文的理解有些难度, 所以这里有篇中文总结还是不错的 App 启动时间:过去,现在和未来
需要了解的概念
- 启动闭包(launch closure)
- 共享缓存 (shared cache)
- 预绑定(prebinding)
- 作用: 用于找到系统中每个 dylib 的固定的地址,动态连接器会尝试从这些地址中加载,如果加载成功,就会编辑这些二进制,等到下次他们被放到同样的地址上时,就不需要做任何工作了。这样能大幅优化启动速度,但这意味着二进制文件在每次启动时都被修改,在安全性和其他方面都有隐患.
- 由于 dyld 2.0 在性能有了显著提升,所以 dyld 1.0 中的预绑定被抛弃了
参考资料
- iOS 程序 main 函数之前发生了什么 : 孙源
- iOS 启动时间测试
- Optimizing App Startup Time - WWDC2016
- 优化 App 的启动时间 - wwdc 学习笔记
- iOS App 启动性能优化
- iOS App 签名的原理: bang
- iOS App签名的原理: 程序君
- iOS APP可执行文件的组成: bang
- Mach-O 可执行文件
- 如何解决IOS可执行文件TEXT段超标
- iOS 开发中的『库』(一)
- How we cut our iOS app’s launch time in half (with this one cool trick)
- Improving Your iOS App’s Launch Time