WWDC Quick Look 💓 By SwiftGGTeam
Ultimate application performance survival guide

Ultimate application performance survival guide

观看原视频

Highlight

Apple 用 Xcode Organizer、MetricKit、Instruments、XCTest 和 App Store Connect API 串起电池、启动、卡顿、内存、磁盘写入、滚动和终止率等性能指标,让开发者能在开发、测试和线上发布后持续发现性能回归。

核心内容

性能问题先出现在用户手里

MealPlanner 加了保存食谱和图片的功能后,问题开始出现。列表滚动会抖,启动时首帧迟迟不出现,保存一次数据可能写入太多文件。开发者在桌面上不一定能复现,但用户已经在真实设备上遇到了这些问题。

过去处理这类问题,常见做法是等用户反馈,再尝试猜测原因。电池、启动、卡顿、内存和磁盘写入各有工具,数据分散,排查路径很长。一个新版本上线后,如果某个指标变差,开发者需要先知道哪里变差,再找到对应代码。

Apple 在这场 session 里给出了一条完整路径。开发中用 Xcode Debug navigator 和 Instruments 观察现场;测试中用 XCTest 写性能测试;线上用 Xcode Organizer、MetricKit 和 App Store Connect API 读取真实设备上的聚合数据。

这条路径解决的是同一个痛点:性能优化不能只靠感觉。每个问题都要有指标,每个指标都要能回到一次用户动作或一段代码。

八个指标对应五套工具

演讲先列出八个核心指标:Battery Usage、Launch Time、Hang Rate、Memory、Disk Writes、Scrolling、Terminations 和 MXSignposts(01:54)。这些指标覆盖了用户最容易感知的体验:耗电、慢启动、界面无响应、滚动卡顿、被系统杀掉。

工具也分层。Xcode Organizer 看线上版本趋势,MetricKit 收集设备端遥测,Instruments 在开发机上定位调用栈,XCTest 把性能要求写成测试,App Store Connect API 让团队把同样的数据接入自己的分析系统。

详细内容

用 MetricKit 收集线上遥测

05:46)MetricKit 是设备端性能遥测框架。App 只要注册 MXMetricManagerSubscriber,就能接收每日指标和诊断 payload。演讲提到,电池日志、CPU 指标等数据可以和 App 自己的上下文结合,用来缩小线上问题的根因范围。

import MetricKit

class AppMetrics: MXMetricManagerSubscriber {
    init() {
        let shared = MXMetricManager.shared
        shared.add(self)
    }

    deinit {
        let shared = MXMetricManager.shared
        shared.remove(self)
    }

    // Receive daily metrics
    func didReceive(_ payloads: [MXMetricPayload]) {
        // Process metrics
    }

    // Receive diagnostics
    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        // Process metrics
    }
}

关键点:

  • import MetricKit 引入 MetricKit API。
  • AppMetrics 遵守 MXMetricManagerSubscriber,表示这个对象可以接收性能数据。
  • MXMetricManager.shared 取得全局 metric manager。
  • shared.add(self) 在初始化时注册订阅者。
  • shared.remove(self) 在释放时移除订阅者,这是演讲中推荐的做法。
  • didReceive(_ payloads: [MXMetricPayload]) 接收每日指标 payload。
  • didReceive(_ payloads: [MXDiagnosticPayload]) 接收诊断 payload。

07:00)同一批线上数据也会进入 Xcode Organizer。开发者可以查看最近 16 个 App 版本的电池使用趋势,并在 Xcode 13 新增的 Regression pane 里看到最新版本显著上升的指标。

用 XCTest 测量滚动卡顿

10:29)滚动卡顿来自一个具体事实:新内容没有在下一次屏幕刷新前准备好。用户看到的是列表跳帧、抖动和停顿。演讲用 XCTOSSignpostMetric.scrollDecelerationMetric 写了一个滚动性能测试,防止带卡顿的版本发布。

func testScrollingAnimationPerformance() throws {

    app.launch()
    app.staticTexts["Meal Planner"].tap()
    let foodCollection = app.collectionViews.firstMatch

    let measureOptions = XCTMeasureOptions()
    measureOptions.invocationOptions = [.manuallyStop]

    measure(metrics: [XCTOSSignpostMetric.scrollDecelerationMetric],
    options: measureOptions) {
        foodCollection.swipeUp(velocity: .fast)
        stopMeasuring()
        foodCollection.swipeDown(velocity: .fast)
    }
}

关键点:

  • app.launch() 启动 UI 测试目标 App。
  • app.staticTexts["Meal Planner"].tap() 进入 Meal Planner 页面。
  • app.collectionViews.firstMatch 找到第一个 collection view,作为滚动对象。
  • XCTMeasureOptions() 创建测量选项。
  • .manuallyStop 让测试手动停止测量,用来排除重置状态的动作。
  • XCTOSSignpostMetric.scrollDecelerationMetric 指定测量滚动减速阶段。
  • foodCollection.swipeUp(velocity: .fast) 执行快速上滑,这是被测动作。
  • stopMeasuring() 停止测量。
  • foodCollection.swipeDown(velocity: .fast) 把界面滚回去,为下一次测量恢复状态。

11:25)iOS 15 和 macOS 12 中,MetricKit 会在问题发生后立即交付诊断,包括 hang diagnostics。线上问题不需要等到第二天的每日 payload,开发者可以更快拿到现场数据。

用 MXSignpost 标记自定义动画

11:53)滚动之外,App 里还有自定义动画。iOS 15 在 MetricKit 中加入 MXSignpost 相关 API,用来给关键代码区间打点。mxSignpostAnimationIntervalBegin 标记动画开始,mxSignpost.end 标记结束,MetricKit 会收集这段区间里的 hitch-rate telemetry。

import MetricKit

func startAnimating() {
    // Mark the beginning of animations
    mxSignpostAnimationIntervalBegin(
        log: MXMetricManager.makeLogHandle(category: "animation_telemetry"),
        name: "custom_animation")
}

func animationDidComplete() {
    // Mark the end of the animation to receive the collected hitch rate telemetry
    mxSignpost(OSSignpostType.end,
        log: MXMetricManager.makeLogHandle(category: "animation_telemetry"),
        name: "custom_animation")
}

关键点:

  • MXMetricManager.makeLogHandle(category:) 创建 MetricKit 使用的 log handle。
  • category: "animation_telemetry" 给动画遥测分组,便于后续分析。
  • mxSignpostAnimationIntervalBegin 标记动画区间开始。
  • name: "custom_animation" 给这段动画命名。
  • animationDidComplete() 在动画完成时调用。
  • mxSignpost(OSSignpostType.end, ...) 标记同名动画区间结束。
  • 开始和结束使用同一组 logname,MetricKit 才能把区间对应起来。

用 XCTest 测量磁盘写入

13:51)频繁写磁盘会拖慢操作,也会影响设备 NAND 健康。演讲建议先用 Instruments 的 File Activity 模板查看文件系统调用,再用 XCTStorageMetric 把关键路径写成性能测试。

// Example performance XCTest

func testSaveMeal() {
    let app = XCUIApplication()
    let options = XCTMeasureOptions()
    options.invocationOptions = [.manuallyStart]

    measure(metrics: [XCTStorageMetric(application: app)], options: options) {
        app.launch()
        startMeasuring()

        let firstCell = app.cells.firstMatch
        firstCell.buttons["Save meal"].firstMatch.tap()

        let savedButton = firstCell.buttons["Saved"].firstMatch
        XCTAssertTrue(savedButton.waitForExistence(timeout: 2))
    }
}

关键点:

  • XCUIApplication() 创建 UI 测试中的 App 对象。
  • XCTMeasureOptions() 创建测量配置。
  • .manuallyStart 让测试在 App 启动后再开始测量。
  • XCTStorageMetric(application: app) 指定测量这个 App 的磁盘写入。
  • measure(metrics:options:) 包住要测量的用户操作。
  • app.launch() 启动 App,但此时还没开始测量。
  • startMeasuring() 从保存动作前开始记录磁盘写入。
  • firstCell.buttons["Save meal"].firstMatch.tap() 模拟用户保存一餐。
  • XCTAssertTrue(savedButton.waitForExistence(timeout: 2)) 确认保存完成。

14:16)这个测试可以设置 baseline。超过预期写入量时,测试会失败。线上版本已经发布后,Organizer 的 Disk Writes metric 可以显示当前版本相对旧版本的趋势。超过 1 GB/24 小时的写入还会生成 Disk Writes Reports,并带有 stack trace。

用工具链追踪启动、终止和内存

19:00)启动时间指用户点击图标到 App 渲染第一帧之间的时间。App Launch 模板会运行 App 五秒,收集 time profile 和 Thread State Trace,帮助开发者看到启动期间线程为什么被阻塞。

func testLaunchPerformance() throws {
    let app = XCUIApplication()

    measure(metrics: [XCTApplicationLaunchMetric()]) {
        app.launch()
    }
}

关键点:

  • XCUIApplication() 创建被测 App。
  • measure(metrics:) 建立性能测量块。
  • XCTApplicationLaunchMetric() 指定测量启动性能。
  • app.launch() 是被测动作,对应用户点击图标后的启动流程。

21:19)内存是 App、系统和内核共享的资源。App 超过内存限制会被系统终止,下一次用户打开时只能走完整启动流程。演讲建议用 Instruments 的 Leaks、Allocations、VM Tracker 模板分析内存,也可以用 MXSignpost 包住关键代码段,收集更细的内存遥测。

import MetricKit

// Collect memory telemetry

func saveAppAssets() {
    mxSignpost(OSSignpostType.begin,
        log: MXMetricManager.makeLogHandle(category: "memory_telemetry"),
        name: "custom_memory")

    // save app metadata

    mxSignpost(OSSignpostType.end,
        log: MXMetricManager.makeLogHandle(category: "memory_telemetry"),
        name: "custom_memory")
}

关键点:

  • mxSignpost(OSSignpostType.begin, ...) 标记关键代码段开始。
  • category: "memory_telemetry" 把这段遥测归到内存分析类别。
  • name: "custom_memory" 给区间命名。
  • // save app metadata 代表实际需要观察的资源保存逻辑。
  • mxSignpost(OSSignpostType.end, ...) 标记区间结束。
  • begin 和 end 使用相同的 category 与 name,便于 MetricKit 汇总这一段的内存数据。

核心启发

  1. 做什么:给 App 做一个性能健康页,只显示最近版本的电池、启动、卡顿和磁盘写入趋势。 为什么值得做:Xcode Organizer 和 App Store Connect API 已经提供线上聚合数据,团队可以把性能回归变成每日可见的指标。 怎么开始:先在 Organizer 手动查看 Battery Usage、Launch Time、Hang Rate 和 Disk Writes,再用 App Store Connect API 拉取同类 JSON 数据接入内部看板。

  2. 做什么:为首页列表写滚动性能测试。 为什么值得做:滚动卡顿直接影响用户停留时间,XCTOSSignpostMetric.scrollDecelerationMetric 可以把卡顿风险挡在发布前。 怎么开始:用 UI Test 打开列表页,在 measure(metrics:) 中执行 swipeUp(velocity:),用 XCTMeasureOptions 排除重置状态的步骤。

  3. 做什么:给图片导入、缓存刷新、批量保存这类重 I/O 操作加磁盘写入测试。 为什么值得做:演讲指出频繁写盘会拖慢体验,Organizer 还会在 24 小时写入超过 1 GB 时生成报告。 怎么开始:用 XCTStorageMetric(application:) 包住一次典型保存动作,设置 baseline,让超过预期写入量的代码在测试阶段失败。

  4. 做什么:为自定义动画和资源保存路径加 MXSignpost。 为什么值得做:MetricKit 可以围绕自定义区间收集 hitch rate 和内存遥测,线上问题能对应到业务动作。 怎么开始:用 MXMetricManager.makeLogHandle(category:) 创建分类,在关键路径开始和结束分别调用 mxSignpostAnimationIntervalBeginmxSignpost

  5. 做什么:给启动流程建立性能基线。 为什么值得做:启动慢可能导致系统终止,用户下一次打开会重新走完整启动流程。 怎么开始:在 XCTest 中使用 XCTApplicationLaunchMetric() 测量启动,再用 Instruments 的 App Launch 模板查看启动前五秒的 time profile 和 Thread State Trace。

关联 Session

评论

GitHub Issues · utterances