用Instruments剖析SwiftUI代码和定位性能问题

译自 www.hackingwithswift.com/quick-start…

建议横屏阅读代码

Xcode 的 Instruments 工具自带了一组很棒的 SwiftUI 分析功能,使得我们能够检测视图重绘的频率,哪些视图的 body 计算过程较慢,以及我们的状态如何跟随时间变化。

首先我们需要来点可以帮助我们在 Instruments 里观察数据的东西。下面的代码创建了一个每 0.01 秒触发一次的定时器,还有一个显示随机 UUID 以及点击时刷新计数的按钮:

import Combine
import SwiftUI

class FrequentUpdater: ObservableObject {
    let objectWillChange = PassthroughSubject<Void, Never>()
    var timer: Timer?

    init() {
        timer = Timer.scheduledTimer(
            withTimeInterval: 0.01,
            repeats: true
        ) { _ in
            self.objectWillChange.send()
        }
    }
}

struct ContentView: View {
    @ObservedObject var updater = FrequentUpdater()
    @State private var tapCount = 0

    var body: some View {
        VStack {
            Text("\(UUID().uuidString)")

            Button("Tap count: \(tapCount)") {
                tapCount += 1
            }
        }
    }
}
复制代码

在模拟器中运行代码,你会发现界面正在不断地重绘 —— 因为有被观察的值正在不断地改变。

注意: 设计这个压力测试是为了让 SwiftUI 高负荷运作,以便在 Instruments 中看到我们感兴趣的数据 —— 在实际的项目中你可千万别这么干。

诊断代码

点击 Cmd+I ,通过 Instruments 运行代码,选择 SwiftUI instrument。Instrument 窗口出现后,点击录制按钮以启动 app。观察一会,点击按钮几次,然后停止 Instruments —— 这里我们应该有足够的数据了。

默认情况下 SwiftUI instrument 会提供下面几种信息:

  1. 运行期间有多少视图被创建,创建它们耗费了多少时间,即 (“View Body”)
  2. 视图的属性以及它们是如何随着时间变化的,即 (“View Properties”)
  3. 发生了多少次 Core Animation 提交,即 (“Core Animation Commits”)
  4. 每个函数调用耗费的时间 (“Time Profiler”)

这些 instruments 有助于你诊断和解决你的 SwiftUI 应用中的性能问题,值得你去了解它们。

对于我们这个小型压力测试,透过 Instruments 我们可以看到 View Body, View Properties 和 Core Animation Commits 几个track都被颜色鲜明的“小墙”填满了。显然,你要警惕起来。它说明不仅是 SwiftUI 正在不断地重建视图,我们的属性也在不断地变化,进而导致 Core Animation 也不需要不停地工作。

img

监测 body 调用

选择 View Body track —— 也就是 instruments 列表的第一行 —— 你应该会发现 Instruments 把监测结果按照 SwiftUI 和你的项目两个类别进行了归类,前者是 SwiftUI 里的原始类型,比如文本视图,按钮,而后者则包含了你的自定义视图类型。在我们的例子里,自定义视图里应该会出现 “ContentView”。

但是在这里你不会看到你的代码到 SwiftUI 视图的一对一完美映射,因为 SwiftUI 对视图体系采用了激进的折叠策略,以便尽可能少地工作。因此,别指望你能在里面找到 VStack 的创建代码 —— 在我们的 app 里它基本上是免费的(指它的开销可以忽略)

屏幕上比较重要的数字是 Count 和 Avg Duration —— 它们分别表示每样东西被创建的次数,以及创建过程耗费的时间。由于这是个压力测试,你应该会看到很大的 Count,但我们的布局是很细碎的,所以 Avg Duration 大概率只有几十微秒。

img

追踪状态变化

接下来,选择 View Properties track,也就是 instruments 列表中的第二行。它显示了所有视图的所有属性,包括它们的当前值和所有之前的值。

我们的例子 app 包含一个按钮,随着点击而增加计数并显示到标签上,你在 instrument 里就能找到相关的属性 —— 在 ContentView 里查找 Property Type 为 State<Int> 的属性。

遗憾的是,Instruments 似乎无法为我们显示精确的属性名,所以如果你有几个相同类型的状态,比如整数 state,可能会比较烦恼。不过,找到想要的属性还是有迹可循的:在录制窗口的顶部,有一个标注当前视图位置的箭头。尝试拖动这个箭头,你会发现应用的状态随着时间变化 —— 每当你点击按钮时,你会看到某个整数状态增大,你还可以快进或者快退来反复观察这个过程。

这个特性释放了非常强大的力量,因为它让我们可以直接看到状态的变化以及这些过程是如何导致重绘变慢以及其他的(性能问题)。它几乎就像给我们提供了一台时间机器 —— 借助这台机器,我们可以检视 app 在运行时每个时刻的精确状态。

img

定位过慢的绘制

尽管 SwiftUI 可以往下借助 Metal 提升性能,多数情况它是优先选择 Core Animation 来渲染的。这意味着我们能从 Instruments 中自动获得包括侦测昂贵提交开销在内的内置的 Core Animation 分析工具。

当多个变化被放到单个分组里时,Core Animation 的工作效率最高,这也被称为“事务”。实际上,我们的做法是把一些工作堆到一个事务中,然后要求CA 处理渲染工作 —— 这个过程就叫“提交”事务。

因此,当 Instruments 向我们展示昂贵的 Core Animation 提交时,它实际上是在向我们展示 SwiftUI 由于更新而被迫在屏幕上重绘的次数。理论上,重绘应该只在我们的应用的某个实际的状态发生改变,导致一个不同视图层级时才应该发生,这是基于 SwiftUI 能够比较新的 body 属性输出和之前的输出的前提。

img

查找过慢的函数调用

最后一条重要的 track 是 Time Profiler,它向我们展示我们的代码的各个部分精确的运行时间。它的工作方式和 Instruments 里常规的时间分析器一样。谨防你没有尝试过这类工具,这里说明几点你需要了解的知识点:

  1. 右边展开的细节窗口默认向你展示了最重的栈跟踪,也就是消耗最多运行时间的代码。明亮色的代码是你写的代码,暗淡色(灰色)的代码是系统库的代码。
  2. 左边你可以看到所有被创建的线程,它们都带有披露箭头,可以让你展开查看它们调用的函数以及这些函数调用的函数,等等。通常,大部分工作会开始于 “start”。
  3. 为了避免显示过多的条目,你可能需要点击底部的 Call Tree 按钮,然后选择 Hide System Libraries,这样就只会显示你写的代码。除非你的问题是由滥用或者错误使用系统库造成的,这样做都会有帮助。
  4. 为了直达具体细节,你也可以点击 Call Tree,然后选择 Invert Call Tree 来反转叶子节点的函数 —— 那些在调用树最末尾的函数 —— 现在显示在最顶部了。

虽然时间分析器对于定位性能问题相当有用,一般只需要关注最重的调用栈,就能发现最大的那个问题。

img

最后一些提示

  1. 检查应用的某个部分时,你应该点击并且拖拽出相关的范围,以便聚焦于特定动作的性能问题,比如某个按钮的点击响应。
  2. 你在 Instruments 里的所见通常是不同颜色的色块,这只是它们的远景 —— 你可以通过按住 Cmd,点击 - 和 + 来看更多细节。
  3. 为了取得最精确的图像,在真机上采样分析。
  4. 改动代码并且重新采样分析时,记住每次都只做出一个改变。如果你做了两个改动,可能一个提升了 20% 的性能而另一个降低了 10%,结果最后你以为整体提升了 10%。
  5. Instruments 是以 release 模式运行你的代码,它会启用所有 Swift 的优先选项,因此也会影响到你添加到代码中的调试标记,留意这一点。

更多内容,欢迎关注公众号 「Swift花园」

分类:
iOS
标签: