启动时间的测量

准备知识

简单了解一下App 的启动过程:

  • 解析Info.plist
  • 加载相关信息,例如如闪屏
  • 沙箱建立、权限检查
  • Mach-O加载
  • 如果是胖二进制文件,寻找合适当前CPU类别的部分
  • 加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
  • 定位内部、外部指针引用,例如字符串、函数等
  • 执行声明为attribute((constructor))的C函数
  • 加载类扩展(Category)中的方法
  • C++静态对象加载、调用ObjC的 +load 函数
  • 程序执行
  • 调用main()
  • 调用UIApplicationMain()
  • 调用application willFinishLaunchingWithOptions (告诉代理进程启动但还没进入状态保存)
  • 调用application didFinishLaunchingWithOptions (告诉代理启动基本完成程序准备开始运行)

对于启动时间的研究,冷启动比热启动重要

当用户按下home键的时候,iOS的App并不会马上被kill掉,还会继续存活若干时间。理想情况下,用户点击App的图标再次回来的时候,App几乎不需要做什么,就可以还原到退出前的状态,继续为用户服务。这种持续存活的情况下启动App,我们称为热启动(热启动调用applicationDidBecomeActive),相对而言冷启动就是App被kill掉以后一切从头开始启动的过程。我们这里只讨论App冷启动的情况。

我们通常讲的启动时间具体指什么,有哪些部分组成

通俗来讲,启动时间是指从用户点击 APP 那一刻开始到用户看到第一个界面这中间的时间。启动时间分为 main( ) 函数之前的时间和 main( ) 函数到第一个界面渲染完成时间这两个部分。

main( )函数之前

在不越狱的情况下,以往很难精确的测量在 main( ) 函数之前的启动耗时,因而我们也往往容易忽略掉这部分数据。小型App确实不需要太过关注这部分。但如果是大型App(自定义的动态库超过50个、或编译结果二进制文件超过30MB),这部分耗时将会变得突出。所幸,苹果已经在Xcode中加入这部分的支持。

main( )函数之后

调用 AppDelegate 中的代理方法,我们主要是在 application didFinishLaunching 这个代理方法中添加自己的代码。代理方法中,一般我们会做那些事情:

  • 初始化Window,初始化基础的ViewController结构(一般是UINavigationController+UITabViewController)
  • 日志、统计等配置事件
  • 一些第三方SDK的初始化和配置事件
  • 用户的相关信息配置

main( )函数之前(pre-main)时间的测量

在Xcode的菜单中选择Project→Scheme→Edit Scheme…,然后找到 Run → Environment Variables,添加DYLD_PRINT_STATISTICSvalue为1的环境变量。

ios 应用启动时间 ios启动时间测量_优化

运行程序,控制台会有如下输出:

ios 应用启动时间 ios启动时间测量_优化_02

对以上信息的解读:
1. main()函数之前总共使用了425.44ms
2. 在425.44ms中,加载动态库用了119.88ms,指针重定位使用了86.36ms,ObjC类初始化使用了103.79ms,各种初始化使用了115.29ms。
3. 在初始化耗费的115.29ms中,用时最多的几个初始化为:AFNetworking,MJRefresh…等。

main( )函数之后时间的测量

方法一:利用 XCode 自带的 Time profile 工具,可以帮我们分析出哪些代码执行过程中比较耗时,在App的启动过程中,我们主要关注主线程的耗时。
方法二:利用打点计时,类似中学物理中的打点计时器。比较好的方式是把启动任务规范化、粒子化,针对每个任务都有打点统计,这样方便后期问题的定位和优化。

启动时间的优化

针对main( )函数之前时间进行优化

  • 重新梳理架构,减少动态库、ObjC类的数目,减少Category的数目
  • 定期扫描不再使用的动态库、类、函数,例如每两个迭代一次
  • 用dispatchonce()代替所有的attribute((constructor))函数、C++静态对象初始化、ObjC的+load
  • 在设计师可接受的范围内压缩图片的大小,会有意外收获

针对main( )函数之后时间进行优化

不同的App在main( )函数之后做的事情往往不同,优化起来的核心思想:

  • 能延迟执行的就延迟执行。比如SDK的初始化,界面的创建。
  • 不能延迟执行的,尽量放到后台执行。比如数据读取,原始JSON数据转对象,日志发送。
  • rootViewController的加载中,适当将某一级的childViewController或subviews延后加载
  • 先加载UI占位,后显示数据

最后,不应放过的一些小细节:

  • 异步操作并不影响指标,但有可能影响交互体验,例如大量网络请求导致数据拥堵
  • 有时候一些交互上的优化比技术手段效果更明显,视觉上的快决不是冰冷的数据可以解释的,好好和你们的设计师谈谈动画