mcpField
A
axiom-performance-profiling

axiom-performance-profiling

github.com/charleswiltgen开发1.2k185 次安装今天更新
代码仓库

关于 axiom-performance-profiling

面向 iOS 开发者的性能分析参考,帮助根据卡顿、内存增长或耗电等症状选择 Instruments 工具并解读结果。内容包含针对 CPU、内存、Core Data 和能耗问题的诊断流程。

主要功能

  1. 01按性能症状选择分析工具
  2. 02Time Profiler CPU 分析
  3. 03Allocations 内存分析
  4. 04Core Data 查询诊断
  5. 05识别分析结果中的常见误区

适用场景

  1. 01定位界面卡顿或 CPU 峰值
  2. 02调查内存持续增长
  3. 03分析 Core Data 加载缓慢

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-performance-profiling

安装后在 Claude Code 中输入/axiom-performance-profiling确认已加载。

详细介绍

性能分析

概述

iOS 应用的性能问题可分为不同类别,每类都有对应的诊断工具。本技能可帮助你在时间紧迫时选择合适的工具、有效使用工具并正确解读结果。

核心原则:先测量,再优化。凭猜测判断性能问题,比进行性能分析更浪费时间。

要求:Xcode 15+、iOS 14+ 相关技能:axiom-swiftui-performance(使用 Instruments 26 进行 SwiftUI 专项分析)、axiom-memory-debugging(内存泄漏诊断)

何时使用性能分析

适用于以下情况
  • ✅ 应用运行缓慢(界面卡顿、加载耗时 5 秒以上)
  • ✅ 内存随时间增长(Xcode 显示内存用量不断增加)
  • ✅ 电池耗电快(设备发烫、电池几小时内耗尽)
  • ✅ 想主动进行性能分析(赶在用户投诉之前)
  • ✅ 不确定该使用哪种 Instruments 工具
  • ✅ 性能分析结果令人困惑或相互矛盾
以下情况改用 axiom-memory-debugging
  • 排查带有保留环的特定内存泄漏
  • 在详细模式下使用 Instruments Allocations
以下情况改用 axiom-swiftui-performance
  • 分析 SwiftUI 视图主体的更新
  • 专门使用 SwiftUI Instrument

性能问题决策树

打开 Instruments 之前,先缩小范围,明确你实际要排查的问题。

步骤 1:症状是什么?
App performance problem?
├─ App feels slow or lags (UI interactions stall, scrolling stutters)
│  └─ → Use Time Profiler (measure CPU usage)
├─ Memory grows over time (Xcode shows increasing memory)
│  └─ → Use Allocations (measure object creation)
├─ Data loading is slow (parsing, database queries, API calls)
│  └─ → Use Core Data instrument (if using Core Data)
│  └─ → Use Time Profiler (if it's computation)
└─ Battery drains fast (device gets hot, depletes in hours)
   └─ → Use Energy Impact (measure power consumption)
步骤 2:能否复现?

能 – 使用 Instruments 进行测量(性能分析最准确)

不能 – 主动进行性能分析

  • 启用 Core Data SQL 调试,捕获 N+1 查询
  • 在正常使用过程中分析应用(滚动、加载、导航)
  • 在进行更改前建立基准指标
步骤 3:使用哪种 Instruments 工具?

Time Profiler – 运行缓慢、界面卡顿、CPU 峰值 Allocations – 内存增长、内存压力、对象数量 Core Data – 查询性能、获取耗时、故障触发 Energy Impact – 电池耗电、持续功耗 Network Link Conditioner – 网络连接导致的运行缓慢 System Trace – 线程阻塞、主线程阻塞、调度


深入了解 Time Profiler

当应用运行缓慢或卡顿时,使用 Time Profiler。它会测量各函数消耗的 CPU 时间。

工作流程:录制并分析
步骤 1:启动 Instruments
open -a Instruments

选择“Time Profiler”模板。

步骤 2:连接到正在运行的应用
  1. 在模拟器或设备上启动应用
  2. 在 Instruments 中,从目标下拉菜单中选择应用
  3. 点击录制(红色圆圈)
  4. 操作运行缓慢的部分(滚动、点击按钮、加载数据)
  5. 交互 10-30 秒后停止录制
步骤 3:读取调用栈

顶部面板会显示 CPU 使用情况随时间变化的时间线。请留意:

  • 高峰值 – 短暂的 CPU 密集型操作
  • 持续高使用率 – 持续进行的高开销工作
  • 主线程阻塞 – UI 线程正在执行工作(会导致界面卡顿)
步骤 4:深入查找热点

在调用树中,点击“Heaviest Stack Trace”,查看哪些函数占用的 CPU 最多:

Time Profiler Results

MyViewController.viewDidLoad() – 500ms (40% of total)
  ├─ DataParser.parse() – 350ms
  │  └─ JSONDecoder.decode() – 320ms
  └─ UITableView.reloadData() – 150ms

Self Time = 在该函数内部花费的时间(不包括其调用的函数) Total Time = 在该函数及其调用的所有函数中花费的时间

常见错误与修复方法
❌ 错误 1:归咎于错误的函数
// ❌ WRONG: Profile shows DataParser.parse() is 80% CPU
// Conclusion: "DataParser is slow, let me optimize it"

// ✅ RIGHT: Check what DataParser is calling
// If JSONDecoder.decode() is doing 99% of the work,
// optimize JSON decoding, not DataParser

问题:Total Time 较高的函数可能只是调用了运行缓慢的代码,而非自身执行缓慢。

修复方法:查看 Self Time,而不是 Total Time。深入查看每个函数调用了什么。

❌ 错误 2:分析了错误的代码路径
// ❌ WRONG: Profile app in Simulator
// Simulator CPU is different than real device
// Results don't reflect actual device performance

// ✅ RIGHT: Profile on actual device
// Device settings: Developer Mode enabled, Xcode attached

修复方法:始终在实际设备上进行性能分析,以获得准确的 CPU 测量结果。

❌ 错误 3:没有隔离问题
// ❌ WRONG: Profile entire app startup
// Sees 2000ms startup time, many functions involved

// ✅ RIGHT: Profile just the slow part
// "App feels slow when scrolling" → profile only scrolling
// Separate concerns: startup slow vs interaction slow

修复方法:复现具体的缓慢操作,而不是整个应用。

压力场景:“性能分析器显示函数 X 占用 80% CPU”

容易产生的想法:“我必须优化函数 X!”

实际情况:函数 X 可能:

  • 调用了开销很大的代码(应该优化被调用的函数,而不是 X)
  • 在主线程上运行(将其移到后台;它本身已经过优化)
  • 执行了看起来很慢的必要工作(基准性能可以接受,用户不会察觉)

应该怎么做:

  1. 查看自身耗时,而不是总耗时

    • 自身耗时占 80%?函数确实在执行开销很大的工作
    • 自身耗时占 5%,总耗时占 80%?函数调用了运行缓慢的代码
  2. 向下深入一层

    • 这个函数调用了什么?
    • 慢代码是否位于你能控制的库中?
  3. 查看时间线

    • 这 80% 是持续存在(一直缓慢),还是偶尔出现峰值(偶发卡顿)?
    • 持续存在 = 需要优化
    • 偶尔出现峰值 = 缓存可能会有帮助
  4. 思考:用户会注意到吗?

    • 后台工作耗时 500 ms = 用户不会注意到
    • 主线程耗时 500 ms = UI 卡顿,用户会看到
    • 每帧主线程耗时 50 ms = UI 流畅(60 fps)

耗时:5 分钟(查看结果)+ 2 分钟(深入分析)= 7 分钟即可了解情况

盲目猜测的代价:花 2 小时优化了错误的函数 + 花 1 小时发现并没有帮助 + 回到原点 = 浪费 3 小时以上


深入了解 Allocations

当内存随时间增长,或你怀疑存在内存压力问题时,请使用 Allocations。

工作流:录制和分析
步骤 1:启动 Instruments
open -a Instruments

选择“Allocations”模板。

步骤 2:附加并录制
  1. 启动你的应用
  2. 在 Instruments 中选择你的应用
  3. 点击 Record
  4. 执行会使用内存的操作(加载数据、显示图像、导航)
  5. 等内存稳定或达到峰值后停止录制
步骤 3:查找内存增长

查看主图表:

  • 蓝线 = 总分配量
  • 陡升 = 正在分配内存
  • 平线 = 内存稳定(良好)
  • 操作停止后没有下降 = 可能存在泄漏(或缓存)
步骤 4:识别持久对象

在“Statistics”下:

  • 按“Persistent”(仍存活的对象)排序
  • 查找数量出乎意料地多的对象:
    UIImage: 500 instances (300MB) – Should be <50 for normal app
    NSString: 50000 instances – Should be <1000
    CustomDataModel: 10000 instances – Should be <100
    
常见错误与修复方法
❌ 错误 1:将“内存增长”误认为“内存泄漏”
// ❌ WRONG: Memory went from 100MB to 500MB
// Conclusion: "There's a leak, memory keeps growing!"

// ✅ RIGHT: Check what caused the growth
// Loaded 1000 images (normal)
// Cached API responses (normal)
// User has 5000 contacts (normal)
// Memory is being used correctly

问题:内存增长 ≠ 泄漏。应用加载数据时,内存使用量合理增长是正常的。

修复方法:在 Allocations 中检查对象数量。如果图像或数据的数量与加载的内容相符,就属于正常情况。如果没有执行任何操作,但对象数量仍持续增长,则可能存在泄漏。

❌ 错误 2:没有考虑缓存
// ❌ WRONG: Allocations shows 1000 UIImages in memory
// Conclusion: "Memory leak, too many images!"

// ✅ RIGHT: Check if this is intentional caching
// ImageCache holds up to 1000 images by design
// When memory pressure happens, cache is cleared
// Normal behavior

修复方法:区分预期的缓存和实际的泄漏。发生内存压力时,泄漏不会释放内存。

❌ 错误 3:性能分析时间太短
// ❌ WRONG: Record for 5 seconds, see 200MB
// Conclusion: "App uses 200MB, optimize memory"

// ✅ RIGHT: Record for 2-3 minutes, see full lifecycle
// Load data: 200MB
// Navigate away: 180MB (20MB still cached)
// Navigate back: 190MB (cache reused)
// Real baseline: ~190MB at steady state

修复方法:进行足够长时间的性能分析,以观察内存是否稳定。短时间录制只能捕捉到暂时的峰值。

压力场景:“内存 500MB,这是泄漏!”

诱惑:“删除缓存,减少对象创建,优化数据结构”

现实:500MB 真的很大吗?

  • iPhone 14 Pro 有 6GB RAM
  • Instagram 加载时使用 400-600MB
  • 照片 App 在浏览大型图库时使用 500MB+
  • 500MB 可能完全正常

应该怎么做:

  1. 在真机上建立基线

    # On device, open Memory view in Xcode
    Xcode → Debug → Memory Debugger → Check "Real Memory" at app launch
    
  2. 检查对象数量,而不是总内存

    • Allocations → Statistics → “Persistent”
    • 图像、视图或数据对象的数量是否是预期的 10 倍?
    • 如果是,调查该对象类型
    • 如果否,内存可能没问题
  3. 在内存压力下测试

    • Xcode → Debug → Simulate Memory Warning
    • 内存是否下降 50%+?这是缓存(正常)
    • 内存是否保持高位?调查持久对象
  4. 分析真实用户旅程

    • 加载数据(像用户那样)
    • 四处导航(像用户那样)
    • 返回 App(从后台)
    • 检查每一步的内存

时间成本:5 分钟(启动 Allocations)+ 3 分钟(记录 App 使用)+ 2 分钟(分析)= 10 分钟

猜测的代价:删除缓存以“减少内存” → App 每个屏幕都重新加载数据 → App 变慢 → 用户抱怨 → 回滚更改 = 浪费 2+ 小时


Core Data 深入探讨

当你的 App 使用 Core Data 且数据加载缓慢时,使用 Core Data 工具。

工作流:启用 SQL 调试并分析
步骤 1:启用 Core Data SQL 日志记录

在 Xcode 的启动参数中添加:

Edit Scheme → Run → Arguments Passed On Launch
Add: -com.apple.CoreData.SQLDebug 1

现在 SQLite 查询会打印到控制台:

CoreData: sql: SELECT ... FROM tracks WHERE artist = ? (time: 0.015s)
CoreData: sql: SELECT ... FROM albums WHERE id = ? (time: 0.002s)
步骤 2:识别 N+1 查询问题

在典型用户操作(加载列表、滚动、筛选)期间观察控制台:

❌ BAD: Loading 100 tracks, then querying album for each
SELECT * FROM tracks (time: 0.050s) → 100 tracks
SELECT * FROM albums WHERE id = 1 (time: 0.005s)
SELECT * FROM albums WHERE id = 2 (time: 0.005s)
SELECT * FROM albums WHERE id = 3 (time: 0.005s)
... 97 more queries
Total: 0.050s + (100 × 0.005s) = 0.550s

✅ GOOD: Fetch tracks WITH album relationship (eager loading)
SELECT tracks.*, albums.* FROM tracks
LEFT JOIN albums ON tracks.albumId = albums.id
(time: 0.050s)
Total: 0.050s
步骤 3:使用 Core Data 工具分析
open -a Instruments

选择“Core Data”模板。

在执行缓慢操作时记录:

Core Data Results

Fetch Requests: 102
Average Fetch Time: 12ms
Slow Fetch: "SELECT * FROM tracks" (180ms)

Fault Fires: 5000
  → Object accessed, requires fetch from database
  → Should use prefetching
常见错误与修复
❌ 错误 1:未正确使用关系
// ❌ WRONG: Fetch tracks, then access album for each
let tracks = try context.fetch(Track.fetchRequest())
for track in tracks {
    print(track.album.title)  // Fires individual query for each
}
// Total: 1 + N queries

// ✅ RIGHT: Fetch with relationship prefetching
let request = Track.fetchRequest()
request.returnsObjectsAsFaults = false
request.relationshipKeyPathsForPrefetching = ["album"]
let tracks = try context.fetch(request)
for track in tracks {
    print(track.album.title)  // Already loaded
}
// Total: 1 query

修复:使用 relationshipKeyPathsForPrefetching 提前加载相关对象。

❌ 错误 2:未使用批处理
// ❌ WRONG: Fetch 50,000 records all at once
let request = Track.fetchRequest()
let allTracks = try context.fetch(request)  // Huge memory spike

// ✅ RIGHT: Batch fetch in chunks
let request = Track.fetchRequest()
request.fetchBatchSize = 500  // Fetch 500 at a time
let allTracks = try context.fetch(request)  // Memory efficient

修复:对大型数据集使用 fetchBatchSize。

❌ 错误 3:未使用故障机制来减少内存
// ❌ WRONG: Keep all objects in memory
let request = Track.fetchRequest()
request.returnsObjectsAsFaults = false  // Keep all in memory
let allTracks = try context.fetch(request)  // 50,000 objects
// Memory spike if you don't use all of them

// ✅ RIGHT: Use faults (lazy loading)
let request = Track.fetchRequest()
// request.returnsObjectsAsFaults = true (default)
let allTracks = try context.fetch(request)  // Just references
// Only load objects you actually access

修复:除非你需要提前获取所有对象,否则保持 returnsObjectsAsFaults 为默认值(true)。

压力场景:“Core Data 查询很慢,重新设计 Schema!”

诱惑:“Schema 错了,我需要重构一切”

现实:99% 的“Core Data 慢”是由于:

  • ❌ 缺少索引
  • ❌ N+1 查询问题
  • ❌ 一次获取太多数据
  • ❌ 未使用批大小或预取

重新设计 Schema 是最后才应该尝试的。

应该怎么做:

  1. 启用 SQL 调试(2 分钟)

    • 添加 -com.apple.CoreData.SQLDebug 1 启动参数
    • 观察执行了哪些查询
  2. 寻找 N+1 模式(3 分钟)

    • 获取 100 个对象,然后为相关数据执行单独查询?
    • 添加关系预取
  3. 如果需要,添加索引(5 分钟)

    • @NSManaged var artist: String 且频繁筛选?
    • 在 Schema 中添加 @Index
  4. 测试改进(2 分钟)

    • 重新运行相同的操作
    • 比较查询数量和总时间
    • 如果快了 10 倍,就完成了
    • 如果仍然慢,进入步骤 5
  5. 只有到那时才考虑 Schema 更改(30+ 分钟)

    • 但你很可能不会走到这一步

时间成本:诊断 + 修复 12 分钟 = 12 分钟

Schema 重新设计的代价:8 小时设计 + 4 小时迁移 + 2 小时测试 + 1 小时回滚 = 总计 15 小时


快速参考:其他工具

能耗影响(电池消耗)

适用场景:应用耗电很快,设备发热

工作流:

  1. 启动 Instruments → 选择 Energy Impact 模板
  2. 正常运行应用 5 分钟以上
  3. 查找持续出现的红色/橙色用量(不佳)
  4. 深入查看哪些子系统在消耗电量

关键指标:

  • 持续功耗 – 持续能耗(应尽可能低)
  • 峰值 – 短暂的高用量(可以接受)
  • CPU – 进程 CPU 时间
  • GPU – 图形渲染
  • 网络 – 蜂窝网络/WiFi 无线电
  • 定位 – GPS 用量

常见问题:

  • 持续使用 1m 精度更新位置(应为 100m)
  • 运行会反复唤醒设备的计时器
  • 网络请求过多(应改为批量请求)
  • 视图不可见时仍在播放动画
Network Link Conditioner(连接模拟)

适用场景:应用在 4G 下似乎很慢,希望无需出行即可进行测试

设置:

  1. 下载 Additional Tools for Xcode
  2. 安装 Network Link Conditioner
  3. 打开“系统设置” → Network Link Conditioner
  4. 选择配置文件(3G、LTE、WiFi Slow 等)
  5. 启用并激活配置文件
  6. 运行应用进行测试

关键配置文件:

  • 3G – 下行 1.6Mbps,上行 768Kbps,延迟 150ms
  • LTE – 下行 10Mbps,上行 5Mbps,延迟 20ms
  • WiFi Slow – 10Mbps,延迟 100ms
  • Custom – 设置自己的参数

注意:ui-testing 中也介绍了此工具,用于网络依赖型测试场景。

System Trace(线程阻塞、调度)

适用场景:UI 卡顿或不流畅,但 Time Profiler 显示 CPU 用量较低

常见原因:主线程被等待锁的后台任务阻塞

工作流:

  1. 启动 Instruments → 选择 System Trace 模板
  2. 在重现问题时录制
  3. 查找主线程间隙(已阻塞、未运行)
  4. 深入查看阻塞原因

关键指标:

  • 主线程间隙 – 空白区域 = 主线程空闲/阻塞
  • 核心调度 – 各线程的运行时机
  • 锁竞争 – 等待锁的线程

OSSignposter — 自定义性能检测

Time Profiler 通常用于查看 CPU 时间的去向,而 OSSignposter 可用于测量你定义的特定操作。它是 Apple 平台上进行自定义性能检测的主要工具。

适用场景
  • 测量特定操作的耗时(数据加载、图像处理、同步周期)
  • 为应用的操作创建自定义 Instruments 时间线轨道
  • 与自动化性能测试衔接(XCTOSSignpostMetric)
  • 测量跨越多个线程或 await 点的操作
基本 API
import os

let signposter = OSSignposter(subsystem: "com.app", category: "DataLoad")

// Interval measurement (start → end)
func loadData() async throws -> [Item] {
    let signpostID = signposter.makeSignpostID()
    let state = signposter.beginInterval("Load Items", id: signpostID)
    defer { signposter.endInterval("Load Items", state) }

    return try await fetchItems()
}

// Point of interest (single event)
func cacheHit(for key: String) {
    signposter.emitEvent("Cache Hit")
}
与 Instruments 集成
  1. 启动 Instruments → 添加“os_signpost”或“Points of Interest”检测器
  2. 录制应用执行已检测的操作
  3. Signpost 时间间隔会以彩色条形显示在时间线上
  4. 按子系统/类别筛选,聚焦于你的操作
何时使用 Signpost,何时使用 Time Profiler
需求工具
通用 CPU 热点Time Profiler
特定操作耗时OSSignposter
跨线程操作计时OSSignposter
自动化回归测试OSSignposter + XCTOSSignpostMetric

压力场景

场景 1:“每次运行的性能分析结果都不一样”

问题:你运行 3 次 Time Profiler,结果分别是 200ms、150ms、280ms。哪个才是正确的?

你可能会有的误区:

  • “结果不可靠,性能分析不准确。”
  • “我直接取平均值吧。”
  • “差异太大了,我没法优化。”

实际情况:结果有差异很正常。不同运行会遇到不同的:

  • 缓存状态(冷缓存 = 更慢)
  • 系统负载(其他应用正在运行)
  • CPU 频率(加速 / 降频)

建议这样做:

  1. 预热缓存(首次运行总是更慢)

    • 执行一次操作(冷缓存)
    • 再执行一次(热缓存)——使用这次的测量结果
  2. 控制系统负载

    • 关闭其他应用
    • 性能分析期间不要操作设备
    • 在设备上进行性能分析(不要使用模拟器)
  3. 观察规律

    • 多次运行:150ms、160ms、155ms(一致 = 良好)
    • 多次运行:150ms、280ms、240ms(不一致 = 需要调查)
    • 不一致说明存在间歇性问题,需要找出原因
  4. 以最慢的一次为准(最坏情况)

    • 如果范围是 150-280ms,就假设 280ms 是真实表现
    • 针对最坏情况进行优化

耗时:10 分钟(运行性能分析器 3 次)+ 2 分钟(解读结果)= 12 分钟

忽视差异的代价:遗漏间歇性性能问题 → 用户偶尔遇到卡顿 → 收到差评


场景 2:“Time Profiler 和 Allocations 显示的问题不同”

问题:Time Profiler 显示 JSON 解析很慢。Allocations 显示内存使用正常。应该修复哪个?

答案:两者都是真实问题,但优先级不同。

Time Profiler: JSONDecoder.decode() = 500ms
Allocations: Memory = 250MB (normal for app size)

Result: App is slow AND memory is fine
Action: Optimize JSON decoding (not memory)

常见冲突:

Time ProfilerAllocations处理方式
CPU 占用高内存正常优化计算(降低 CPU 占用)
CPU 占用低内存持续增长查找内存泄漏或减少对象创建
两者都高两者都高优先分析哪个问题对用户影响更大

建议这样做:

  1. 按用户影响确定优先级

    • 响应慢(UI 卡顿)= 优先修复
    • 内存问题(后台问题)= 其次修复
  2. 检查两者是否相关

    • JSON 解析会导致内存泄漏吗?(不会 → 是独立问题)
    • 内存增长会拖慢 CPU 吗?(可能 → 先修复内存问题)
  3. 按影响程度依次修复

    • JSON 解析慢:每次加载数据都会受影响
    • 内存正常:不会影响用户
    • → 修复 JSON 解析

耗时:5 分钟(分析两项结果)= 5 分钟

修错问题的代价:花 4 小时优化本来就正常的内存使用 → 用户体验没有改善


场景 3:“截止日期临近时进行性能分析”

情况:经理说:“我们 2 小时后发布。性能可以接受吗?”

你可能会有的误区:

  • “性能分析太耗时了,我直接问用户吧。”
  • “我没时间好好做性能分析,按现状发布吧。”
  • “快速跑一次就能知道有没有问题。”

实际情况:性能分析总共只需 15-20 分钟,只占剩余时间的 1%。

建议这样做:

  1. 分析关键路径(3 分钟)

    • 用户最常做的操作(加载列表、滚动、搜索)
    • 不分析整个应用,只分析慢的部分
  2. 正确记录一次运行结果(5 分钟)

    • 首次使用冷缓存
    • 第二次使用热缓存
    • 使用热缓存的结果
  3. 快速解读(5 分钟)

    • Time Profiler:主线程上是否有任何操作耗时超过 100ms?(如果没有,就没问题)
    • Allocations:内存是否持续增长?(如果没有,就没问题)
  4. 放心发布(2 分钟)

    • 如果结果可以接受,就发布
    • 如果不可以,你还有 90 分钟可以修复问题或推迟发布

耗时:性能分析 15 分钟 + 分析 5 分钟 = 20 分钟

不做性能分析的代价:在不了解性能表现的情况下发布 → 用户遇到卡顿 → 收到差评 → 两周后紧急发布热修复版本

算一算:现在花 20 分钟做性能分析 << 发布后花 2 周以上处理问题


CLI 快速检查(无需 Instruments)

Xcode 自带 CLI 性能分析工具,无需打开 Instruments 即可快速检查。

CPU 性能分析
# Quick 5-second CPU sample of running app
xcrun sample MyApp 5

# Sample by PID, save to file for analysis
xcrun sample 12345 5 -file output.txt

适用场景:在决定进行完整的 xctrace 会话之前,快速检查 CPU。5 秒内即可查看哪些函数占用资源较多。

内存分析
# Quick leak check — is there a leak at all?
xcrun leaks MyApp

有关 heap、vmmap、stringdups 和完整的 CLI 诊断工作流,请参阅 axiom-memory-debugging。

无头 Instruments(xctrace)
# CPU profile from CLI
xcrun xctrace record --instrument 'CPU Profiler' --attach 'MyApp' --time-limit 10s --output cpu.trace

# Memory allocations from CLI
xcrun xctrace record --instrument 'Allocations' --attach 'MyApp' --time-limit 30s --output alloc.trace

有关 xctrace 的完整参考,请参阅 axiom-xctrace-ref。

快速参考

常见操作
// Time Profiler: Launch Instruments
open -a Instruments

// Core Data: Enable SQL logging
// Edit Scheme → Run → Arguments Passed On Launch
-com.apple.CoreData.SQLDebug 1

// Allocations: Check persistent objects
Instruments → Allocations → Statistics → sort "Persistent"

// Memory warning: Simulate pressure
Xcode → Debug → Simulate Memory Warning

// Energy Impact: Profile battery drain
Instruments → Energy Impact template

// Network Link Conditioner: Simulate 3G
System Preferences → Network Link Conditioner → 3G profile
决策树摘要
Performance problem?
├─ App feels slow/laggy?
│  └─ → Time Profiler (measure CPU)
├─ Memory grows over time?
│  └─ → Allocations (find object growth)
├─ Data loading is slow?
│  └─ → Core Data instrument (if using Core Data)
│  └─ → Time Profiler (if computation slow)
└─ Battery drains fast?
   └─ → Energy Impact (measure power)

实际案例

示例 1:识别 Core Data 中的 N+1 查询问题

场景:应用加载包含艺术家姓名的专辑列表。速度很慢(100 张专辑需要 5 秒以上)。你怀疑是 Core Data 导致的。

设置:先启用 SQL 日志

# Edit Scheme → Run → Arguments Passed On Launch
-com.apple.CoreData.SQLDebug 1

控制台中看到的内容:

CoreData: sql: SELECT ... FROM albums WHERE ... (time: 0.050s)
CoreData: sql: SELECT ... FROM artists WHERE id = 1 (time: 0.003s)
CoreData: sql: SELECT ... FROM artists WHERE id = 2 (time: 0.003s)
... 98 more individual queries
Total: 0.050s + (100 × 0.003s) = 0.350s

使用此技能进行诊断:

  • 获取 100 张专辑,然后为每张专辑的艺术家单独查询 = N+1 查询问题(Core Data 深入解析,第 302-325 行)

修复:

// ❌ WRONG: Each album access triggers separate artist query
let request = Album.fetchRequest()
let albums = try context.fetch(request)
for album in albums {
    print(album.artist.name)  // Extra query for each
}

// ✅ RIGHT: Prefetch the relationship
let request = Album.fetchRequest()
request.returnsObjectsAsFaults = false
request.relationshipKeyPathsForPrefetching = ["artist"]
let albums = try context.fetch(request)
for album in albums {
    print(album.artist.name)  // Already loaded
}

结果:0.350 秒 → 0.050 秒(快 7 倍)


示例 2:找出 UI 卡顿的真正原因

场景:加载视图时,应用 UI 会卡顿 1-2 秒。你的联合负责人说“到处添加后台线程处理”。你想先进行测量。

使用此技能的工作流(Time Profiler 深入解析,第 82-118 行):

  1. 打开 Instruments:
open -a Instruments
# Select "Time Profiler"
  1. 记录卡顿:
App launches
Time Profiler records
View loads
Stall happens (observe the spike in Time Profiler)
Stop recording
  1. 检查结果:
Call Stack shows:

viewDidLoad() – 1500ms
  ├─ loadJSON() – 1200ms (Self Time: 50ms)
  │   └─ loadImages() – 1150ms (Self Time: 1150ms) ← HERE'S THE CULPRIT
  ├─ parseData() – 200ms
  └─ layoutUI() – 100ms
  1. 应用此技能(第 173-175 行):
loadJSON() has Self Time: 50ms, Total Time: 1200ms
→ loadJSON() isn't slow, something it CALLS is slow
→ loadImages() has Self Time: 1150ms
→ loadImages() is the actual bottleneck
  1. 修复真正的问题:
// ❌ WRONG: Thread everything
DispatchQueue.global().async { loadJSON() }

// ✅ RIGHT: Thread only the slow part
func loadJSON() {
    let data = parseJSON()  // 50ms, fine on main

    // Move ONLY the slow part to background
    DispatchQueue.global().async {
        let images = loadImages()  // 1150ms, now background
        DispatchQueue.main.async {
            updateUI(with: images)
        }
    }
}

结果:1500 毫秒 → 350 毫秒(快 4 倍,主线程不再阻塞)

为何这很重要:你修复了真正的瓶颈(1150 毫秒),而不是盲目猜测线程处理方式。


示例 3:内存增长还是内存泄漏

场景:应用使用 30 分钟后,Allocations 显示内存从 150MB 增长到 600MB。你的经理说“内存泄漏了!”。你需要确认这是否属实。

使用此技能的工作流(Allocations 深入解析,第 199-277 行):

  1. 在 Instruments 中启动 Allocations

  2. 记录 3 分钟的正常应用使用情况:

User loads data → memory grows to 400MB
User navigates around → memory stays at 400MB
User goes to Settings → memory at 400MB
User comes back → memory at 400MB
  1. 检查 Allocations 统计信息:
Persistent Objects:
- UIImage: 1200 instances (300MB) ← Large count
- NSString: 5000 instances (4MB)
- CustomDataModel: 800 instances (15MB)
  1. 向此技能提出问题(第 220-240 行):
  • 是否确实加载了 1200 张图像?(用户加载了含 1000 张照片的照片图库)→ 是
  • 触发内存警告后,内存是否会下降?(使用 Xcode 模拟)→ 是,降至 180MB
  • 这是按预期工作的缓存吗?→ 是

诊断:不是内存泄漏。这是正常缓存(第 235-248 行)

Memory growing = apps using data users asked for
Memory dropping under pressure = cache working correctly
Memory staying high indefinitely = possible leak
  1. 结论:
// ✅ This is working correctly
let imageCache = NSCache<NSString, UIImage>()
// Holds up to 1200 images by design
// Clears when system memory pressure happens
// No leak

结果:无需采取任何措施。所谓的“泄漏”实际上是缓存正常发挥作用。


防止回归的流水线

性能优化并非随修复发布就结束。如果不检测回归,优化效果会随着时间推移悄然下降。这个三阶段流水线可以在每个阶段捕获回归。

三个阶段
阶段工具时机捕获内容
开发OSSignposter编写代码时特定操作的耗时
CIXCTest 性能测试每个 PR与基线相比的回归
生产MetricKit发布后真实场景中的性能退化
阶段 1:为代码添加检测(OSSignposter)

参阅上面的 OSSignposter 部分。在性能关键代码路径中添加 signpost 区间。

阶段 2:使用 XCTest 性能测试实现自动化
func testDataLoadPerformance() throws {
    let options = XCTMeasureOptions()
    options.iterationCount = 10

    measure(metrics: [
        XCTClockMetric(),        // Wall clock time
        XCTCPUMetric(),          // CPU time and cycles
        XCTMemoryMetric(),       // Peak physical memory
    ], options: options) {
        loadData()
    }
}
可用的 XCTMetric 类型
  • XCTClockMetric — 实际耗时
  • XCTCPUMetric — CPU 时间、已执行的指令数、周期数
  • XCTMemoryMetric — 测试期间的峰值物理内存
  • XCTStorageMetric — 对存储的逻辑写入
  • XCTOSSignpostMetric — signpost 区间的耗时(连接阶段 1 → 阶段 2)
  • XCTApplicationLaunchMetric — 应用启动时间(冷启动/热启动/优化启动)
  • XCTHitchMetric — 卡顿时间占比(滚动和动画卡顿)
设置基线

运行一次后,在 Xcode 的测试结果中点击数值 →“设置基线”。后续运行会与基线比较,如果回归超过容差(默认 10%),测试就会失败。

反模式:没有基线的性能测试
// ❌ Test always passes — no baseline set
func testPerformance() {
    measure { doWork() }
}

// ✅ Set baseline in Xcode after first run
// Tests fail when performance regresses beyond tolerance
将 Signpost 接入测试(XCTOSSignpostMetric)
// In production code
let signposter = OSSignposter(subsystem: "com.app", category: "Sync")

func syncData() {
    let id = signposter.makeSignpostID()
    let state = signposter.beginInterval("Full Sync", id: id)
    defer { signposter.endInterval("Full Sync", state) }
    // ... sync logic
}

// In test
func testSyncPerformance() {
    let metric = XCTOSSignpostMetric(
        subsystem: "com.app",
        category: "Sync",
        name: "Full Sync"
    )
    measure(metrics: [metric]) {
        syncData()
    }
}
阶段 3:在生产环境中监控(MetricKit)

有关 MetricKit 集成的完整说明,请参阅 axiom-metrickit-ref。需要监控的关键指标:

  • MXAppLaunchMetric — 启动时间回归
  • MXAppResponsivenessMetric — 卡顿率上升
  • MXCPUMetric — 每次前台会话的 CPU 时间
  • MXMemoryMetric — 不同版本间峰值内存增长

资源

WWDC:2023-10160、2024-10217、2025-308、2025-312

文档:/library/archive/documentation/cocoa/conceptual/coredataperformance、/library/archive/technotes/tn2224、/os/ossignposter、/xctest/xctestcase/measure

技能:axiom-memory-debugging、axiom-swiftui-performance、axiom-swift-concurrency、axiom-metrickit-ref


目标平台: iOS 14+、Swift 5.5+ 工具: Instruments、Core Data 历史记录: 有关变更,请参阅 git log

评论

支持 Markdown