IOS知识积累iOS项目性能优化iOS

深入探索 iOS 启动速度优化

2020-05-12  本文已影响0人  一意孤行的程序猿

介绍

App 的启动时间是体现其性能优劣的一个重要指标,启动时间越快用户的等待时间就越短,提升用户体验感,大厂应用甚至会做到“ 毫秒必究 ”。

我们将 App 启动方式分为:

名称 说明
冷启动 App 启动时,应用进程不在系统中(初次打开或程序被杀死),需要系统分配新的进程来启动应用。
热启动 App 退回后台后,对应的进程还在系统中,启动则将应用返回前台展示。

本篇文章主要针对冷启动方式进行优化分析,介绍常用的检测工具及优化方法。

冷启动流程

Apple 官方的《WWDC Optimizing App Startup Time》 将 iOS 应用的启动可分为 pre-main 阶段和 main 两个阶段,最佳的启动速度是400ms以内,最慢不得大于20s,否则会被系统进程杀死(最低配置设备)。

为了更好的区分,笔者将整个启动流程分为三个阶段, App总启动流程 = pre-main + main函数代理(didFinishLaunchingWithOptions)+ 首屏渲染(viewDidAppear),后两个阶段都属于 main函数 执行阶段。

pre-main 执行内容

此时对应的 App 页面是闪屏页的展示。

main函数代理执行内容

main() 函数开始执行到 didFinishLaunchingWithOptions 方法执行结束的耗时。通常会在这个过程中进行各种工具(监控工具、推送、定位等)初始化、权限申请、判断版本、全局配置等。

首屏渲染执行内容

首屏 UI 构建阶段,需要 CPU 计算布局并由 GPU 完成渲染,如果数据来源于网络,还需进行网络请求。

优化方案

pre-main阶段

检测方法

获得 main() 方法执行前的耗时比较简单,通过 Xcode 自带的测量方法既可以。将 Xcode 中 Product -> Scheme -> Edit scheme -> Run -> Environment Variables 将环境变量 DYLD_PRINT_STATISTICSDYLD_PRINT_STATISTICS_DETAILS 设为 1 即可获得执行每项耗时:

// example 
// DYLD_PRINT_STATISTICS
Total pre-main time: 383.50 milliseconds (100.0%)
         dylib loading time: 254.02 milliseconds (66.2%)
        rebase/binding time:  20.88 milliseconds (5.4%)
            ObjC setup time:  29.33 milliseconds (7.6%)
           initializer time:  79.15 milliseconds (20.6%)
           slowest intializers :
             libSystem.B.dylib :   8.06 milliseconds (2.1%)
    libMainThreadChecker.dylib :  22.19 milliseconds (5.7%)
                  AFNetworking :  11.66 milliseconds (3.0%)
                  TestDemo :  38.19 milliseconds (9.9%)

// DYLD_PRINT_STATISTICS_DETAILS
  total time: 614.71 milliseconds (100.0%)
  total images loaded:  401 (380 from dyld shared cache)
  total segments mapped: 77, into 1785 pages with 252 pages pre-fetched
  total images loading time: 337.21 milliseconds (54.8%)
  total load time in ObjC:  12.81 milliseconds (2.0%)
  total debugger pause time: 307.99 milliseconds (50.1%)
  total dtrace DOF registration time:   0.07 milliseconds (0.0%)
  total rebase fixups:  152,438
  total rebase fixups time:   2.23 milliseconds (0.3%)
  total binding fixups: 496,288
  total binding fixups time: 218.03 milliseconds (35.4%)
  total weak binding fixups time:   0.75 milliseconds (0.1%)
  total redo shared cached bindings time: 221.37 milliseconds (36.0%)
  total bindings lazily fixed up: 0 of 0
  total time in initializers and ObjC +load:  43.56 milliseconds (7.0%)
                         libSystem.B.dylib :   3.67 milliseconds (0.5%)
               libBacktraceRecording.dylib :   3.41 milliseconds (0.5%)
                libMainThreadChecker.dylib :  21.19 milliseconds (3.4%)
                              AFNetworking :  10.89 milliseconds (1.7%)
                              TestDemo :   2.37 milliseconds (0.3%)
total symbol trie searches:    1267474
total symbol table binary searches:    0
total images defining weak symbols:  34
total images using weak symbols:  97

优化点

main函数代理阶段

检测方法

通过双击具体函数可以跳转到对应代码处,另外可以将 Call Tree 的 Seperate by ThreadHide System Libraries 勾选上,方便查看。

正常Time Profiler是每1ms采样一次, 默认只采集所有在运行线程的调用栈,最后以统计学的方式汇总。所以会无法统计到耗时过短的函数和休眠的线程,比如下图中的5次采样中,method3都没有采样到,所以最后聚合到的栈里就看不到method3。

我们可以将 File -> Recording Options 中的配置调高,即可获取更精确的调用栈。

可以看到整个记录过程耗时7s,但 Time Profiler 上只显示了1.17s,且看到启动后有一段时间是空白的。这时通过 System Trace 查看各个线程的具体状态。

可以看到主线程有段时间被阻塞住了,存在一个互斥锁,切换到 Events:Thread State观察阻塞的下一条指令,发现0x5d39c 执行完成释放锁后,主线程才开始执行。

接着我们观察 0x5d39c 线程,发现在主线程阻塞的这段时间,该线程执行了多次10ms的 sleep 操作,到此就找到了主线程被子线程阻塞导致启动缓慢的原因。

今后,当我们想更清楚的看到各个线程之间的调度就可以使用 System Trace,但还是建议优先使用 Time Profiler,使用简单易懂,排查问题效率更高。

优化点

首屏渲染阶段

检测方法

记录首屏 viewDidLoad 开始时间和viewDidAppear 开始时间,两者的差值即为整个首屏渲染耗时,如果要获得具体每个步骤耗时,则可同main函数代理阶段使用 Time ProfilerHook objc_msgSend

优化点

其它优化

二进制重排

去年年底二进制重排的概念被宇宙厂带火了起来,个人觉得噱头大于效果,详细内容可参考文章

总结

启动优化不应该是一次性的,最好的方案也不是在出现才去解决,而应该包括:

只有在开发的前中后同时介入,才能保证 App 的出品质量,毕竟开发是前人挖坑给后人填坑的过程 😂。

部分工具

参考资料

推荐👇:

申请即送:

作者:SimonYe
链接:https://juejin.im/post/5e950106f265da47b725eaff

上一篇 下一篇

猜你喜欢

热点阅读