热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

 本文为(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

 
 

转载请注明出处,谢谢~


推荐阅读
  • 本文讨论了一个关于cuowu类的问题,作者在使用cuowu类时遇到了错误提示和使用AdjustmentListener的问题。文章提供了16个解决方案,并给出了两个可能导致错误的原因。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • IhaveconfiguredanactionforaremotenotificationwhenitarrivestomyiOsapp.Iwanttwodiff ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 安卓select模态框样式改变_微软Office风格的多端(Web、安卓、iOS)组件库——Fabric UI...
    介绍FabricUI是微软开源的一套Office风格的多端组件库,共有三套针对性的组件,分别适用于web、android以及iOS,Fab ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 本文介绍了一个题目的解法,通过二分答案来解决问题,但困难在于如何进行检查。文章提供了一种逃逸方式,通过移动最慢的宿管来锁门时跑到更居中的位置,从而使所有合格的寝室都居中。文章还提到可以分开判断两边的情况,并使用前缀和的方式来求出在任意时刻能够到达宿管即将锁门的寝室的人数。最后,文章提到可以改成O(n)的直接枚举来解决问题。 ... [详细]
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
  • IvebeentryingforadayortwototryandgetashadowtodrawinsidethetextofanNSTextField ... [详细]
  • Flutter入门——Flutter功能概览
    IT之家12月5日消息:今天谷歌官方宣布Flutter的1.0版本正式发布!Flutter是Google打造的UI工具包,帮助你通过一套代码同时在iOS和Android上构建媲美原 ... [详细]
  • 多态性这个词表示有许多形式。通常,当存在类的层次结构并且通过继承相关时,会发生多态性。Objective-C多态表示对成员函数的调用将导致执行不同的函数,具体取决于调用该函数的对 ... [详细]
  • ext将html代码转为字符串,在iOS中将HTML转换为NSAttributedString
    在iOS7中,UIKit添加了一个initWithData:options:documentAttributes:error ... [详细]
author-avatar
龙蛇虫
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有