iOS专题

优化 App 的启动时间

2017-03-27  本文已影响339人  茗涙

这是一篇 WWDC 2016 Session 406 的学习笔记,从原理到实践讲述了如何优化 App 的启动时间。

App 运行理论

main()
执行前发生的事
Mach-O 格式
虚拟内存基础
Mach-O 二进制的加载

理论速成

Load dylibs -> Rebase -> Bind -> ObjC -> Initializers

xcrun dyldinfo -rebase -bind -lazy_bind myapp.app/myapp

通过这个命令可以查看所有的 Fix-up。rebase,bind,weak_bind,lazy_bind 都存储在 __LINKEDIT 段中,并可通过 LC_DYLD_INFO_ONLY 查看各种信息的偏移量和大小。建议用 MachOView 查看更加方便直观。从 dyld
源码层面简要介绍下 Rebasing 和 Binding 的流程。

ImageLoader是一个用于加载可执行文件的基类,它负责链接镜像,但不关心具体文件格式,因为这些都交给子类去实现。每个可执行文件都会对应一个 ImageLoader 实例。ImageLoaderMachO 是用于加载 Mach-O 格式文件的 ImageLoader 子类,而 ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 都继承于 ImageLoaderMachO,分别用于加载那些 __LINKEDIT 段为传统格式和压缩格式的 Mach-O 文件。
因为 dylib 之间有依赖关系,所以 ImageLoader 中的好多操作都是沿着依赖链递归操作的,Rebasing 和 Binding 也不例外,分别对应着 recursiveRebase() 和 recursiveBind() 这两个方法。因为是递归,所以会自底向上地分别调用 doRebase() 和 doBind() 方法,这样被依赖的 dylib 总是先于依赖它的 dylib 执行 Rebasing 和 Binding。传入 doRebase() 和 doBind() 的参数包含一个 LinkContext 上下文,存储了可执行文件的一堆状态和相关的函数。
在 Rebasing 和 Binding 前会判断是否已经 Prebinding。如果已经进行过预绑定(Prebinding),那就不需要 Rebasing 和 Binding 这些 Fix-up 流程了,因为已经在预先绑定的地址加载好了。

ImageLoaderMachO 实例不使用预绑定会有四个原因:
Mach-O Header 中 MH_PREBOUND 标志位为 0

镜像加载地址有偏移(这个后面会讲到)
依赖的库有变化
镜像使用 flat-namespace,预绑定的一部分会被忽略
LinkContext 的环境变量禁止了预绑定

ImageLoaderMachO 中 doRebase() 做的事情大致如下:
如果使用预绑定,fgImagesWithUsedPrebinding
计数加一,并 return;否则进入第二步
如果 MH_PREBOUND 标志位为 1(也就是可以预绑定但没使用),且镜像在共享内存中,重置上下文中所有的 lazy pointer。(如果镜像在共享内存中,稍后会在 Binding 过程中绑定,所以无需重置)
如果镜像加载地址偏移量为0,则无需 Rebasing,直接 return;否则进入第四步调用 rebase() 方法,这才是真正做 Rebasing 工作的方法。如果开启 TEXT_RELOC_SUPPORT 宏,会允许 rebase() 方法对 __TEXT 段做写操作来对其进行 Fix-up。所以其实 __TEXT 只读属性并不是绝对的。

ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 分别实现了自己的 doRebase() 方法。实现逻辑大同小异,同样会判断是否使用预绑定,并在真正的 Binding 工作时判断 TEXT_RELOC_SUPPORT 宏来决定是否对 __TEXT 段做写操作。最后都会调用 setupLazyPointerHandler 在镜像中设置 dyld 的 entry point,放在最后调用是为了让主可执行文件设置好 __dyld 或 __program_vars。

Slide = actual_address - preferred_address

然后就是重复不断地对 __DATA 段中需要 rebase 的指针加上这个偏移量。这就又涉及到 page fault 和 COW。这可能会产生 I/O 瓶颈,但因为 rebase 的顺序是按地址排列的,所以从内核的角度来看这是个有次序的任务,它会预先读入数据,减少 I/O 消耗。

改善启动时间

从点击 App 图标到加载 App 闪屏之间会有个动画,我们希望 App 启动速度比这个动画更快。虽然不同设备上 App 启动速度不一样,但启动时间最好控制在 400ms。需要注意的是启动时间一旦超过 20s,系统会认为发生了死循环并杀掉 App 进程。当然启动时间最好以 App 所支持的最低配置设备为准。直到 applicationWillFinishLaunching
被调动,App 才启动结束。

测量启动时间

Warm launch: App 和数据已经在内存中Cold launch: App 不在内核缓冲存储器中冷启动(Cold launch)耗时才是我们需要测量的重要数据,为了准确测量冷启动耗时,测量前需要重启设备。在 main()
方法执行前测量是很难的,好在 dyld
提供了内建的测量方法:

在 Xcode 中 Edit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 1
。控制台输出的内容如下:
Total pre-main time: 228.41 milliseconds (100.0%) dylib loading time: 82.35 milliseconds (36.0%) rebase/binding time: 6.12 milliseconds (2.6%) ObjC setup time: 7.82 milliseconds (3.4%) initializer time: 132.02 milliseconds (57.8%) slowest intializers : libSystem.B.dylib : 122.07 milliseconds (53.4%) CoreFoundation : 5.59 milliseconds (2.4%)

优化启动时间

可以针对 App 启动前的每个步骤进行相应的优化工作。

加载 Dylib之前提到过加载系统的 dylib 很快,因为有优化。但加载内嵌(embedded)的 dylib 文件很占时间,所以尽可能把多个内嵌 dylib 合并成一个来加载,或者使用 static archive。使用 dlopen() 来在运行时懒加载是不建议的,这么做可能会带来一些问题,并且总的开销更大。

Rebase/Binding
之前提过 Rebaing 消耗了大量时间在 I/O 上,而在之后的 Binding 就不怎么需要 I/O 了,而是将时间耗费在计算上。所以这两个步骤的耗时是混在一起的。之前说过可以从查看 __DATA 段中需要修正(fix-up)的指针,所以减少指针数量才会减少这部分工作的耗时。对于 ObjC 来说就是减少 Class,selector 和 category 这些元数据的数量。从编码原则和设计模式之类的理论都会鼓励大家多写精致短小的类和方法,并将每部分方法独立出一个类别,其实这会增加启动时间。对于 C++ 来说需要减少虚方法,因为虚方法会创建 vtable,这也会在 __DATA 段中创建结构。虽然 C++ 虚方法对启动耗时的增加要比 ObjC 元数据要少,但依然不可忽视。最后推荐使用 Swift 结构体,它需要 fix-up 的内容较少。

ObjC Setup
针对这步所能事情很少,几乎都靠 Rebasing 和 Binding 步骤中减少所需 fix-up 内容。因为前面的工作也会使得这步耗时减少。

Initializer
显式初始化
使用 +initialize 来替代 +load

不要使用 atribute((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时才执行。比如使用 dispatch_once(),pthread_once() 或 std::once()。也就是在第一次使用时才初始化,推迟了一部分工作耗时。

隐式初始化
对于带有复杂(non-trivial)构造器的 C++ 静态变量:
在调用的地方使用初始化器。
只用简单值类型赋值(POD:Plain Old Data),这样静态链接器会预先计算 __DATA 中的数据,无需再进行 fix-up 工作。
使用编译器 warning 标志 -Wglobal-constructors 来发现隐式初始化代码。
使用 Swift 重写代码,因为 Swift 已经预先处理好了,强力推荐。

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

Reference

http://yulingtianxia.com/blog/2016/10/30/Optimizing-App-Startup-Time
/https://developer.apple.com/videos/play/wwdc2016/406/

上一篇 下一篇

猜你喜欢

热点阅读