axiom-memory-debugging by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-memory-debugging内存问题表现为长时间使用后崩溃。核心原则 90% 的内存泄漏遵循 3 种模式(循环引用、计时器/观察者泄漏、集合增长)。使用 Instruments 进行系统性诊断,切勿猜测。
泄漏与正常情况对比:正常 = 保持在 100MB。泄漏 = 50MB → 100MB → 150MB → 200MB → 崩溃。
务必先进行诊断(在阅读代码之前):
这告诉你的信息:平坦 = 不是泄漏。线性增长 = 典型泄漏。峰值后平坦 = 正常缓存。峰值叠加 = 复合泄漏。
为何先进行诊断:使用 Instruments 查找泄漏:5-15 分钟。猜测:45+ 分钟。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
关键工具:堆分配(对象计数)、泄漏对象(直接检测)、VM 跟踪器(按类型)。
// 向可疑类添加 deinit 日志记录
class MyViewController: UIViewController {
deinit { print("✅ MyViewController deallocated") }
}
@MainActor
class ViewModel: ObservableObject {
deinit { print("✅ ViewModel deallocated") }
}
导航到视图,然后离开。看到 "✅ deallocated" 了吗?是 = 无泄漏。否 = 被某处保留。
Jetsam 不是错误 —— iOS 终止后台应用以释放内存。不是崩溃(无崩溃日志),但频繁终止会损害用户体验。
| 终止原因 | 原因 | 解决方案 |
|---|---|---|
| 超出内存限制 | 你的应用使用了太多内存 | 减少峰值内存占用 |
| Jetsam | 系统需要内存给其他应用 | 将后台内存减少到 <50MB |
在进入后台时清除缓存:
// SwiftUI
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
imageCache.clearAll()
URLCache.shared.removeAllCachedResponses()
}
}
用户不应注意到 jetsam。使用 @SceneStorage (SwiftUI) 或 stateRestorationActivity (UIKit) 来恢复导航位置、草稿和滚动位置。
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 {
// 发送到分析平台
}
}
}
}
应用在使用时内存增长? → 内存泄漏(修复保留问题)
应用在后台被终止? → Jetsam(减少后台内存)
为什么仅使用 [weak self] 无法修复计时器泄漏:RunLoop 保留了已调度的计时器。[weak self] 仅防止闭包保留 self —— Timer 对象本身继续存在并触发。你必须显式调用 invalidate() 来打破 RunLoop 的保留。
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
// 计时器从未停止 → RunLoop 使其保持活动状态并永远触发
cancellable = Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .default)
.autoconnect()
.sink { [weak self] _ in self?.updateProgress() }
// 无需 deinit —— cancellable 在释放时自动清理
替代方案:在适当的拆卸方法(viewWillDisappear、停止方法等)和 deinit 中都调用 timer?.invalidate(); timer = nil。
关于计时器崩溃模式(EXC_BAD_INSTRUCTION)和 RunLoop 模式问题,请参阅
axiom-timer-patterns。
NotificationCenter.default.addObserver(self, selector: #selector(handle),
name: AVAudioSession.routeChangeNotification, object: nil)
// 没有匹配的 removeObserver → 累积监听器
NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
.sink { [weak self] _ in self?.handleChange() }
.store(in: &cancellables) // 与 viewModel 一起自动清理
替代方案:在 deinit 中调用 NotificationCenter.default.removeObserver(self)。
updateCallbacks.append { [self] track in
self.refreshUI(with: track) // 强捕获 → 循环
}
updateCallbacks.append { [weak self] track in
self?.refreshUI(with: track)
}
在 deinit 中清空回调数组。仅当确定 self 的生命周期长于闭包时才使用 [unowned self]。
player?.onPlaybackEnd = { [self] in self.playNextTrack() }
// self → player → closure → self (循环)
player?.onPlaybackEnd = { [weak self] in self?.playNextTrack() }
使用带有 AnyObject 协议的委托模式(支持弱引用),而不是捕获视图控制器的闭包。
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()。
使用 Memory 模板进行分析,重复操作 10 次。平坦 = 不是泄漏(停止)。稳定攀升 = 泄漏(继续)。
内存图调试器 → 紫色/红色圆圈 → 点击 → 阅读循环引用链。
常见位置:计时器(50%)、通知/KVO(25%)、集合中的闭包(15%)、委托循环(10%)。
应用上述模式的修复。添加 deinit { print("✅ deallocated") }。再次运行 Instruments —— 内存应保持平坦。
真实应用通常有 2-3 个泄漏叠加。先修复最大的,重新运行 Instruments,重复直到平坦。
当 Instruments 阻止重现(海森堡错误)或泄漏仅发生在特定用户数据时:
轻量级诊断(当无法附加 Instruments 时):
deinit { print("✅ ClassName deallocated") }。运行 20+ 个会话。当泄漏发生时(例如,每 5 次运行中有 1 次),缺失的 deinit 消息会揭示哪些对象被保留。MXMetricPayload.memoryMetrics.peakMemoryUsage 监控生产环境中的峰值内存。当超过阈值(例如 400MB)时发出警报。这可以捕获仅在使用真实用户数据量时才显现的泄漏。间歇性泄漏的常见原因:在生命周期事件(viewWillAppear、applicationDidBecomeActive)上添加通知观察者,而没有先移除重复项。每次重新注册都会累积一个监听器 —— 时间安排决定了重复项是否会触发。
TestFlight 验证:向受影响的用户发布诊断版本。添加 os_log 内存里程碑。在修复部署后监控 MetricKit 24-48 小时。
invalidate() 或 cancel()timer?.invalidate() 停止触发,但引用仍然存在。务必随后执行 timer = nilSet<AnyCancellable> 属性中| 场景 | 工具 | 寻找什么 |
|---|---|---|
| 渐进式内存增长 | Memory | 线条稳定攀升 = 泄漏 |
| 特定对象泄漏 | Memory Graph | 紫色/红色圆圈 = 泄漏对象 |
| 直接泄漏检测 | Leaks | 红色 "! Leak" 徽章 = 确认泄漏 |
| 按类型划分的内存 | VM Tracker | 消耗最多内存的对象 |
| 缓存行为 | Allocations | 已分配但未释放的对象 |
xcrun xctrace record --template "Memory" --output memory.trace
xcrun xctrace dump memory.trace
leaks -atExit -excludeNoise YourApp
修复前: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(交互式检查循环引用)
每周安装量
106
仓库
GitHub Stars
610
首次出现
Jan 21, 2026
安全审计
安装于
opencode88
claude-code83
gemini-cli81
codex81
cursor79
github-copilot76
Memory issues manifest as crashes after prolonged use. Core principle 90% of memory leaks follow 3 patterns (retain cycles, timer/observer leaks, collection growth). Diagnose systematically with Instruments, never guess.
Leak vs normal : Normal = stays at 100MB. Leak = 50MB → 100MB → 150MB → 200MB → CRASH.
ALWAYS diagnose FIRST (before reading code):
What this tells you : Flat = not a leak. Linear growth = classic leak. Spike then flat = normal cache. Spikes stacking = compound leak.
Why diagnostics first : Finding leak with Instruments: 5-15 min. Guessing: 45+ min.
Key instruments: Heap Allocations (object count), Leaked Objects (direct detection), VM Tracker (by type).
// Add deinit logging to suspect classes
class MyViewController: UIViewController {
deinit { print("✅ MyViewController deallocated") }
}
@MainActor
class ViewModel: ObservableObject {
deinit { print("✅ ViewModel deallocated") }
}
Navigate to view, navigate away. See "✅ deallocated"? Yes = no leak. No = retained somewhere.
Jetsam is not a bug — iOS terminates background apps to free memory. Not a crash (no crash log), but frequent kills hurt UX.
| Termination | Cause | Solution |
|---|---|---|
| Memory Limit Exceeded | Your app used too much memory | Reduce peak footprint |
| Jetsam | System needed memory for other apps | Reduce background memory to <50MB |
Clear caches on backgrounding:
// SwiftUI
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
imageCache.clearAll()
URLCache.shared.removeAllCachedResponses()
}
}
Users shouldn't notice jetsam. Use @SceneStorage (SwiftUI) or stateRestorationActivity (UIKit) to restore navigation position, drafts, and scroll position.
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)
Why[weak self] alone doesn't fix timer leaks: The RunLoop retains scheduled timers. [weak self] only prevents the closure from retaining self — the Timer object itself continues to exist and fire. You must explicitly invalidate() to break the RunLoop's retention.
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
// Timer never stopped → RunLoop keeps it alive and firing forever
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
Alternative : Call timer?.invalidate(); timer = nil in both the appropriate teardown method (viewWillDisappear, stop method, etc.) AND deinit.
For timer crash patterns (EXC_BAD_INSTRUCTION) and RunLoop mode issues, see
axiom-timer-patterns.
NotificationCenter.default.addObserver(self, selector: #selector(handle),
name: AVAudioSession.routeChangeNotification, object: nil)
// No matching removeObserver → accumulates listeners
NotificationCenter.default.publisher(for: AVAudioSession.routeChangeNotification)
.sink { [weak self] _ in self?.handleChange() }
.store(in: &cancellables) // Auto-cleanup with viewModel
Alternative : NotificationCenter.default.removeObserver(self) in deinit.
updateCallbacks.append { [self] track in
self.refreshUI(with: track) // Strong capture → cycle
}
updateCallbacks.append { [weak self] track in
self?.refreshUI(with: track)
}
Clear callback arrays in deinit. Use [unowned self] only when certain self outlives the closure.
player?.onPlaybackEnd = { [self] in self.playNextTrack() }
// self → player → closure → self (cycle)
player?.onPlaybackEnd = { [weak self] in self?.playNextTrack() }
Use the delegation pattern with AnyObject protocol (enables weak references) instead of closures that capture view controllers.
PHImageManager.requestImage() returns a PHImageRequestID that must be cancelled. Without cancellation, pending requests queue up and hold memory when scrolling.
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
}
}
Similar patterns: AVAssetImageGenerator → cancelAllCGImageGeneration(), URLSession.dataTask() → cancel().
Profile with Memory template, repeat action 10 times. Flat = not a leak (stop). Steady climb = leak (continue).
Memory Graph Debugger → purple/red circles → click → read retain cycle chain.
Common locations: Timers (50%), Notifications/KVO (25%), Closures in collections (15%), Delegate cycles (10%).
Apply fix from patterns above. Add deinit { print("✅ deallocated") }. Run Instruments again — memory should stay flat.
Real apps often have 2-3 leaks stacking. Fix the largest first, re-run Instruments, repeat until flat.
When Instruments prevents reproduction (Heisenbug) or leaks only happen with specific user data:
Lightweight diagnostics (when Instruments can't be attached):
deinit { print("✅ ClassName deallocated") } to all suspect classes. Run 20+ sessions. When the leak occurs (e.g., 1 in 5 runs), missing deinit messages reveal which objects are retained.MXMetricPayload.memoryMetrics.peakMemoryUsage. Alert when exceeding threshold (e.g., 400MB). This catches leaks that only manifest with real user data volumes.Common cause of intermittent leaks : Notification observers added on lifecycle events (viewWillAppear, applicationDidBecomeActive) without removing duplicates first. Each re-registration accumulates a listener — timing determines whether the duplicate fires.
TestFlight verification : Ship diagnostic build to affected users. Add os_log memory milestones. Monitor MetricKit for 24-48 hours after fix deployment.
invalidate() or cancel()timer?.invalidate() stops firing but reference remains. Always follow with timer = nilSet<AnyCancellable> property| Scenario | Tool | What to Look For |
|---|---|---|
| Progressive memory growth | Memory | Line steadily climbing = leak |
| Specific object leaking | Memory Graph | Purple/red circles = leak objects |
| Direct leak detection | Leaks | Red "! Leak" badge = confirmed leak |
| Memory by type | VM Tracker | Objects consuming most memory |
| Cache behavior | Allocations | Objects allocated but not freed |
xcrun xctrace record --template "Memory" --output memory.trace
xcrun xctrace dump memory.trace
leaks -atExit -excludeNoise YourApp
Before : 50+ PlayerViewModel instances with uncleared timers → 50MB → 200MB → Crash (13min) After : Timer properly invalidated → 50MB stable for hours
Key insight 90% of leaks come from forgetting to stop timers, observers, or subscriptions. Always clean up in deinit or use reactive patterns that auto-cleanup.
WWDC : 2021-10180, 2020-10078, 2018-416
Docs : /xcode/gathering-information-about-memory-use, /metrickit/mxbackgroundexitdata
Skills : axiom-performance-profiling, axiom-objc-block-retain-cycles, axiom-metrickit-ref, axiom-lldb (inspect retain cycles interactively)
Weekly Installs
106
Repository
GitHub Stars
610
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode88
claude-code83
gemini-cli81
codex81
cursor79
github-copilot76
ESLint迁移到Oxlint完整指南:JavaScript/TypeScript项目性能优化工具
1,600 周安装
UltraThink Orchestrator - 已弃用的AI工作流编排工具,推荐使用dev-orchestrator替代
85 周安装
数据库管理员技能:PostgreSQL/MySQL/MongoDB高可用架构、性能调优与灾难恢复
86 周安装
PDF编程技能:使用PDFKit、PDF.js、Puppeteer生成、解析、合并PDF文档
86 周安装
Spring Boot 3 工程师技能指南:微服务、云原生与响应式编程最佳实践
86 周安装
第一性原理思维教练:苏格拉底式提问,拆解难题,从零重建创新解决方案
86 周安装
临床试验方案生成工具:基于检查点的模块化AI技能,支持医疗器械与药物研究
86 周安装