(WWDC)优化应用启动时间 —— 实践篇

 

本文为 (WWDC)优化应用启动时间 —— 理论篇 的续篇。

 

内容概览

  • 多快才算快?
  • 如何度量?
  • 什么导致了启动慢?
  • 优化启动时间
  • 总结

当你启动一个应用并且你需要等待数秒才能开始使用这个应用,你会产生什么想法?

卸载它,是吗?

 


多快才算快?

 

不同平台有不同的要求,不过 400ms 是一个比较适合的启动等待时长。
我们需要一段时间来完成主页到应用首页的转场动画。
在这个动画播放的时候,我们有机会去完成加载应用的工作。

如果你的应用启动等待时长超过 20s,操作系统会假设你的应用已经陷入死循环,然后它会终结这个应用的进程。

因此,你需要在应用所兼容的性能最差的设备上进行测试,以此来保证应用确实兼容这些设备。

 
 

回顾应用启动的过程

在上述过程结束后,main()UIApplicationMain() 会被调用,最后 AppDelegate 中的 applicationWillFinishLaunching 回调函数也被调用。

最后这两个步骤,在 400ms 启动等待时间中占据了重要部分,不过在本文中不会对此进行讨论。
如果你想了解这两个步骤的细节,强烈推荐观看 iOS App Performance: Responsiveness

 
 

冷启动 & 热启动

热启动:应用和数据已经被加载到内存中
冷启动:应用尚未被加载到操作系统内核的缓冲缓存

冷启动和热启动的时间不同,我们更需要关注冷启动的耗时。
在测试冷启动的耗时时,需要重启设备

在你优化热启动过程的时候,冷启动过程也会被优化。
所以在多次测量热启动过程之后,也可以进行一次冷启动过程的测量。

 
 


如何度量?

 

  • main() 函数被调用之前进行测量是很困难的
  • Dyld 内置了测量工具
    • scheme 中启用 DYLD_PRINT_STATISTICS 环境变量(在已发布的系统中都是可用的,而且新版系统对此进行了优化,从 seed 2 版本开始就可以启用)
  • 调试器会在加载每个动态库时暂停,然后将解析应用里的符号并加载断点,以及进行耗时的USB线路传输操作
    • Dyld 知道这个过程,所以这些时间会从测量时间里减去
    • 如果你自己用计时器测量的时间远大于工具测量的时间,这是正常的

 
 


什么导致了启动慢?

 

在配置了 DYLD_PRINT_STATISTICS 环境变量后,你可以在控制台看到类似的输出:


假设下面整个的进度条是全部的启动时间,那么虚线处就代表耗时为 400ms,很明显这个应用没有达标。

 
 


优化启动时间

 

首先,我们需要理解控制台中输出的内容。

 

dylib loading time


这个过程需要加载很多 dylibs,平均为 100 - 400 个,其中大多数为系统内置的 dylib

  • 加载这些 dylibs 是昂贵的耗时操作
  • 我们无法提前计算加载这些 dylibs 所需的时间,解决方案就是尽量少加载 dylibs
    • 合并现有的 dylibs
    • 使用静态库
    • 使用 dlopen() 进行懒加载(dlopen()可能导致其他问题,甚至会做更多工作,所以不推荐)

 

比如,现在需要用到 26 个动态库:


加载时间为 240 ms

然后,把这些动态库合并为 2 个动态库:


现在,加载时间仅为 21 ms

优化后的大致效果:

当然,这样做是有代价的!
更多的库可以有效节省构建和重新链接的时间,从而加速开发过程。
所以,推荐将动态库的数量控制在一定范围内,推荐的动态库数量为 6 个。

 

rebase/binding time

这个过程的耗时是 351 ms
在这个过程中,变基需要进行 IO 操作,绑定需要进行计算操作( IO 操作已经由变基完成)。
这个过程主要就是修复 __DATA 段中的指针。

优化方案:

  • 减少 __DATA 段中需要修复的指针的数量
  • 减少 Objective-C 中的元数据(classes, selectors, and categories
  • 减少 C++ 虚函数的数量
  • 优先使用 Swift 中的 struct
  • 检查工具生成的代码(避免使用指针,可使用结构体进行替代,或者标记为只读)

现在,这个项目中有超过 10000 个类,事实上是 20000 个!

如果把这个数量缩减到 1000 个,耗时会从 351 ms 变为不到 20 ms

 

ObjC setup time

在这个过程中,主要进行类注册、处理 Non-fragile ivars offset 更新、分类(Category)注册、Selector 唯一化处理等操作。

通过处理变基、绑定过程,这个过程也得到了优化。所以,这里不需要做什么特殊处理。
这部分的优化效果微乎其微,从 11.8 ms 变为了 4.6 ms


 

initializer time

这个过程的耗时占了最大的比例,所以需要着重优化。
主要是对显式、隐式的初始化方法进行优化。

 

显示初始化:

  • 对于 ObjC,使用 +initiailize 替换 +load。这可以使代码在运行时加载,而不是源代码文件被加载时。
  • 对于 C/C++__attribute__((constructor)) 会在这个阶段生成初始化方法,尽量避免使用。
  • 使用调用点初始化方法(在第一次被调用时才会执行):
    • dispatch_once()
    • pthread_once()
    • std::once()

dispatch_once() 已经被深度优化。在第一次执行后,再次执行的效果类似于空操作,所以强烈推荐使用这个方法而不是显式的初始化方法。

 

隐式初始化:

  • C++ 中的非轻量级构造器
    • 使用调用点初始化方法替代
    • 仅仅设置为简单的值(PODs - plain old data),静态链接器就可以计算 __DATA 段的内存布局,就不再需要进行后续的指针修复操作
    • 为编译器添加 -Wglobal-constructors 标识参数,帮助你找到
    • 使用 Swift 重写 (Swift 全局变量使用 dispatch_once() 完成初始化)
  • 不要在初始化方法中调用 dlopen(),因为这会造成性能问题(加锁、死锁、undefined behaviors
  • 不要在初始化方法中创建线程,原因同上

这个项目中有一个非轻量级(non-trivial)的构造器:


Pause onLaunch(10); 注释掉之后,耗时从 10 s 缩减为不到 4 ms

 
 


总结

  • 设置 DYLD_PRINT_STATISTICS 环境变量,测量启动耗时
  • 减少启动耗时的方法:
    a. 减少 dylibs 的数量
    b. 缩减 ObjC 类和分类(Category)的数量
    c. 消除静态初始化方法
  • 尽可能使用 Swift
  • 尽可能不使用 dlopen() (可能引发性能和死锁问题)

 
 


参考内容:

Optimizing App Startup Time

 
 

转载请注明出处,谢谢~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值