正在加载今日诗词....
8 min read

极客时间 | App 启动速度怎么做优化与监控?| 读书笔记

戴老师上面讲的如果是生活中使用高频,短暂的应用,如果启动时间太长,确实会丢失掉特别多的用户. 当然对于国内想增加开屏广告之类的应用来说, 此处优化倒是没有那么迫切,但身为程序员还对要对这方面要多一些了解, 以备不时之需.
极客时间 | App 启动速度怎么做优化与监控?| 读书笔记

--- 首先贴出极客时间原文地址 ----
02 | App 启动速度怎么做优化与监控?

戴老师上面讲的如果是生活中使用高频,短暂的应用,如果启动时间太长,确实会丢失掉特别多的用户. 当然对于国内想增加开屏广告之类的应用来说, 此处优化倒是没有那么迫切,但身为程序员还对要对这方面要多一些了解, 以备不时之需. 当然为了避免付费课程的版权问题,这里只是对原文涉及到的部分知识点进行总结, 不会原文粘贴. 同时我为了避免一家之言,查找了多家对于 应用启动有看法的文章, 详情见 参考, 当然最有说服力的仍然是 Optimizing App Startup Time - WWDC2016 独家了.

应用启动

首先来讲,应用启动这个过程,它分为两类

  1. 热启动 :应用进程已经启动,只是从后台到应用前台的切换启动过程.
  2. 冷启动 : 指的是应用第一次创建应用进程,进驻系统内运行.

热启动阶段

大部分应用这个过程时长相差不大. 是否还有优化空间, 应该也有,比如进入前台,一般有些应用会进行数据更新等操作, 只要不阻塞主线程就好.

冷启动阶段

此过程一般因业务不同,时长差异会较大, 而大部分优化也是从这里着手.

它主要分为3个阶段

  1. 进入 main 方法之前
  2. 执行 main 方法之后
  3. 用户能首次看到应用界面的时间点

main 方法之前

说这个的时候, 先来一张图, 很多博客都有这里, 不知道来源到底是哪里, 不过总之对于 应用启动这回事, 这个图很有专家范, 暂时先放上这个图, 如有侵权,请联系删除.

iOS-boot-process-34ffb37c-9d67-4d21-8c14-8647dddd33e4-1552579076278-53022086

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` 

根据 孙源 博客上写的, 可以根据上面的指令查看 应用下所有 linkframework

其中 libobjc 即我们知道的 objcruntime.

  • 其三 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

instrument-static-initializer-4f6469b0-dbfe-4638-96d8-3bdcb925c714-1553148614296-90197114

iOS 11 之类增加了分析静态初始化工作的分析功能, 想要优化这块时间的可以参考 wwdc 2017 - App Startup Time: Past, Present, and Future

示例中展示了初始化方法中一个严重延迟的方法

initializer-app-startup-615067b2-25de-4f7d-94f7-02bfd4ee3906-1553149671743-16808007

移除或者改造后可以极大提升体验.

全程英文的理解有些难度, 所以这里有篇中文总结还是不错的 App 启动时间:过去,现在和未来

需要了解的概念

  • 启动闭包(launch closure)
  • 共享缓存 (shared cache)
  • 预绑定(prebinding)
    • 作用: 用于找到系统中每个 dylib 的固定的地址,动态连接器会尝试从这些地址中加载,如果加载成功,就会编辑这些二进制,等到下次他们被放到同样的地址上时,就不需要做任何工作了。这样能大幅优化启动速度,但这意味着二进制文件在每次启动时都被修改,在安全性和其他方面都有隐患.
    • 由于 dyld 2.0 在性能有了显著提升,所以 dyld 1.0 中的预绑定被抛弃了

参考资料