关于 axiom-performance-profiling
面向 iOS 开发者的性能分析参考,帮助根据卡顿、内存增长或耗电等症状选择 Instruments 工具并解读结果。内容包含针对 CPU、内存、Core Data 和能耗问题的诊断流程。
主要功能
- 01按性能症状选择分析工具
- 02Time Profiler CPU 分析
- 03Allocations 内存分析
- 04Core Data 查询诊断
- 05识别分析结果中的常见误区
适用场景
- 01定位界面卡顿或 CPU 峰值
- 02调查内存持续增长
- 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:连接到正在运行的应用
- 在模拟器或设备上启动应用
- 在 Instruments 中,从目标下拉菜单中选择应用
- 点击录制(红色圆圈)
- 操作运行缓慢的部分(滚动、点击按钮、加载数据)
- 交互 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)
- 在主线程上运行(将其移到后台;它本身已经过优化)
- 执行了看起来很慢的必要工作(基准性能可以接受,用户不会察觉)
应该怎么做:
-
查看自身耗时,而不是总耗时
- 自身耗时占 80%?函数确实在执行开销很大的工作
- 自身耗时占 5%,总耗时占 80%?函数调用了运行缓慢的代码
-
向下深入一层
- 这个函数调用了什么?
- 慢代码是否位于你能控制的库中?
-
查看时间线
- 这 80% 是持续存在(一直缓慢),还是偶尔出现峰值(偶发卡顿)?
- 持续存在 = 需要优化
- 偶尔出现峰值 = 缓存可能会有帮助
-
思考:用户会注意到吗?
- 后台工作耗时 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:附加并录制
- 启动你的应用
- 在 Instruments 中选择你的应用
- 点击 Record
- 执行会使用内存的操作(加载数据、显示图像、导航)
- 等内存稳定或达到峰值后停止录制
步骤 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 可能完全正常
应该怎么做:
-
在真机上建立基线
# On device, open Memory view in Xcode Xcode → Debug → Memory Debugger → Check "Real Memory" at app launch -
检查对象数量,而不是总内存
- Allocations → Statistics → “Persistent”
- 图像、视图或数据对象的数量是否是预期的 10 倍?
- 如果是,调查该对象类型
- 如果否,内存可能没问题
-
在内存压力下测试
- Xcode → Debug → Simulate Memory Warning
- 内存是否下降 50%+?这是缓存(正常)
- 内存是否保持高位?调查持久对象
-
分析真实用户旅程
- 加载数据(像用户那样)
- 四处导航(像用户那样)
- 返回 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 是最后才应该尝试的。
应该怎么做:
-
启用 SQL 调试(2 分钟)
- 添加
-com.apple.CoreData.SQLDebug 1启动参数 - 观察执行了哪些查询
- 添加
-
寻找 N+1 模式(3 分钟)
- 获取 100 个对象,然后为相关数据执行单独查询?
- 添加关系预取
-
如果需要,添加索引(5 分钟)
@NSManaged var artist: String且频繁筛选?- 在 Schema 中添加
@Index
-
测试改进(2 分钟)
- 重新运行相同的操作
- 比较查询数量和总时间
- 如果快了 10 倍,就完成了
- 如果仍然慢,进入步骤 5
-
只有到那时才考虑 Schema 更改(30+ 分钟)
- 但你很可能不会走到这一步
时间成本:诊断 + 修复 12 分钟 = 12 分钟
Schema 重新设计的代价:8 小时设计 + 4 小时迁移 + 2 小时测试 + 1 小时回滚 = 总计 15 小时
快速参考:其他工具
能耗影响(电池消耗)
适用场景:应用耗电很快,设备发热
工作流:
- 启动 Instruments → 选择 Energy Impact 模板
- 正常运行应用 5 分钟以上
- 查找持续出现的红色/橙色用量(不佳)
- 深入查看哪些子系统在消耗电量
关键指标:
- 持续功耗 – 持续能耗(应尽可能低)
- 峰值 – 短暂的高用量(可以接受)
- CPU – 进程 CPU 时间
- GPU – 图形渲染
- 网络 – 蜂窝网络/WiFi 无线电
- 定位 – GPS 用量
常见问题:
- 持续使用 1m 精度更新位置(应为 100m)
- 运行会反复唤醒设备的计时器
- 网络请求过多(应改为批量请求)
- 视图不可见时仍在播放动画
Network Link Conditioner(连接模拟)
适用场景:应用在 4G 下似乎很慢,希望无需出行即可进行测试
设置:
- 下载 Additional Tools for Xcode
- 安装 Network Link Conditioner
- 打开“系统设置” → Network Link Conditioner
- 选择配置文件(3G、LTE、WiFi Slow 等)
- 启用并激活配置文件
- 运行应用进行测试
关键配置文件:
- 3G – 下行 1.6Mbps,上行 768Kbps,延迟 150ms
- LTE – 下行 10Mbps,上行 5Mbps,延迟 20ms
- WiFi Slow – 10Mbps,延迟 100ms
- Custom – 设置自己的参数
注意:ui-testing 中也介绍了此工具,用于网络依赖型测试场景。
System Trace(线程阻塞、调度)
适用场景:UI 卡顿或不流畅,但 Time Profiler 显示 CPU 用量较低
常见原因:主线程被等待锁的后台任务阻塞
工作流:
- 启动 Instruments → 选择 System Trace 模板
- 在重现问题时录制
- 查找主线程间隙(已阻塞、未运行)
- 深入查看阻塞原因
关键指标:
- 主线程间隙 – 空白区域 = 主线程空闲/阻塞
- 核心调度 – 各线程的运行时机
- 锁竞争 – 等待锁的线程
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 集成
- 启动 Instruments → 添加“os_signpost”或“Points of Interest”检测器
- 录制应用执行已检测的操作
- Signpost 时间间隔会以彩色条形显示在时间线上
- 按子系统/类别筛选,聚焦于你的操作
何时使用 Signpost,何时使用 Time Profiler
| 需求 | 工具 |
|---|---|
| 通用 CPU 热点 | Time Profiler |
| 特定操作耗时 | OSSignposter |
| 跨线程操作计时 | OSSignposter |
| 自动化回归测试 | OSSignposter + XCTOSSignpostMetric |
压力场景
场景 1:“每次运行的性能分析结果都不一样”
问题:你运行 3 次 Time Profiler,结果分别是 200ms、150ms、280ms。哪个才是正确的?
你可能会有的误区:
- “结果不可靠,性能分析不准确。”
- “我直接取平均值吧。”
- “差异太大了,我没法优化。”
实际情况:结果有差异很正常。不同运行会遇到不同的:
- 缓存状态(冷缓存 = 更慢)
- 系统负载(其他应用正在运行)
- CPU 频率(加速 / 降频)
建议这样做:
-
预热缓存(首次运行总是更慢)
- 执行一次操作(冷缓存)
- 再执行一次(热缓存)——使用这次的测量结果
-
控制系统负载
- 关闭其他应用
- 性能分析期间不要操作设备
- 在设备上进行性能分析(不要使用模拟器)
-
观察规律
- 多次运行:150ms、160ms、155ms(一致 = 良好)
- 多次运行:150ms、280ms、240ms(不一致 = 需要调查)
- 不一致说明存在间歇性问题,需要找出原因
-
以最慢的一次为准(最坏情况)
- 如果范围是 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 Profiler | Allocations | 处理方式 |
|---|---|---|
| CPU 占用高 | 内存正常 | 优化计算(降低 CPU 占用) |
| CPU 占用低 | 内存持续增长 | 查找内存泄漏或减少对象创建 |
| 两者都高 | 两者都高 | 优先分析哪个问题对用户影响更大 |
建议这样做:
-
按用户影响确定优先级
- 响应慢(UI 卡顿)= 优先修复
- 内存问题(后台问题)= 其次修复
-
检查两者是否相关
- JSON 解析会导致内存泄漏吗?(不会 → 是独立问题)
- 内存增长会拖慢 CPU 吗?(可能 → 先修复内存问题)
-
按影响程度依次修复
- JSON 解析慢:每次加载数据都会受影响
- 内存正常:不会影响用户
- → 修复 JSON 解析
耗时:5 分钟(分析两项结果)= 5 分钟
修错问题的代价:花 4 小时优化本来就正常的内存使用 → 用户体验没有改善
场景 3:“截止日期临近时进行性能分析”
情况:经理说:“我们 2 小时后发布。性能可以接受吗?”
你可能会有的误区:
- “性能分析太耗时了,我直接问用户吧。”
- “我没时间好好做性能分析,按现状发布吧。”
- “快速跑一次就能知道有没有问题。”
实际情况:性能分析总共只需 15-20 分钟,只占剩余时间的 1%。
建议这样做:
-
分析关键路径(3 分钟)
- 用户最常做的操作(加载列表、滚动、搜索)
- 不分析整个应用,只分析慢的部分
-
正确记录一次运行结果(5 分钟)
- 首次使用冷缓存
- 第二次使用热缓存
- 使用热缓存的结果
-
快速解读(5 分钟)
- Time Profiler:主线程上是否有任何操作耗时超过 100ms?(如果没有,就没问题)
- Allocations:内存是否持续增长?(如果没有,就没问题)
-
放心发布(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 行):
- 打开 Instruments:
open -a Instruments
# Select "Time Profiler"
- 记录卡顿:
App launches
Time Profiler records
View loads
Stall happens (observe the spike in Time Profiler)
Stop recording
- 检查结果:
Call Stack shows:
viewDidLoad() – 1500ms
├─ loadJSON() – 1200ms (Self Time: 50ms)
│ └─ loadImages() – 1150ms (Self Time: 1150ms) ← HERE'S THE CULPRIT
├─ parseData() – 200ms
└─ layoutUI() – 100ms
- 应用此技能(第 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
- 修复真正的问题:
// ❌ 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 行):
-
在 Instruments 中启动 Allocations
-
记录 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
- 检查 Allocations 统计信息:
Persistent Objects:
- UIImage: 1200 instances (300MB) ← Large count
- NSString: 5000 instances (4MB)
- CustomDataModel: 800 instances (15MB)
- 向此技能提出问题(第 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
- 结论:
// ✅ 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 | 编写代码时 | 特定操作的耗时 |
| CI | XCTest 性能测试 | 每个 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