App 启动速度优化与监控
App 启动的三个阶段
一般而言,App 的启动时间指的是从用户点击 App 开始,到用户看到第一个界面之间的时间。 主要分为三个阶段:
- 1.main() 函数执行前;
- 2.main() 函数执行后;
- 3.首页渲染完成后。
1.main() 函数执行前
执行的操作
- 加载可执行文件(App 的 .o 文件集合)
- 加载动态链接库,进行 rebase 指针调整和 bind 符号绑定
- oc 运行时的初始化,包括 oc 相关类的注册、Category 的注册、selector 唯一性检测等
- 初始化,包括执行+load() 方法,attribute((constructor)) 修饰的函数调用,创建 C++ 静态 全局变量
优化
- 减少动态库加载,尽可能将多个动态库合并
- 减少加载启动后不会去使用的类或者方法
- +load()方法里的内容可以放到首屏渲染完成后再执行,或使用 +initialize()方法替换掉, 。在+load()方法里,进行运行时方法替换操作会带来4毫秒的消耗。积少成多
- 控制C++全局变量的数量
iOS8 开始,由于 Extension 的出现,苹果开始允许自建动态库并在 iOS App 中使用, 这样宿主 App 和插件之间共享动态库,iOS 上的动态库是私有的,不允许进程间共享,因为不能将动态库放在除自身沙盒以外的 其它任何地方。
动态库在应用打包编译的时候,仅把链接信息编译到应用二进制可执行文件中,自身以类似资源的形式 以一个单独的framework文件存放在安装包中,将framework的加载推迟到运行时。
如果把动态库全改为静态库,也可以减少启动时间,静态库会打包到App的.o文件集合中, 时间会小于加载动态库的时长。但是全打包到.o中,会使应用提交评审时的代码段非常大,苹果 官方文档对代码段有限制,会被拒绝。
2.main() 函数执行后
main()函数执行后的阶段,指的是从main()函数执行开始,到Appdelegate的didFinishLaunchingWithOptions 方法里首屏渲染相关方法执行完成。
首页的业务代码都是要在这个阶段,也就是屏渲染完成前执行的,主要包括以:
- 首屏初始化所需配置文件的读写操作
- 首屏列表大数据的读取
- 首屏渲染的大量计算等
从功能上梳理出哪些是首屏渲染必要的初始化功能,哪些是 App 启动必要的初始化功能,而哪些是只需要在对应功能开始使用才需要初始化的。 放入合适的阶段初始化。
3.首屏渲染完成后
从渲染完成时开始,到 didFinishLaunchingWithOptions 方法作用域结束时结束。
主要完成的是非首屏其他服务模块的初始化、监听的注册、配置文件的读取。
优化
功能级别
从 main() 函数执行后的这个阶段下手。main() 函数开始执行后到首屏渲染完成前只处理首屏相关的业务,其他非首屏 业务的初始化、监听注册、配置文件读取等都放到首屏渲染完成后进行。
方法级别的启动优化
检查首屏渲染完成前主线程上有哪些耗时方法,将没必要的耗时方法滞后或异步执行。通常情况下,主要表现为计算大量数据,加载、编辑、储存图片和文件等资源。
对App启动速度的监控,主要有两种手段
定时抓取主线程上的方法调用堆栈,计算一段时间里各个方法的耗时
Xcode 自带的工具套件 Time Profiler,采用的就是这种方式。
对 objc_msgSend 方法进行 hook 来掌握所有方法的执行耗时
hook 方法的意思是,在原方法开始执行时换成其他你指定的方法,或者在原有方法执行前后执行你指定的方法,来掌握和改变 指定方法的目的。
对于 C 和 block,可以使用 libffi 的 ffi_call 来达成 hook,但缺点就是编写维护相关工具门槛高。
Facebook 开源了一个库,可以在 iOS 上运行的 Mach-O 二进制文件中动态 地重新绑定符号,fishhook
大致思路是,通过重新绑定符号,可以实现对c方法的hook。dyld是通过更新Mach-O二进制 的__DATA segment特定的部分中的指针来绑定lazy和non-lazy符号,通过确认传递给rebind_symbol 里每个符号名称更新的位置,就可以找出对应替换来重新绑定这些符号。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论