学计算机的那个

不是我觉到、悟到,你给不了我,给了也拿不住;只有我觉到、悟到,才有可能做到,能做到的才是我的.

0%

【译】iOS启动优化

通过最小化启动时间,为应用创造更快速的体验。

概述

用户使用应用程序的第一次体验是它启动时的等待。操作系统表明,应用程序在iOS上启动时出现闪屏,在macOS上的Dock中弹出图标。应用程序需要准备好帮助用户尽快完成任务。如果一款应用启动时间过长,可能会让用户感到沮丧。在iOS平台上,如果启动时间过长,看门狗就会终止该应用。通常情况下,如果应用是用户日常工作流程的一部分,那么用户一天会启动应用多次,而较长的启动时间会导致执行任务的延迟。

当用户点击主屏幕上应用程序的图标时,iOS会在将控制权移交给应用程序进程之前为应用程序的启动做好准备。然后,应用程序运行代码,准备将其UI绘制到屏幕上。即使在应用程序的UI可见之后,应用程序可能仍然在准备内容或用最终控件替换插页界面(例如,加载旋转器)。这些步骤都会影响应用的总启动时间,你可以采取措施减少它们的持续时间。

了解应用激活

当用户点击你的图标或回到你的应用程序时,就会发生激活。

在iOS上,激活既可以是启动,也可以是恢复。启动是当进程需要启动时,而恢复是当你的应用程序已经有一个活跃的进程,即使暂停。恢复通常要快得多,优化启动和恢复的工作是不同的。

在macOS上,系统不会终止您的进程作为正常使用的一部分。激活可能需要系统从压缩器(compressor)引入内存、交换和重新渲染。

理解冷启动和热启动

应用程序的激活取决于设备上之前的操作。

例如,在iOS平台上,如果你滑回主屏幕并立即重新进入应用程序,这就是最快的激活速度。它也可能是一份恢复。当系统确定需要启动时,通常称为“热启动”。

相反地,如果用户刚刚玩了一款内存密集型的游戏,然后他们再次进入你的应用,那么它可能会比你的平均激活速度慢得多。在iOS上,你的应用程序通常会被从内存中移除,以允许前台应用程序拥有更多的内存。应用程序启动所依赖的框架和守护进程(Frameworks and daemons)也可能需要重新启动并从磁盘进行分页加载^守护进程。这种场景,或者在系统启动后立即启动app,通常称为“冷启动”。

把冷启动和热启动看作一个光谱。在实际使用中,用户将根据设备的状态体验到不同的性能。这就是为什么在各种条件下进行测试对于预测真实世界的性能至关重要。

收集关于应用发布时间的参数

启动过程中的各种变化意味着理解应用在该领域的运行方式可能具有挑战性。

对于iOS应用,使用Xcode Organizer中的Launch Time面板来查看从用户点击图标到静态启动屏幕后绘制第一个屏幕之间的毫秒数。使用过滤器检查不同设备上的启动时间以及典型(第50个百分位数)和最长(第90个百分位数)时间。通过单击所需版本的图表中的栏,比较当前版本的发布时间与之前版本的发布时间。

MetricKit除了报告启动时间外,还报告恢复应用程序的时间。MXAppLaunchMetric包含前一天启动和恢复时间的直方图。

分析应用的启动时间

一旦你知道应用启动需要多长时间,你就需要知道为什么需要这么长时间。分析你的应用程序的代码是一种收集你需要的关于你的应用程序花费时间的数据的方法。在分析过程中,Instruments收集有关应用程序调用的方法以及执行这些方法花费的时间的信息。使用这些数据来识别代码中潜在的瓶颈或问题。

Instruments中使用app Launch模板来配置你的应用。在启动期间,Instruments收集时间配置文件和线程状态跟踪。使用时间配置文件来识别应用程序在启动期间运行的代码。使用线程状态跟踪来查找线程活动或阻塞的时间,并发现线程阻塞的原因。

分析应用在不同情况下的启动时间,看看这些因素是如何影响体验的。下面是一些需要测试的不同情况的例子:

  • 打开设备,第一次解锁,然后启动你的应用。

  • 强制退出你的应用,然后启动它。系统将终止应用程序进程,系统将执行热启动。

  • 如果你打开其他应用程序,然后启动你的应用程序,系统将部分回收你的应用程序及其依赖项。这反映了一个常见的用户工作流程。

  • 使用一个非常大的应用程序——例如,一个使用许多图形资源或实时摄像头输入的应用程序——然后启动你的应用程序。系统可能会终止你的应用程序的进程,这意味着系统需要在你下次启动时分页加载应用程序的许多依赖项。

UIKit在主线程上绘制视图和处理用户事件,所以当应用程序完成启动时,该线程必须可用来绘制第一帧。在Instruments线程跟踪中,主线程运行或被抢占的时间是它无法绘制视图或响应用户输入事件的时间。

对于应用程序启动的不同视图,使用时间配置文件模板(Time Profile template)分析应用程序。App生命周期时间轴将App启动期间的活动划分为进程初始化、UIKit初始化、UIKit初始场景渲染和初始帧渲染。

减少对外部框架和动态库的依赖

在你的任何代码运行之前,系统必须找到并加载你的应用程序的可执行文件和它所依赖的任何库。

动态加载器(dyld)加载应用程序的可执行文件,并检查可执行文件中的Mach加载命令,以查找应用程序需要的框架(frameworks)和动态库。然后它将每个框架加载到内存中,并解析可执行文件中的动态符号以指向动态库中的适当地址。

你的应用加载的每一个额外的第三方框架都会增加启动时间。尽管dyld在用户安装应用程序时在启动闭包(launch closure)中缓存了大量的工作,但启动闭包的大小和加载后完成的工作量仍然取决于加载的库的数量和大小。你可以通过限制嵌入的第三方框架的数量来缩短应用程序的启动时间。在Xcode的Target编辑器中,你导入或添加到应用的链接框架和库设置中的框架都计入这个数字。像CoreFoundation这样的内置框架对启动的影响要小得多,因为它们与使用相同框架的其他进程使用共享内存

删除或减少静态初始化代码

应用程序中的某些代码必须在iOS运行你的应用程序的main()函数之前运行,这增加了启动时间。此代码包括:

  • c++ static constructors。
  • Objective-C + load在类或类别中定义的方法。
  • 带有clang属性__attribute__((constructor))标记的函数。
  • 任何链接到应用程序或框架二进制文件的__DATA__mod_init_func部分的函数。

在可能的情况下,将代码移到应用程序生命周期的后期阶段,即在应用程序完成启动后,但在需要工作结果之前。在Instruments中,静态初始化器调用工具测量应用程序运行静态初始化器所花费的时间。

将耗时(expensive)的任务移出应用程序委托

审查初始化代码以延迟耗时的工作。系统在启动周期中调用你的应用程序委托的方法,给你时间来执行所需的任务。这些方法在主线程上同步执行,直到两个方法都成功返回,启动周期才会结束。因此,从这些方法执行的任何开销很大的任务都会延迟该启动周期的完成。

UIKit初始化你的应用程序委托类的一个实例(符合UIApplicationDelegate协议的类),并向它发送 application(_:willFinishLaunchingWithOptions:)application(_:didFinishLaunchingWithOptions:)消息。UIKit在主线程上发送这些消息,在这些方法中执行代码所花费的时间增加了应用程序的启动时间。在这些方法中只做准备应用程序初始显示所需的工作;将其他任务推迟到应用程序生命周期中更合适的时间。

在刷新内容时向用户显示过时的内容是有意义的,将网络服务的数据模型的同步推迟到应用程序运行时。将同步移动到异步后台队列。注册一个后台任务来从网络服务中获取更新,以减少启动时数据的陈旧性和更新数据所需的工作量。

初始化非视图功能,如持久存储和位置服务,在第一次使用时,而不是在应用启动时。只检索显示应用程序初始视图所需的数据。请注意您的应用程序是否正在恢复状态,并准备显示正在恢复的视图所需的数据。如果没有恢复状态,只准备默认的初始视图。例如,一个图片库应用程序可能会在默认情况下显示一组图像缩略图,并让用户选择一张照片以获得详细视图。如果应用程序启动时没有恢复状态,它只需要显示一个占位符的一个屏幕的缩略图,并在应用程序完成启动后用真实的图像缩略图填充它们。它不需要加载完整的、详细的图片,直到用户点击其中一个缩略图。

初始化应用行为的受限子集,这在初始启动时是可行的。例如,一个任务管理器应用程序可以让用户在启动时创建一个新任务,即使应用程序还没有从它的持久存储或网络服务中检索到用户现有的所有任务。

降低初始视图的复杂性

Xcode OrganizerMetricKit都使用到第一帧的时间作为启动时间的度量,包括绘制在第一帧上显示的视图所需的时间。你只能在主线程上修改视图层次结构;因此,具有更多视图的更复杂的视图层次结构比简单的层次结构需要更长的渲染时间。

减少应用程序初始视图的复杂性可以提高加载时间,用标准视图替换重写(override)draw(_:)的自定义视图也是如此。在需要自定义视图的地方,注意传递给draw(_:)的矩形,只渲染该矩形内的部分视图。这样做可以避免在视图中未呈现到屏幕的部分解码图像和计算颜色、坐标和绘图命令。

跟踪额外的启动活动

启动时间度量从用户点击主屏幕上的应用程序图标到应用程序在屏幕上绘制第一帧的时间。在此期间绘制default.pnglaunch-screen storyboard,它的出现不会结束启动时间计数器。

如果你的应用在绘制了第一帧之后,但在用户可以开始使用应用之前,仍然需要运行代码,那么这段时间不会影响到启动时间指标。额外的启动活动仍然有助于用户感知应用程序的响应能力。例如,如果你的应用程序在打开后渲染一个文档,用户可能会等待文档渲染,并将其视为你的启动时间的一部分,即使系统将在你显示加载图标时结束启动测量。

要跟踪额外的启动活动,在应用程序中创建一个OSLog对象,类别为pointsOfInterest。使用os_signpost函数来记录应用程序准备任务的开始和结束,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
class ViewController: UIViewController {
static let startupActivities:StaticString = "Startup Activities"
let poiLog = OSLog(subsystem: "com.example.CocoaPictures", category: .pointsOfInterest)

override func viewDidAppear() {
super.viewDidAppear()
os_signpost(.begin, log: self.poiLog, name: ViewController.startupActivities)
// do work to prepare the view
os_signpost(.end, log: self.poiLog, name: ViewController.startupActivities)
}
}

在Instruments中,Points of Interest在其时间轴中显示os_signposts。你可以使用这些信息将应用程序中的活动与应用程序的附加启动任务关联起来。

参考

守护进程百科

启动优化

認識系統服務 (daemons)