阅读 926

Optimizing App Launch 应用启动优化

视频地址:https://developer.apple.com/videos/play/wwdc2019/423/

文档地址:https://devstreaming-cdn.apple.com/videos/wwdc/2019/423lzf3qsjedrzivc7/423/423_optimizing_app_launch.pdf

Why Launch Is Important

  • App的首次使用应该是愉悦的
  • 预示着代码的整体性能
  • 影响系统性能和电池寿命

Launch Types

冷启动Cold

  • After reboot 重启后
  • App is not in memory 应用不在内存
  • No process exists 没有进程存在

重启后或App很长时间未启动, 会发生冷启动. 需要将其从磁盘加载到内存, 启动支持App的系统服务, 然后生成相应进程, 冷启动发生过后, 后续将体验到热启动

热启动Warm

  • Recently terminated 最近终止
  • App is partially in memory 应用部分在内存中
  • No process exists 没有进程存在

应用热启动需要被唤醒, 由于已经启动了系统服务, 所以也会更快一些

继续运行Resumn

  • App is suspended 应用挂起
  • App is fully in memory 应用在内存中
  • Process exists 进程存在

继续运行通常称为启动, 但不是一个真正的启动. 用户在主屏幕或者从App切换器, 重新进入App时, 会发生继续运行, 此时App启动会更快

注意 在进行评估的时候不要将启动和继续运行进行混淆

Phases of App Launch

400ms To render first frame

从启动到向用户显示像素, 并在启动动画完成时, App就具有了交互性和相应性

启动通常发生在用户点按主屏幕上的图标时, 在接下来100毫秒左右iOS将执行必要的系统工作初始化App. 然后剩下300毫秒左右给开发者来创建视图加载内容并生成第一帧. 这一帧不一定要真正的完成, 它也可以有一些占位符, 用于异步加载数据. 此时的App是可以进行交互和响应的. 然后在接下来的几百毫秒内为用户显示异步加载的数据填充内容生成最终帧

Phases of App Launch

以上6个阶段涵盖了系统初始化到App初始化, 再到试图创建和布局, App可能需要扩展的阶段, 用于数据异步加载.

System Interface

System Interface

DYLD3

  • Dynamic Linker loads shared libraries and frameworks
  • Introduces caching of runtime dependencies to improve warm launch

系统界面的前半部分是DYLD3, 动态连接器会加载共享库和框架, DYLD3在这个阶段进行优化. 在iOS13中提供了热启动缓存运行时依赖项, 能够提高热启动的性能. 在新的链接器中, 为了充分利用其优化, 应当

  • 避免链接未使用的框架, 因为这会产生潜在的成本
  • 在启动的时候避免加载动态库, 例如dlopen(), NSBundle中的load(), 因为这会破坏在缓存中建立的优势
  • 应当使用硬链接所有依赖项

libSystem Init

  • Initializes the interfaces with low level system components
  • System side work with a fixed cost

系统界面的后半部分是libSystem Init, 这是App初始化底层系统组件的时候进行的工作.主要是系统方面的工作, 成本是固定的. 因此, 开发者无需关注这部分

Static Runtime Initialization

Static Runtime Initialization

  • Initializes the language runtime
  • Invokes all class static load methods

接下来是静态运行时初始化, 运行时初始化Objective-CSwift, 一般来说, App在这个阶段不应该做任何工作, 除非代码中要执行一些静态初始化方法, 也有可能是链接的库执行的某些操作. 通常不建议进行静态初始化.

  • Expose dedicated initialization API in frameworks
  • Reduce impact to launch by avoiding +[Class load]
  • Use +[Class initialize] to lazily conduct static initialization

如果使用了一个包含有静态初始化的框架, 应该暴露相应API尽早执行相应初始化工作. 如果必须使用静态初始化, 则考虑把代码移出+[Class load]方法. 因为在App启动期间总会被调用. 使用+[Class initialize]懒惰地进行静态初始化(首次使用类的方法的时候会被懒加载调用)

UIKit Initialization

UIKit Initialization

  • Instantiates the UIApplication and UIApplicationDelegate
  • Begins event processing and integration with the system

这个阶段系统将实例化UIApplicationUIApplicationDelegate, 在大多数情况下, 这是系统方面的工作, 设置事件处理和系统的集成. 但是开发者任然可以影响这个阶段.

  • Minimize work in UIApplication subclass
  • Minimize work in UIApplicationDelegate initialization

如果在子类UIApplicationUIApplicationDelegate初始化程序中做任何工作, 应当执行能够初始化程序的做小工作集.

Application Initialization

UIKit Initialization

Lifecycle Callbacks

针对没有使用UISence API 或者iOS 12之前更早版本, App初始化仍可调用一下回调方法

application:willFinishLaunchingWithOptions: application:didFinishLaunchingWithOptions:

当App呈现给用户的时候下面的方法会被调用

applicationDidBecomeActive:

使用UISence API初始化App时, 工作方式会略有不同. 当然任然可以使用willFinishLaunchingWithOptionsdidFinishLaunchingWithOptions. 当App显示给用户的时候, 将会获得UISceneDelegate生命周期的回调

scene:willConnectToSession:options: sceneWillEnterForeground: sceneDidBecomeActive:

应当使用scene:willConnectToSession:options:创建视图控制器. 注意的是只用scene: willConnectToSessionwithOptions, or didFinishLaunchingwithOptions方法创建

建议

  • Defer unrelated work
  • Share resources between scenes

在这个阶段, 建议推迟任何与提交第一帧不相关的工作, 可以将这些工作放到后台队列中进行执行, 或者是稍后执行

如果使用UISence还可以做一件事情, 就是在场景之间共享资源, 减少一些不必要的开销

First Frame Render

First Frame Render

这个阶段相对简单, 这是创建视图进行布局然后绘制的阶段

  • Creates, performs layout for, and draws views
  • Commits and renders first frame

loadView viewDidLoad layoutSubviews

可以通过减少层级结构中的视图数来影响此阶段

  • Flatten view hierarchies and lazily load views
  • Optimize auto layout usage

减少视图层级数, 在启动期间延迟加载视图, 还应当查看视图的自动布局, 看看是否能够减少约束数量

Extended

Extended

  • App-specific period after first frame
  • Displays asynchronously loaded data
  • App should be interactive and responsive

这个阶段是第一次提交到显示最终帧的App特定时间, 有可能会展示异步加载的数据. 并不是所有的App都会有这个阶段, 如果要, 确保App的交互性和响应性

在这个阶段的建议是

  • Leverage os_signpost to measure work

了解当前阶段发生了什么, 并且通过os_signpost API来标记和度量这个阶段发生的工作

Trading Representativeness for Consistency

  • Remove sources of variance to produce more consistent results
  • May result in launch times that are not representative
  • Use consistent results to evaluate progress

在任何给定的时间, iOS设备处于各种不同的状态和情况下, 可能会在启动时产生不同的差异. 因此当进行分析和比较启动结果时, 确保Apple-Apple对比至关重要. 因为在进行任何更改之前, 启动的结果完全不可预测. 使其可以测的第一步是消除差异源, 例如网络干扰, 后台进程的干扰. 因为这些会导致启动不能代表常规使用. 拥有一致性的结果可以是评估很好的进展. 在实际情况中收集远程数据验证新能改进

Test in a Clean and Consistent Environment

  • Reboot then let system quiesce for 2–3 minutes
  • Enable Airplane Mode or mock the network
  • Use unchanging or no iCloud Account
  • Use release build of your app
  • Measure warm launches

重启设备, 这将清除不必要的状态, 然后让它在接下来的几分钟内安定下来以清除任何启动时的工作.

还可以通过打开飞行模式或者在代码中标记网络依赖来减少对网络的依赖, 网络可以引入相当多的差异

iCloud可以在后台运行, 可以为用户提供无缝体验, 但是后台工作会影响App的启动, 因此在测量过程中使用不变的iCloud账户和数据, 或者完全注销iCloud, 接下来一定要在测量时, 使用App的发布构建版本, 这是为了减少测量时间, 不必要的代码调试时间. 并且利用编译时优化.

最后, 要使用热启动进行测量, 因为这样更加一致, App的一些系统服务已经处于运行状态.

Test with Representative Data

设置一些数据来测试, 创建一致的模拟数据非常重要. 可能需要为不同类型的用户提供一些数据, 例如具有少量数据的用户和具有大量数据的用户. 在理想情况下, App应该能够扩展到任何数量的数据. 这就是为甚么, 在显示第一帧的时候只加载必要数据的原因.

Target Older and Newer Devices

选择对用户重要的各种设备, 然后使用它们, 强制一致性. 确保最早支持的版本也在内. 因为新设备和旧设备之间的的性能特征的不同, 两者具有不同的RAM和CPU内核. 这将确保App在所有的设备上都能使用户感到愉悦.

Measuring Launch with XCTest

在Xcode 11中使用新的XCTest进行测试, 测试App的性能, 只需几行代码, Xcode就会重启App然后提供有关其执行的统计结果

如何改进启动

在代码和工具中, 查看App的启动时, 应当记住这三个提示和技巧

  • Minimize首先是最小化工作
  • Prioritize然后优先考虑自己的工作
  • Optimize最后优化工作

Minimize Work

  • Defer work unrelated to first frame
  • Move blocking work off main thread
  • Reduce memory usage

最小化工作时, 应该推迟与生成第一帧无关的任何内容. 这意味着推迟任何未显示的视图或者尚未使用的pre-warming features.

应当避免阻塞主线程, 比如网络I/O, 文件I/O等, 可将它们移至后台执行

最后应该注意减少内存使用和分配、操作内存使用的时间

Prioritize Work

  • Identify the right QoS for your task
  • Utilize scheduler optimizations for app launch
  • Preserve the priority with the right primitives

确保正确的服务质量安排工作, 在iOS 13中, 对调度器进行了优化使得App启动时间更快. 使用正确的原语保留优先级, 比以往任何时候都更加重要.

Optimize Work

  • Simplify or limit existing work
  • Optimize algorithms and data structures
  • Cache resources and computations

最小化工作然后考虑优先级, 所以优化应该是简化的, 有限的. 例如, 限制仅在启动期间获取所需数据的数据量, 或者懒计算所需的任何变量和结果. 如果这样做了, 记得查看代码的方法和算法, 看看能否对它们进行优化. 通过对它们不同的计算结果或使用不同的数据结构来获得显著的提升. 最后应该缓存使用的资源和复杂功能. 当然通过多次不必要的工作, 来减少不必要的内存和CPU开销.

Track Your Launch Over Time

  • Make performance a development-time priority
  • Plot it and have a target

不要让性能优化变成事后的想法, 应该在每个bug修复的开始, 每个参数修改的开始, 以及每个功能研发开始的时候, 考虑并研究它们. 因为这些都是小问题, 但小问题多了堆积就是大问题, 如果不立即修改它们, 之后就很难在找到了, 为了做到这些要进行回归检测.

应该定期绘制App启动并定期运行测试确保达到优化目标

Adopt MetricKit for More Statistics

  • Collect custom power and performance metrics
  • Aggregated results delivered every 24 hours

希望对更对的统计数据进行控制, 则可以使用MetricKit, 它可以指定自定义功率和性能指标, 可在24小时之内进行数据汇总, 然后在App授权的方法中回传回来. 从那里开始就可以随意处理数据

Summary

  • Start understanding your launch today
  • Measure — don’t estimate — performance
  • Track performance in all phases of development

理解如有错误 望指正 转载请说明出处

文章分类
iOS
文章标签