概要:
- 什么是启动?
- 如何测量启动?
- 使用
Instruments
分析启动- 跟踪启动的进度
一、启动的重要性
- 影响用户的体验
- 表明代码的整体性能
- 影响系统性能和电池
二、启动类型
- 冷启动(Cold)
当重启设备或App长时间未启动(App不在内存中)时,则会发生冷启动。
为了启动App,需要将它从磁盘读入内存,启动支持App的系统服务,然后生成App的进程。 - 热启动(Warm)
当App发生一次启动之后,再次启动就是热启动。此时App程序部分在内存中。 - 继续运行(Resume)
当App从主屏幕,或App切换器重新进入自己的App时,则发生这种情况。此时App程序完全在内存中。
冷启动、热启动、运行之间的区别:
三、启动过程
这六个过程包含了从系统初始化到App初始化,再到视图创建和布局,若App有需要,还可能会有一个扩展阶段,用于数据的异步加载。
(一)System Interface
第一阶段System Interface
的前半部是DYLD3
,关于DYLD3
可以参考资料 App Startup Time: Past, Present, and Future
- 静态链接器会加载App的共享库和框架;
- 为热启动缓存运行时的依赖项;
第一阶段System Interface
的后半部是libSystem Init
。在App中初始化底层系统组件的时候,现在这主要是系统方面的工作,有固定的消耗。因此我们不需要关注这一部分。
优化建议:
- 避免链接未使用的框架;
- 避免在启动期间加载动态库;
例如:dlopen()
、或NSBundle
、或load()
,因为这样会损失在缓存中建立的那些优势- 硬链接所有的依赖;
(二)Static Runtime Initialization
第二阶段是静态运行时初始化,该阶段主要完成以下工作:
- 系统初始化Objective-C和Swift语言运行时;
- 调用所有类的静态加载方法;
一般而言我们的App不应该在这里做任何工作,除非我们的程序中存在静态初始化方法,或者我们链接的框架带来的。通常不建议静态初始化。
优化建议:
- 暴露框架中的初始化API;
- 避免使用
+[Class load]
方法,减少对启动的影响;- 使用
+[Class initialize]
方法延迟静态初始化;
如果程序中有一个使用静态初始化的框架,则要考虑暴露API来尽早初始化我们的栈。如果必须要使用静态初始化,请考虑将代码移出+[Class load]
。因为+load
方法在App启动期间总会被调用。我们可以在类中第一次使用方法时来延迟调用。
(三) UIKit Initialization
第三阶段是UIKit初始化,该阶段的工作如下:
- 系统初始化
UIApplication
和UIApplicationDelegate
; - 开始事件的处理和系统的集成;
该阶段是系统初始化程序UIApplication
和UIApplicationDelegate
的时候。在大多数情况下,这是系统的工作,设置事件处理和系统的集成。如果我们在子类UIApplication
,或者在UIApplicationDelegate
初始化程序中做其他工作,仍然会影响这一阶段。
优化建议:
- 减少在子类
UIApplication
中工作;- 减少在
UIApplicationDelegate
初始化中的工作;
(四)Application Initialization
第四阶段是App初始化,最重要的东西都在这里,该阶段的工作如下:
- 调用
UIApplicationDelegate
的App生命周期回调;
这是作为开发者能够对App启动产生重大影响的地方。如果我们的App还没有采用UIScene
API,或针对iOS 12及更早版本的用户来说,App初始化仍然可以用这些回调方法。
application:willFinishLaunchingWithOptions:
application:didFinishLaunchingWithOptions:
- 调用
UIApplicationDelegate
的UI生命周期回调;
当App展示给用户时,将会进一步调用下面的方法。
applicationDidBecomeActive:
- 为每个场景调用
UISceneDelegate
的UI生命周期回调
当我们的App没有采用UIScene
API时,我们应该在application:didFinishLaunchingWithOptions:
方法中创建视图控制器。当使用UIScene
时,App初始化的工作方式略有不同。我们仍然可以获得application:willFinishLaunchingWithOptions:
和application:didFinishLaunchingWithOptions:
方法,但是当App展示给用户时,我们将获得UISceneDelegate
生命周期回调。
scene:willConnectToSession:options:
sceneWillEnterForeground:
sceneDidBecomeActive:
优化建议:
- 推迟无关的工作;
但没必要提交你的第一帧,可以通过将其推送到后台队列,或者稍后再完全执行。- 在场景之间共享资源;
如果我们的App采用了UIScene
,要确保在场景之间共享资源。这样做是为了减少多次不必要地进行一些工作的开销。
(五) Frame Render
第五阶段是第一帧渲染,这个阶段相对简单,该阶段的工作如下:
- 创建视图,执行布局,绘制视图;
loadView
viewDidLoad
layoutSubviews
- 提交和渲染第一帧;
优化建议:
- 展平视图层次结构和延迟加载视图;
我们可以减少层次结构中的视图数量来影响次阶段。也可以通过展平视图来减少使用,或延迟加载在启动期间未显示的视图来实现。- 优化自动布局的使用;
查看自动布局,减少正在使用的约束数量;
(六)Extended
第六阶段是扩展,该阶段的工作如下:
- 第一帧后App特定时间段;
- 显示异步加载的数据;
- 应用程序应具有交互性和响应性;
这是从我们第一次提交到向用户显示最后帧的App特定时间段。这是当我们加载异步数据时。其实不是每个App都有这个阶段。如果我们App有这个阶段,那么我们的App应该具有交互性和响应性。
优化建议:
- 利用os_signpost衡量工作;
当我们的应用确实有这个阶段时,我们需要了解正在发生什么,并且可以通过利用os_signpost
Api来标记和衡量在这两个时期发生的工作。
四、如何正确测量启动
(一) 一致性
- 消除差异来源以产生更一致的结果
- 可能导致启动时间不具有代表性
- 使用一致的结果评估进度
(二)在一个干净一致的环境中测试
通过以下方法,可以保证创建一个干净一致的测试环境:
- 重启设备,并让系统休眠2-3分钟;
重启设备可以清除不必要的状态,让系统休眠2-3分钟,来清除任何启动时间工作。
- 打开飞行模式,或模拟网络;
减少对网络的依赖。因为网络会引入相当多的差异。
- 使用不变的iCloud帐户和数据,或完全注销iCloud帐户;
iCloud在后台运行。这回干扰App的启动。
- 使用应用的发布版本;
这是为了减少测量期间不必需要的调试代码的开销,并利用编译时优化。
- 测量热启动;
这是因为它们更加一致,因为某些App可能已经在内存中,并且其中一些系统端服务可能已在运行。
(三)用具有代表性数据进行测试
(四)测试较新和较旧的设备
(五)使用XCTest测量启动
在任何给定的时间,iOS设备都处于各种不同的状态和情况下,这可能会在启动时引起很大的差异。因此当我们分析和比较启动结果时,确保我们进行"Apple - To - App le"的比较是至关重要的。因为如果在进行任何更改之前,你的启动结果完全不可预测。我们如何知道自己是否取得了进展呢?使其可预测的第一步是消除这些差异的来源,例如网络干扰,后台进程中的干扰。现在我们意识到这听起来有悖常理,因为这可能会导致启动不能完全代表常规使用。但这没有关系,拥有一致的结果可以评估很好地进展,这一点尤为重要。在Apple中一直只用这种技术,在开发过程中成功检测回归,并缩短启动时间。然后通过使用在实际情况中收集的遥测数据来验证这些性能的改进。
五、如何优化启动
当我们在代码和工具中查看App的启动时,我们应该记住以下三个提示和技巧:
(一)最小化你的工作
推迟与第一帧无关的工作;
推迟未显示的视图,或尚未使用的预加热功能等内容。避免阻塞主线程;
比如网络I/O,文件I/O或其他。这些都会影响启动,可以将其移动到后台线程。减少内存的使用;
分配和操作内存可能需要时间。
(二)优先考虑你的工作
- 为任务分配正确的
QoS
; - 利用调度程序优化来App启动;
- 使用正确的原语保证优先级;
Modernizing Grand Central Dispatch Usage
(三)优化你的工作
- 简化或限制现有工作
例如限制仅在启动期间获取所需数据的数据量,或者懒计算所需的任何变量和结果 - 优化算法和数据结构
- 缓存资源和计算
我们应该缓存资源和复杂的功能,减少CPU和内存开销。
六、跟踪启动的进度
- 使用
Xcode Organizer
监视用户的启动;
- 采用
MetricKit
获取更多的统计信息;