关于 axiom-memory-debugging
axiom-memory-debugging 是用于诊断和修复应用内存泄漏及内存压力问题的实用技能,指导开发者使用 Instruments、Memory Graph Debugger、常见保留模式和命令行检查。
主要功能
- 01逐步排查内存泄漏的流程
- 02Memory Graph Debugger 与 Instruments 指南
- 03定时器、观察者和闭包循环的修复方法
- 04Jetsam 与后台内存指南
- 05命令行内存诊断检查
适用场景
- 01调查应用内存持续增长
- 02查找视图控制器关闭后仍被保留的原因
- 03检查定时器和通知的清理
- 04诊断内存压力导致的后台终止
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-memory-debugging安装后在 Claude Code 中输入/axiom-memory-debugging确认已加载。
详细介绍
内存调试
概述
内存问题通常表现为长时间使用后发生崩溃。核心原则: 90% 的内存泄漏都符合 3 种模式(循环引用、计时器/观察者泄漏、集合增长)。使用 Instruments 系统地诊断,切勿猜测。
示例提示词
- “我的应用在运行 10-15 分钟后崩溃,且没有错误消息”
- “执行特定操作后,内存从 50MB 跳到 200MB 以上——是泄漏还是缓存?”
- “关闭视图控制器后,它们没有释放”
- “计时器/观察者导致内存泄漏——如何验证?”
- “应用占用 200MB,但我不知道这是否正常”
警示信号——很可能存在内存泄漏
- 内存持续增长:50MB → 100MB → 200MB(没有趋于稳定)
- 应用在运行 10-15 分钟后崩溃,Xcode 控制台中没有错误
- 设备日志中反复出现内存警告
- 关闭视图控制器后,它们没有释放(可在 Memory Graph Debugger 中看到)
- 多次执行同一操作会导致内存线性增长
泄漏与正常情况的区别:正常情况 = 保持在 100MB。泄漏 = 50MB → 100MB → 150MB → 200MB → 崩溃。
必须执行的第一步
始终先诊断(再阅读代码):
- 检查设备日志中的 "Memory pressure critical"、"Jetsam killed"、"Low Memory"
- 使用 Memory Graph Debugger(见下文)——查看对象数量的增长
- Xcode → Product → Profile → Memory。执行操作 5 次,记录内存是否持续增长
诊断结果说明:平稳 = 不是泄漏。线性增长 = 典型泄漏。先飙升再平稳 = 正常缓存。多次飙升叠加 = 复合泄漏。
为何先诊断:使用 Instruments 找到泄漏:5-15 分钟。凭猜测排查:45 分钟以上。
检测泄漏——逐步操作
第 1 步:Memory Graph Debugger(最快)
- 在模拟器中打开应用
- Debug → Memory Graph Debugger(或点击工具栏图标)
- 查找带有 "⚠" 标记的紫色/红色圆圈
- 点击它们 → Xcode 会显示循环引用链
第 2 步:Instruments(详细分析)
- Product → Profile (Cmd+I) → "Memory" 模板
- 执行操作 5-10 次
- 每次操作后,内存曲线都上升?= 确认存在泄漏
关键工具:Heap Allocations(对象数量)、Leaked Objects(直接检测)、VM Tracker(按类型)。
第 3 步:检查释放情况
// Add deinit logging to suspect classes
class MyViewController: UIViewController {
deinit { print("✅ MyViewController deallocated") }
}
@MainActor
class ViewModel: ObservableObject {
deinit { print("✅ ViewModel deallocated") }
}
进入视图,再离开。看到 "✅ deallocated" 了吗?是 = 没有泄漏。否 = 在某处仍被保留。
Jetsam(内存压力终止)
Jetsam 不是 Bug——iOS 会终止后台应用以释放内存。这不是崩溃(没有崩溃日志),但频繁被终止会损害用户体验。
| 终止原因 | 成因 | 解决方案 |
|---|---|---|
| 内存超出限制 | 应用使用了过多内存 | 减少峰值内存占用 |
| Jetsam | 系统需要为其他应用腾出内存 | 将后台内存占用降至 <50MB |
降低 Jetsam 频率
进入后台时清除缓存:
// SwiftUI
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
imageCache.clearAll()
URLCache.shared.removeAllCachedResponses()
}
}
状态恢复
用户不应察觉到 Jetsam。使用 @SceneStorage(SwiftUI)或 stateRestorationActivity(UIKit)恢复导航位置、草稿和滚动位置。
使用 MetricKit 进行监控
class JetsamMonitor: NSObject, MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
guard let exitData = payload.applicationExitMetrics else { continue }
let bgData = exitData.backgroundExitData
if bgData.cumulativeMemoryPressureExitCount > 0 {
// Send to analytics
}
}
}
}
App memory grows while in USE? → Memory leak (fix retention)
App killed in BACKGROUND? → Jetsam (reduce bg memory)
常见内存泄漏模式(及修复方法)
模式 1:计时器泄漏(最常见——占泄漏的 50%)
仅使用 [weak self] 无法修复计时器泄漏的原因:RunLoop 会保留已调度的计时器。[weak self] 只能防止闭包保留 self——Timer 对象本身仍会继续存在并触发。必须显式调用 invalidate(),才能解除 RunLoop 对它的保留。
❌ 泄漏——计时器从未失效
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
// Timer never stopped → RunLoop keeps it alive and firing forever
✅ 最佳修复:Combine(自动清理)
cancellable = Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .default)
.autoconnect()
.sink { [weak self] _ in self?.updateProgress() }
// No deinit needed — cancellable auto-cleans when released
替代方法:在适当的清理方法(viewWillDisappear、停止方法等)和 deinit 中都调用 timer?.invalidate(); timer = nil。
有关计时器崩溃模式(EXC_BAD_INSTRUCTION)和 RunLoop 模式问题,请参阅
axiom-timer-patterns。
模式 2:观察者/通知泄漏(占泄漏的 25%)
❌ 泄漏——没有调用 removeObserver
NotificationCenter.default.addObserver(self, selector: #selector(handle),
name: AVAudioSession.routeChangeNotification, object: nil)
// No matching removeObserver → accumulates listeners
✅ 最佳修复:Combine 发布者
NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
.sink { [weak self] _ in self?.handleChange() }
.store(in: &cancellables) // Auto-cleanup with viewModel
替代方法:在 deinit 中调用 NotificationCenter.default.removeObserver(self)。
模式 3:闭包捕获泄漏(占泄漏的 15%)
❌ 泄漏 — 数组中的闭包捕获了 self
updateCallbacks.append { [self] track in
self.refreshUI(with: track) // Strong capture → cycle
}
✅ 修复:使用 [weak self]
updateCallbacks.append { [weak self] track in
self?.refreshUI(with: track)
}
在 deinit 中清空回调数组。只有确定 self 的生命周期长于闭包时,才使用 [unowned self]。
模式 4:强引用循环
❌ 泄漏 — 相互强引用
player?.onPlaybackEnd = { [self] in self.playNextTrack() }
// self → player → closure → self (cycle)
✅ 修复:在闭包中使用 [weak self]
player?.onPlaybackEnd = { [weak self] in self?.playNextTrack() }
模式 5:视图/布局回调泄漏
使用带有 AnyObject 协议(支持弱引用)的委托模式,而不是会捕获视图控制器的闭包。
模式 6:PhotoKit 图像请求泄漏
PHImageManager.requestImage() 会返回必须取消的 PHImageRequestID。如果不取消,待处理请求会不断排队,并在滚动时占用内存。
class PhotoCell: UICollectionViewCell {
private var imageRequestID: PHImageRequestID = PHInvalidImageRequestID
func configure(with asset: PHAsset, imageManager: PHImageManager) {
if imageRequestID != PHInvalidImageRequestID {
imageManager.cancelImageRequest(imageRequestID)
}
imageRequestID = imageManager.requestImage(for: asset, targetSize: PHImageManagerMaximumSize,
contentMode: .aspectFill, options: nil) { [weak self] image, _ in
self?.imageView.image = image
}
}
override func prepareForReuse() {
super.prepareForReuse()
if imageRequestID != PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(imageRequestID)
imageRequestID = PHInvalidImageRequestID
}
imageView.image = nil
}
}
类似模式:AVAssetImageGenerator → cancelAllCGImageGeneration(),URLSession.dataTask() → cancel()。
系统化调试工作流
阶段 1:确认泄漏(5 分钟)
使用 Memory 模板进行分析,将操作重复 10 次。内存曲线平稳 = 没有泄漏(停止)。持续上升 = 存在泄漏(继续)。
阶段 2:定位泄漏(10-15 分钟)
Memory Graph Debugger → 紫色/红色圆圈 → 点击 → 查看保留循环链。
常见位置:计时器(50%)、通知/KVO(25%)、集合中的闭包(15%)、委托循环(10%)。
阶段 3:修复并验证(5 分钟)
应用上述模式中的修复方法。添加 deinit { print("✅ deallocated") }。再次运行 Instruments — 内存应保持平稳。
复合泄漏
实际应用通常会叠加 2-3 个泄漏。先修复最大的一个,再次运行 Instruments,然后重复此过程,直到内存曲线平稳。
无法复现/间歇性泄漏
当 Instruments 导致问题无法复现(Heisenbug),或泄漏只在特定用户数据下发生时:
轻量级诊断(无法附加 Instruments 时):
- 将 deinit 日志作为主要诊断手段 — 在所有可疑类中添加
deinit { print("✅ ClassName deallocated") }。运行 20 次以上会话。发生泄漏时(例如 5 次中出现 1 次),缺失的 deinit 消息可揭示哪些对象仍被保留。 - 隔离触发条件 — 分别测试每条导航路径。如果问题与时序有关,则快速切换前台/后台状态。逐步缩小范围,找出发生泄漏的具体路径。
- 使用 MetricKit 进行线上诊断 — 通过
MXMetricPayload.memoryMetrics.peakMemoryUsage监控生产环境中的峰值内存。超过阈值时发出警报(例如 400MB)。这可以捕捉到只有在真实用户数据量下才会显现的泄漏。
间歇性泄漏的常见原因:在生命周期事件(viewWillAppear、applicationDidBecomeActive)中添加通知观察者,却没有先移除重复项。每次重新注册都会累积一个监听器 — 是否触发重复监听取决于时序。
TestFlight 验证:向受影响的用户发布诊断版本。添加 os_log 内存里程碑。修复部署后,监控 MetricKit 24-48 小时。
常见错误
- 使用 [weak self] 却不调用 invalidate() — 计时器会继续运行并消耗 CPU。务必调用
invalidate()或cancel() - 调用 invalidate 却不设为 nil —
timer?.invalidate()会停止触发,但引用仍然存在。之后务必执行timer = nil - 局部 AnyCancellable — 会立即超出作用域,订阅随即失效。将其存储在
Set<AnyCancellable>属性中 - deinit 中只有日志 — 应添加实际清理操作(使计时器失效、移除观察者),而不只是打印日志
- 使用了错误的 Instruments 模板 — Memory 显示内存用量。Leaks 会检测实际泄漏。两者都要使用
Instruments 快速参考
| 场景 | 工具 | 观察内容 |
|---|---|---|
| 内存持续增长 | Memory | 曲线持续上升 = 泄漏 |
| 特定对象泄漏 | Memory Graph | 紫色/红色圆圈 = 泄漏对象 |
| 直接检测泄漏 | Leaks | 红色 "! Leak" 标记 = 已确认泄漏 |
| 按类型查看内存 | VM Tracker | 占用内存最多的对象 |
| 缓存行为 | Allocations | 已分配但未释放的对象 |
CLI 快速检查(无需 Instruments)
Xcode 提供了 CLI 工具,无需打开 Instruments 即可快速诊断内存问题。开发过程中可使用这些工具进行快速检查。
leaks — 检测运行中进程的泄漏
# Check running app by name (positional argument, not --process)
xcrun leaks MyApp
# Check by PID
xcrun leaks 12345
# Show full stack traces for each leak
xcrun leaks --fullStacks MyApp
# Analyze a memgraph file (from Xcode's Debug Memory Graph)
xcrun leaks MyApp.memgraph
使用时机:无需录制 Instruments 跟踪即可快速检查泄漏。在执行可疑代码路径后运行。
heap — 检查实时堆分配
# Show heap summary by class (process name is positional)
xcrun heap MyApp
# Show all instances of a specific class
xcrun heap --addresses=MyViewController MyApp
# Sort by size (find biggest consumers)
xcrun heap -s MyApp
# Analyze a memgraph
xcrun heap MyApp.memgraph
使用时机:查找当前占用内存的内容。无需使用 Instruments,就能回答“存在多少个 MyViewController 实例?”。
vmmap — 虚拟内存映射
# Summary view (dirty, clean, swapped)
xcrun vmmap --summary MyApp.memgraph
# Full memory regions
xcrun vmmap MyApp.memgraph
使用时机:了解内存构成。显示脏页(你的数据)、干净页(映射文件)和压缩内存。
stringdups — 查找重复字符串
# Find duplicate strings in running process (positional argument)
xcrun stringdups MyApp
# Analyze a memgraph
xcrun stringdups MyApp.memgraph
使用时机:减少重复字符串分配造成的内存占用。没有对应的 GUI 工具。
malloc_history — 跟踪分配来源
# Enable malloc logging first: set MallocStackLogging=1 in scheme env vars
# Then query a specific address
xcrun malloc_history <pid> <address>
# Show all allocations sorted by size
xcrun malloc_history <pid> -allBySize
使用时机:追踪泄漏对象的分配位置。需要在 scheme 中设置 MallocStackLogging=1 环境变量。
快速诊断工作流
# 1. Is there a leak? (30 seconds)
xcrun leaks MyApp
# 2. What's on the heap? (30 seconds)
xcrun heap -s MyApp
# 3. Any duplicate strings wasting memory? (30 seconds)
xcrun stringdups MyApp
# 4. Where is memory allocated? (requires memgraph)
xcrun vmmap --summary MyApp.memgraph
耗时:完整的 CLI 内存检查需 2 分钟,而启动 Instruments 需 10 分钟以上。
xctrace(无头 Instruments)
# Record memory trace without GUI
xcrun xctrace record --instrument 'Allocations' --attach 'MyApp' --time-limit 30s --output memory.trace
# Record leak detection
xcrun xctrace record --instrument 'Leaks' --attach 'MyApp' --time-limit 30s --output leaks.trace
实际影响
之前:50 多个 PlayerViewModel 实例带有未清理的计时器 → 50MB → 200MB → 崩溃(13 分钟) 之后:正确使计时器失效 → 50MB,数小时内保持稳定
关键见解:90% 的泄漏都是因为忘记停止计时器、观察者或订阅。务必在 deinit 中清理,或使用会自动清理的响应式模式。
资源
WWDC:2021-10180、2020-10078、2018-416
文档:/xcode/gathering-information-about-memory-use、/metrickit/mxbackgroundexitdata
技能:axiom-performance-profiling、axiom-objc-block-retain-cycles、axiom-metrickit-ref、axiom-lldb(交互式检查保留环)