axiom-memory-debugging
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMemory Debugging
内存调试
Overview
概述
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.
内存问题表现为长时间使用后崩溃。核心原则:90%的内存泄漏遵循3种模式(循环引用、定时器/观察者泄漏、集合增长)。使用Instruments进行系统化诊断,切勿猜测。
Example Prompts
示例提问
These are real questions developers ask that this skill is designed to answer:
以下是开发者实际会问的问题,本技能专为解答这些问题设计:
1. "My app crashes after 10-15 minutes of use, but there are no error messages. How do I figure out what's leaking?"
1. "我的应用使用10-15分钟后崩溃,但没有错误信息。我该如何找出泄漏点?"
→ The skill covers systematic Instruments workflows to identify memory leaks vs normal memory pressure, with real diagnostic patterns
→ 本技能涵盖系统化的Instruments工作流程,用于区分内存泄漏与正常内存压力,并提供真实的诊断模式
2. "I'm seeing memory jump from 50MB to 200MB+ when I perform a specific action. Is this a leak or normal caching behavior?"
2. "当我执行某个特定操作时,内存从50MB跃升至200MB以上。这是泄漏还是正常的缓存行为?"
→ The skill distinguishes between progressive leaks (continuous growth) and temporary spikes (caches that stabilize), with diagnostic criteria
→ 本技能区分渐进式泄漏(持续增长)和临时峰值(稳定的缓存),并提供诊断标准
3. "View controllers don't seem to be deallocating after I dismiss them. How do I find the retain cycle causing this?"
3. "视图控制器在我关闭后似乎没有被释放。我该如何找到导致此问题的循环引用?"
→ The skill demonstrates Memory Graph Debugger techniques to identify objects holding strong references and common retain cycle patterns
→ 本技能演示如何使用Memory Graph Debugger技术识别持有强引用的对象,以及常见的循环引用模式
4. "I have timers/observers in my code and I think they're causing memory leaks. How do I verify and fix this?"
4. "我的代码中有定时器/观察者,我认为它们导致了内存泄漏。我该如何验证并修复?"
→ The skill covers the 5 diagnostic patterns, including specific timer and observer leak signatures with prevention strategies
→ 本技能涵盖5种诊断模式,包括特定的定时器和观察者泄漏特征以及预防策略
5. "My app uses 200MB of memory and I don't know if that's normal or if I have multiple leaks. How do I diagnose?"
5. "我的应用占用200MB内存,我不知道这是正常情况还是存在多处泄漏。我该如何诊断?"
→ The skill provides the Instruments decision tree to distinguish normal memory use, expected caches, and actual leaks requiring fixes
→ 本技能提供Instruments决策树,用于区分正常内存使用、预期缓存和需要修复的实际泄漏
Red Flags — Memory Leak Likely
危险信号——可能存在内存泄漏
If you see ANY of these, suspect memory leak not just heavy memory use:
- Progressive memory growth: 50MB → 100MB → 200MB (not plateauing)
- App crashes after 10-15 minutes with no error in Xcode console
- Memory warnings appear repeatedly in device logs
- Specific screen/operation makes memory jump (10-50MB spike)
- View controllers don't deallocate after dismiss (visible in Memory Graph Debugger)
- Same operation run multiple times causes linear memory growth
如果你遇到以下任何一种情况,怀疑是内存泄漏而非单纯的高内存使用:
- 渐进式内存增长:50MB → 100MB → 200MB(未趋于平稳)
- 应用运行10-15分钟后崩溃,Xcode控制台无错误信息
- 设备日志中反复出现内存警告
- 特定屏幕/操作导致内存骤增(10-50MB峰值)
- 视图控制器关闭后未被释放(在Memory Graph Debugger中可见)
- 重复执行同一操作导致内存线性增长
Difference from normal memory use
与正常内存使用的区别
- Normal: App uses 100MB, stays at 100MB (memory pressure handled by iOS)
- Leak: App uses 50MB, becomes 100MB, 150MB, 200MB → CRASH
- 正常情况:应用占用100MB,保持在100MB(iOS处理内存压力)
- 泄漏:应用占用50MB,变为100MB、150MB、200MB → 崩溃
Mandatory First Steps
必须执行的第一步
ALWAYS run these commands/checks FIRST (before reading code):
bash
undefined在查看代码之前,务必先运行以下命令/检查:
bash
undefined1. Check device logs for memory warnings
1. 检查设备日志中的内存警告
Connect device, open Xcode Console (Cmd+Shift+2)
连接设备,打开Xcode控制台(Cmd+Shift+2)
Trigger the crash scenario
触发崩溃场景
Look for: "Memory pressure critical", "Jetsam killed", "Low Memory"
查找:"Memory pressure critical"、"Jetsam killed"、"Low Memory"
2. Check which objects are leaking
2. 检查哪些对象存在泄漏
Use Memory Graph Debugger (below) — shows object count growth
使用Memory Graph Debugger(如下)——查看对象计数增长情况
3. Check instruments baseline
3. 检查Instruments基准线
Xcode → Product → Profile → Memory
Xcode → Product → Profile → Memory
Run for 1 minute, note baseline
运行1分钟,记录基准线
Perform operation 5 times, note if memory keeps growing
执行操作5次,查看内存是否持续增长
undefinedundefinedWhat this tells you
这些检查的作用
- Memory stays flat → Likely not a leak, check memory pressure handling
- Memory grows linearly → Classic leak (timer, observer, closure capture)
- Sudden spikes then flattens → Probably normal (caches, lazy loading)
- Spikes AND keeps growing → Compound leak (multiple leaks stacking)
- 内存保持平稳 → 可能不是泄漏,检查内存压力处理逻辑
- 内存线性增长 → 典型泄漏(定时器、观察者、闭包捕获)
- 突然峰值后趋于平稳 → 可能是正常情况(缓存、懒加载)
- 峰值且持续增长 → 复合泄漏(多个泄漏叠加)
Why diagnostics first
为什么先诊断
- Finding leak with Instruments: 5-15 minutes
- Guessing and testing fixes: 45+ minutes
- 使用Instruments查找泄漏:5-15分钟
- 猜测并测试修复:45分钟以上
Quick Decision Tree
快速决策树
Memory growing?
├─ Progressive growth every minute?
│ └─ Likely retain cycle or timer leak
├─ Spike when action performed?
│ └─ Check if operation runs multiple times
├─ Spike then flat for 30 seconds?
│ └─ Probably normal (collections, caches)
├─ Multiple large spikes stacking?
│ └─ Compound leak (multiple sources)
└─ Can't tell from visual inspection?
└─ Use Instruments Memory Graph (see below)内存是否增长?
├─ 每分钟都有渐进式增长?
│ └─ 可能是循环引用或定时器泄漏
├─ 执行操作时出现峰值?
│ └─ 检查是否重复执行该操作
├─ 出现峰值后平稳30秒?
│ └─ 可能是正常情况(集合、缓存)
├─ 多个大峰值叠加?
│ └─ 复合泄漏(多个泄漏源)
└─ 视觉检查无法判断?
└─ 使用Instruments Memory Graph(见下文)Detecting Leaks — Step by Step
检测泄漏——分步指南
Step 1: Memory Graph Debugger (Fastest Leak Detection)
步骤1:Memory Graph Debugger(最快的泄漏检测工具)
1. Open your app in Xcode simulator
2. Click: Debug → Memory Graph Debugger (or icon in top toolbar)
3. Wait for graph to generate (5-10 seconds)
4. Look for PURPLE/RED circles with "⚠" badge
5. Click them → Xcode shows retain cycle chain1. 在Xcode模拟器中打开你的应用
2. 点击:Debug → Memory Graph Debugger(或顶部工具栏中的图标)
3. 等待图表生成(5-10秒)
4. 查找带有"⚠"标记的紫色/红色圆圈
5. 点击它们 → Xcode会显示循环引用链What you're looking for
需要关注的内容
✅ Object appears once
❌ Object appears 2+ times (means it's retained multiple times)✅ 对象仅出现一次
❌ 对象出现2次以上(表示被多次持有)Example output (indicates leak)
示例输出(表示存在泄漏)
PlayerViewModel
↑ strongRef from: progressTimer
↑ strongRef from: TimerClosure [weak self] captured self
↑ CYCLE DETECTED: This creates a retain cycle!PlayerViewModel
↑ strongRef from: progressTimer
↑ strongRef from: TimerClosure [weak self] captured self
↑ CYCLE DETECTED: This creates a retain cycle!Step 2: Instruments (Detailed Memory Analysis)
步骤2:Instruments(详细内存分析)
1. Product → Profile (Cmd+I)
2. Select "Memory" template
3. Run scenario that causes memory growth
4. Perform action 5-10 times
5. Check: Does memory line go UP for each action?
- YES → Leak confirmed
- NO → Probably not a leak1. Product → Profile(Cmd+I)
2. 选择"Memory"模板
3. 运行导致内存增长的场景
4. 执行操作5-10次
5. 检查:每次操作后内存是否上升?
- 是 → 确认存在泄漏
- 否 → 可能不是泄漏Key instruments to check
需要检查的关键Instruments工具
- Heap Allocations: Shows object count
- Leaked Objects: Direct leak detection
- VM Tracker: Shows memory by type
- System Memory: Shows OS pressure
- Heap Allocations:显示对象计数
- Leaked Objects:直接检测泄漏
- VM Tracker:按类型显示内存占用
- System Memory:显示系统内存压力
How to read the graph
如何解读图表
Time ──→
Memory
│ ▗━━━━━━━━━━━━━━━━ ← Memory keeps growing (LEAK)
│ ▄▀
│ ▄▀
│ ▄
└─────────────────────
Action 1 2 3 4 5
vs normal pattern:
Time ──→
Memory
│ ▗━━━━━━━━━━━━━━━━━━ ← Memory plateaus (OK)
│ ▄▀
│▄
└─────────────────────
Action 1 2 3 4 5时间 ──→
内存
│ ▗━━━━━━━━━━━━━━━━ ← 内存持续增长(泄漏)
│ ▄▀
│ ▄▀
│ ▄
└─────────────────────
操作1 2 3 4 5
对比正常模式:
时间 ──→
内存
│ ▗━━━━━━━━━━━━━━━━━━ ← 内存趋于平稳(正常)
│ ▄▀
│▄
└─────────────────────
操作1 2 3 4 5Step 3: View Controller Memory Check
步骤3:视图控制器内存检查
For SwiftUI or UIKit view controllers:
swift
// SwiftUI: Check if view disappears cleanly
@main
struct DebugApp: App {
init() {
NotificationCenter.default.addObserver(
forName: NSNotification.Name("UIViewControllerWillDeallocate"),
object: nil,
queue: .main
) { _ in
print("✅ ViewController deallocated")
}
}
var body: some Scene { ... }
}
// UIKit: Add deinit logging
class MyViewController: UIViewController {
deinit {
print("✅ MyViewController deallocated")
}
}
// SwiftUI: Use deinit in view models
@MainActor
class ViewModel: ObservableObject {
deinit {
print("✅ ViewModel deallocated")
}
}针对SwiftUI或UIKit视图控制器:
swift
// SwiftUI:检查视图是否正常消失
@main
struct DebugApp: App {
init() {
NotificationCenter.default.addObserver(
forName: NSNotification.Name("UIViewControllerWillDeallocate"),
object: nil,
queue: .main
) { _ in
print("✅ ViewController deallocated")
}
}
var body: some Scene { ... }
}
// UIKit:添加deinit日志
class MyViewController: UIViewController {
deinit {
print("✅ MyViewController deallocated")
}
}
// SwiftUI:在视图模型中使用deinit
@MainActor
class ViewModel: ObservableObject {
deinit {
print("✅ ViewModel deallocated")
}
}Test procedure
测试流程
1. Add deinit logging above
2. Launch app in Xcode
3. Navigate to view/create ViewModel
4. Navigate away/dismiss
5. Check Console: Do you see "✅ deallocated"?
- YES → No leak there
- NO → Object is retained somewhere1. 添加上述deinit日志
2. 在Xcode中启动应用
3. 导航到目标视图/创建ViewModel
4. 离开/关闭该视图
5. 检查控制台:是否看到"✅ deallocated"?
- 是 → 此处无泄漏
- 否 → 对象被某处持有Jetsam (Memory Pressure Termination)
Jetsam(内存压力终止)
Jetsam is not a bug in your app — it's the system reclaiming memory from background apps to keep foreground apps responsive. However, frequent jetsam kills hurt user experience.
Jetsam不是应用中的bug——它是系统从后台应用回收内存,以保持前台应用响应的机制。然而,频繁的Jetsam终止会损害用户体验。
What Is Jetsam
什么是Jetsam
When system memory is low, iOS terminates background apps to free memory. This is called jetsam (memory pressure exit).
Key characteristics:
- Most common termination reason (more than crashes)
- Not a crash — no crash log generated
- User sees app restart when returning to it
- No debugger notification (only MetricKit/Organizer)
当系统内存不足时,iOS会终止后台应用以释放内存。这被称为Jetsam(内存压力退出)。
关键特征:
- 最常见的终止原因(比崩溃更频繁)
- 不是崩溃——不会生成崩溃日志
- 用户返回应用时会看到应用重启
- 调试器无通知(仅MetricKit/Organizer中有记录)
Jetsam vs Memory Limit Exceeded
Jetsam与内存超限的区别
| Termination | Cause | Solution |
|---|---|---|
| Memory Limit Exceeded | Your app used too much memory (foreground OR background) | Reduce peak memory footprint |
| Jetsam | System needed memory for other apps | Reduce background memory to <50MB |
Both show as "memory" terminations but have different causes and fixes.
| 终止类型 | 原因 | 解决方案 |
|---|---|---|
| 内存超限 | 应用占用过多内存(前台或后台) | 降低峰值内存占用 |
| Jetsam | 系统需要为其他应用释放内存 | 将后台内存占用降至50MB以下 |
两者都显示为"内存"相关终止,但原因和解决方案不同。
Device Memory Limits
设备内存限制
Memory limits vary by device. Older devices have stricter limits:
| Device | Approx. Memory Limit | Safe Target |
|---|---|---|
| iPhone 6s | ~200MB | 150MB |
| iPhone X | ~400MB | 300MB |
| iPhone 12 | ~500MB | 400MB |
| iPhone 14 Pro | ~600MB | 500MB |
| iPad (varies) | ~300-800MB | Check device |
Note: Limits are NOT documented by Apple and vary by iOS version. Test on oldest supported device.
内存限制因设备而异。旧设备的限制更严格:
| 设备 | 大致内存限制 | 安全目标 |
|---|---|---|
| iPhone 6s | ~200MB | 150MB |
| iPhone X | ~400MB | 300MB |
| iPhone 12 | ~500MB | 400MB |
| iPhone 14 Pro | ~600MB | 500MB |
| iPad(不同型号) | ~300-800MB | 查看具体设备 |
注意:苹果未公开这些限制,且会随iOS版本变化。在最旧的支持设备上进行测试。
Monitoring Jetsam with MetricKit
使用MetricKit监控Jetsam
swift
import MetricKit
class JetsamMonitor: NSObject, MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
guard let exitData = payload.applicationExitMetrics else { continue }
// Background exits (jetsam happens here)
let bgData = exitData.backgroundExitData
let jetsamCount = bgData.cumulativeMemoryPressureExitCount
let memoryLimitCount = bgData.cumulativeMemoryResourceLimitExitCount
if jetsamCount > 0 {
print("⚠️ Jetsam kills: \(jetsamCount)")
// Send to analytics
}
if memoryLimitCount > 0 {
print("⚠️ Memory limit exceeded: \(memoryLimitCount)")
// This is YOUR app using too much memory
}
}
}
}swift
import MetricKit
class JetsamMonitor: NSObject, MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
guard let exitData = payload.applicationExitMetrics else { continue }
// 后台退出(Jetsam发生在此场景)
let bgData = exitData.backgroundExitData
let jetsamCount = bgData.cumulativeMemoryPressureExitCount
let memoryLimitCount = bgData.cumulativeMemoryResourceLimitExitCount
if jetsamCount > 0 {
print("⚠️ Jetsam kills: \(jetsamCount)")
// 发送至分析平台
}
if memoryLimitCount > 0 {
print("⚠️ Memory limit exceeded: \(memoryLimitCount)")
// 这是你的应用占用了过多内存
}
}
}
}Reducing Jetsam Rate
降低Jetsam终止率
Goal: Keep background memory under 50MB.
目标:将后台内存占用保持在50MB以下。
Upon Backgrounding
进入后台时的处理
swift
class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) {
// 1. Flush state to disk
saveApplicationState()
// 2. Clear image caches
URLCache.shared.removeAllCachedResponses()
imageCache.removeAllObjects()
// 3. Release large data structures
largeDataStore.flush()
// 4. Clear view controllers not visible
releaseOffscreenViewControllers()
// 5. Log memory after cleanup
logMemoryUsage("Background cleanup complete")
}
private func logMemoryUsage(_ context: String) {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
if result == KERN_SUCCESS {
let memoryMB = Double(info.resident_size) / 1_000_000
print("📊 [\(context)] Memory: \(String(format: "%.1f", memoryMB))MB")
}
}
}swift
class AppDelegate: UIResponder, UIApplicationDelegate {
func applicationDidEnterBackground(_ application: UIApplication) {
// 1. 将状态刷新到磁盘
saveApplicationState()
// 2. 清除图片缓存
URLCache.shared.removeAllCachedResponses()
imageCache.removeAllObjects()
// 3. 释放大型数据结构
largeDataStore.flush()
// 4. 释放不可见的视图控制器
releaseOffscreenViewControllers()
// 5. 记录清理后的内存占用
logMemoryUsage("Background cleanup complete")
}
private func logMemoryUsage(_ context: String) {
var info = mach_task_basic_info()
var count = mach_msg_type_number_t(MemoryLayout<mach_task_basic_info>.size) / 4
let result = withUnsafeMutablePointer(to: &info) {
$0.withMemoryRebound(to: integer_t.self, capacity: 1) {
task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count)
}
}
if result == KERN_SUCCESS {
let memoryMB = Double(info.resident_size) / 1_000_000
print("📊 [\(context)] Memory: \(String(format: "%.1f", memoryMB))MB")
}
}
}SwiftUI: Clearing State on Background
SwiftUI:进入后台时清除状态
swift
@main
struct MyApp: App {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var imageCache = ImageCache()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(imageCache)
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
// Clear caches to reduce jetsam
imageCache.clearAll()
}
}
}
}swift
@main
struct MyApp: App {
@Environment(\.scenePhase) private var scenePhase
@StateObject private var imageCache = ImageCache()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(imageCache)
}
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .background {
// 清除缓存以减少Jetsam终止
imageCache.clearAll()
}
}
}
}Recovering from Jetsam
从Jetsam终止中恢复
Users shouldn't notice your app was terminated. Implement state restoration:
用户不应察觉到应用被终止。实现状态恢复:
UIKit State Restoration
UIKit状态恢复
swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
// Save state when backgrounding
let activity = NSUserActivity(activityType: "com.app.state")
activity.userInfo = [
"currentTab": tabController.selectedIndex,
"scrollPosition": tableView.contentOffset.y,
"draftText": textField.text ?? ""
]
return activity
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// Restore state on launch
if let activity = connectionOptions.userActivities.first {
restoreState(from: activity)
}
}
private func restoreState(from activity: NSUserActivity) {
guard let userInfo = activity.userInfo else { return }
if let tabIndex = userInfo["currentTab"] as? Int {
tabController.selectedIndex = tabIndex
}
if let scrollY = userInfo["scrollPosition"] as? CGFloat {
tableView.setContentOffset(CGPoint(x: 0, y: scrollY), animated: false)
}
if let draftText = userInfo["draftText"] as? String {
textField.text = draftText
}
}
}swift
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
// 进入后台时保存状态
let activity = NSUserActivity(activityType: "com.app.state")
activity.userInfo = [
"currentTab": tabController.selectedIndex,
"scrollPosition": tableView.contentOffset.y,
"draftText": textField.text ?? ""
]
return activity
}
func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions) {
// 启动时恢复状态
if let activity = connectionOptions.userActivities.first {
restoreState(from: activity)
}
}
private func restoreState(from activity: NSUserActivity) {
guard let userInfo = activity.userInfo else { return }
if let tabIndex = userInfo["currentTab"] as? Int {
tabController.selectedIndex = tabIndex
}
if let scrollY = userInfo["scrollPosition"] as? CGFloat {
tableView.setContentOffset(CGPoint(x: 0, y: scrollY), animated: false)
}
if let draftText = userInfo["draftText"] as? String {
textField.text = draftText
}
}
}SwiftUI State Restoration
SwiftUI状态恢复
swift
struct ContentView: View {
@SceneStorage("selectedTab") private var selectedTab = 0
@SceneStorage("draftText") private var draftText = ""
@SceneStorage("scrollPosition") private var scrollPosition: Double = 0
var body: some View {
TabView(selection: $selectedTab) {
// Tabs...
}
}
}swift
struct ContentView: View {
@SceneStorage("selectedTab") private var selectedTab = 0
@SceneStorage("draftText") private var draftText = ""
@SceneStorage("scrollPosition") private var scrollPosition: Double = 0
var body: some View {
TabView(selection: $selectedTab) {
// 标签页...
}
}
}What to Restore After Jetsam
Jetsam终止后需要恢复的状态
| State | Priority | Example |
|---|---|---|
| Navigation position | High | Current tab, navigation stack |
| User input (drafts) | High | Text field content, unsent messages |
| Media playback position | High | Video/audio timestamp |
| Scroll position | Medium | Table/collection scroll offset |
| Search query | Medium | Active search text |
| Filter selections | Low | Sort order, filter toggles |
| 状态 | 优先级 | 示例 |
|---|---|---|
| 导航位置 | 高 | 当前标签页、导航栈 |
| 用户输入(草稿) | 高 | 文本框内容、未发送消息 |
| 媒体播放位置 | 高 | 视频/音频时间戳 |
| 滚动位置 | 中 | 表格/集合视图滚动偏移 |
| 搜索查询 | 中 | 活跃搜索文本 |
| 筛选选项 | 低 | 排序方式、筛选开关 |
Jetsam Debugging Checklist
Jetsam调试清单
- Check Organizer > Terminations > Memory Pressure for jetsam rate
- Add MetricKit to track background exits
- Profile background memory (should be <50MB)
- Clear caches in
applicationDidEnterBackground - Release images and large data structures
- Implement state restoration (users shouldn't notice restart)
- Test on oldest supported device (lowest memory limits)
- Verify restoration works after simulated memory pressure
- 检查Organizer > Terminations > Memory Pressure中的Jetsam终止率
- 添加MetricKit以跟踪后台退出
- 分析后台内存占用(应低于50MB)
- 在中清除缓存
applicationDidEnterBackground - 释放图片和大型数据结构
- 实现状态恢复(用户不应察觉到应用重启)
- 在最旧的支持设备上测试(内存限制最低)
- 验证模拟内存压力后恢复功能正常
Simulating Memory Pressure
模拟内存压力
bash
undefinedbash
undefinedIn Simulator
在模拟器中
Debug > Simulate Memory Warning
Debug > Simulate Memory Warning
On device (Instruments)
在设备上(使用Instruments)
Use Memory template, trigger warnings manually
使用Memory模板,手动触发警告
undefinedundefinedJetsam vs Leak Quick Distinction
Jetsam与泄漏的快速区分
App memory grows while in USE?
├─ YES → Memory leak (fix retention)
└─ NO, but app killed in BACKGROUND → Jetsam (reduce bg memory)应用在使用时内存增长?
├─ 是 → 内存泄漏(修复引用问题)
└─ 否,但应用在后台被终止 → Jetsam(降低后台内存占用)Common Memory Leak Patterns (With Fixes)
常见内存泄漏模式(含修复方案)
Pattern 1: Timer Leaks (Most Common)
模式1:定时器泄漏(最常见)
❌ Leak — Timer retains closure, closure retains self
❌ 泄漏——定时器持有闭包,闭包持有self
swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
private var progressTimer: Timer?
func startPlayback(_ track: Track) {
currentTrack = track
// LEAK: Timer.scheduledTimer captures 'self' in closure
// Even with [weak self], the Timer itself is strong
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
// Timer is never stopped → keeps firing forever
}
// Missing: Timer never invalidated
deinit {
// LEAK: If timer still running, deinit never called
}
}swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
private var progressTimer: Timer?
func startPlayback(_ track: Track) {
currentTrack = track
// 泄漏:Timer.scheduledTimer在闭包中捕获'self'
// 即使使用[weak self],Timer本身仍为强引用
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
// 定时器从未停止 → 永远触发
}
// 缺失:定时器从未失效
deinit {
// 泄漏:如果定时器仍在运行,deinit永远不会被调用
}
}Leak mechanism
泄漏机制
ViewController → strongly retains ViewModel
↓
ViewModel → strongly retains Timer
↓
Timer → strongly retains closure
↓
Closure → captures [weak self] but still holds reference to TimerViewController → 强持有 ViewModel
↓
ViewModel → 强持有 Timer
↓
Timer → 强持有 闭包
↓
Closure → 捕获[weak self]但仍持有Timer的引用Closure captures self
weakly BUT
self闭包捕获self
为弱引用但仍泄漏的原因
self- Timer is still strong reference in ViewModel
- Timer is still running (repeats: true)
- Even with [weak self], timer closure doesn't go away
- ViewModel中仍持有Timer的强引用
- Timer仍在运行(repeats: true)
- 即使使用[weak self],定时器闭包也不会被释放
✅ Fix 1: Invalidate on deinit
✅ 修复方案1:在deinit中失效定时器
swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
private var progressTimer: Timer?
func startPlayback(_ track: Track) {
currentTrack = track
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
}
func stopPlayback() {
progressTimer?.invalidate()
progressTimer = nil // Important: nil after invalidate
currentTrack = nil
}
deinit {
progressTimer?.invalidate() // ← CRITICAL FIX
progressTimer = nil
}
}swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
private var progressTimer: Timer?
func startPlayback(_ track: Track) {
currentTrack = track
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.updateProgress()
}
}
func stopPlayback() {
progressTimer?.invalidate()
progressTimer = nil // 重要:失效后置为nil
currentTrack = nil
}
deinit {
progressTimer?.invalidate() // ← 关键修复
progressTimer = nil
}
}✅ Fix 2: Use AnyCancellable (Modern approach)
✅ 修复方案2:使用AnyCancellable(现代方案)
swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
private var cancellable: AnyCancellable?
func startPlayback(_ track: Track) {
currentTrack = track
// Timer with Combine - auto-cancels when cancellable is released
cancellable = Timer.publish(
every: 1.0,
tolerance: 0.1,
on: .main,
in: .default
)
.autoconnect()
.sink { [weak self] _ in
self?.updateProgress()
}
}
func stopPlayback() {
cancellable?.cancel() // Auto-cleans up
cancellable = nil
currentTrack = nil
}
// No need for deinit — Combine handles cleanup
}swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
private var cancellable: AnyCancellable?
func startPlayback(_ track: Track) {
currentTrack = track
// 使用Combine的Timer - 当cancellable被释放时自动取消
cancellable = Timer.publish(
every: 1.0,
tolerance: 0.1,
on: .main,
in: .default
)
.autoconnect()
.sink { [weak self] _ in
self?.updateProgress()
}
}
func stopPlayback() {
cancellable?.cancel() // 自动清理
cancellable = nil
currentTrack = nil
}
// 无需deinit —— Combine处理清理
}✅ Fix 3: Weak self + nil check (Emergency fix)
✅ 修复方案3:弱引用self + nil检查(紧急修复)
swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
private var progressTimer: Timer?
func startPlayback(_ track: Track) {
currentTrack = track
// If progressTimer already exists, stop it first
progressTimer?.invalidate()
progressTimer = nil
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else {
// If self deallocated, timer still fires but does nothing
// Still not ideal - timer keeps consuming CPU
return
}
self.updateProgress()
}
}
func stopPlayback() {
progressTimer?.invalidate()
progressTimer = nil
}
deinit {
progressTimer?.invalidate()
progressTimer = nil
}
}swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentTrack: Track?
private var progressTimer: Timer?
func startPlayback(_ track: Track) {
currentTrack = track
// 如果progressTimer已存在,先停止它
progressTimer?.invalidate()
progressTimer = nil
progressTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
guard let self = self else {
// 如果self已被释放,定时器仍会触发但无操作
// 仍不理想 - 定时器仍会消耗CPU
return
}
self.updateProgress()
}
}
func stopPlayback() {
progressTimer?.invalidate()
progressTimer = nil
}
deinit {
progressTimer?.invalidate()
progressTimer = nil
}
}Why the fixes work
修复方案的工作原理
- : Stops timer immediately, breaks retain cycle
invalidate() - : Automatically invalidates when released
cancellable - : If ViewModel released before timer, timer becomes no-op
[weak self] - : Ensures timer always cleaned up
deinit cleanup
- :立即停止定时器,打破循环引用
invalidate() - :被释放时自动失效
cancellable - :如果ViewModel在定时器前被释放,定时器变为空操作
[weak self] - :确保定时器始终被清理
deinit清理
Test the fix
测试修复效果
swift
func testPlayerViewModelNotLeaked() {
var viewModel: PlayerViewModel? = PlayerViewModel()
let track = Track(id: "1", title: "Song")
viewModel?.startPlayback(track)
// Verify timer running
XCTAssertNotNil(viewModel?.progressTimer)
// Stop and deallocate
viewModel?.stopPlayback()
viewModel = nil
// ✅ Should deallocate without leak warning
}swift
func testPlayerViewModelNotLeaked() {
var viewModel: PlayerViewModel? = PlayerViewModel()
let track = Track(id: "1", title: "Song")
viewModel?.startPlayback(track)
// 验证定时器正在运行
XCTAssertNotNil(viewModel?.progressTimer)
// 停止并释放
viewModel?.stopPlayback()
viewModel = nil
// ✅ 应无泄漏警告地释放
}Pattern 2: Observer/Notification Leaks
模式2:观察者/通知泄漏
❌ Leak — Observer holds strong reference to self
❌ 泄漏——观察者持有self的强引用
swift
@MainActor
class PlayerViewModel: ObservableObject {
init() {
// LEAK: addObserver keeps strong reference to self
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionChange),
name: AVAudioSession.routeChangeNotification,
object: nil
)
// No matching removeObserver → accumulates listeners
}
@objc private func handleAudioSessionChange() { }
deinit {
// Missing: Never unregistered
}
}swift
@MainActor
class PlayerViewModel: ObservableObject {
init() {
// 泄漏:addObserver持有self的强引用
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionChange),
name: AVAudioSession.routeChangeNotification,
object: nil
)
// 没有对应的removeObserver → 监听器不断累积
}
@objc private func handleAudioSessionChange() { }
deinit {
// 缺失:从未注销
}
}✅ Fix 1: Manual cleanup in deinit
✅ 修复方案1:在deinit中手动清理
swift
@MainActor
class PlayerViewModel: ObservableObject {
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionChange),
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
@objc private func handleAudioSessionChange() { }
deinit {
NotificationCenter.default.removeObserver(self) // ← FIX
}
}swift
@MainActor
class PlayerViewModel: ObservableObject {
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleAudioSessionChange),
name: AVAudioSession.routeChangeNotification,
object: nil
)
}
@objc private func handleAudioSessionChange() { }
deinit {
NotificationCenter.default.removeObserver(self) // ← 修复
}
}✅ Fix 2: Use modern Combine approach (Best practice)
✅ 修复方案2:使用现代Combine方案(最佳实践)
swift
@MainActor
class PlayerViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(
for: AVAudioSession.routeChangeNotification
)
.sink { [weak self] _ in
self?.handleAudioSessionChange()
}
.store(in: &cancellables) // Auto-cleanup with viewModel
}
private func handleAudioSessionChange() { }
// No deinit needed - cancellables auto-cleanup
}swift
@MainActor
class PlayerViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(
for: AVAudioSession.routeChangeNotification
)
.sink { [weak self] _ in
self?.handleAudioSessionChange()
}
.store(in: &cancellables) // 随viewModel自动清理
}
private func handleAudioSessionChange() { }
// 无需deinit - cancellables自动清理
}✅ Fix 3: Use @Published with map (Reactive)
✅ 修复方案3:使用@Published和map(响应式)
swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentRoute: AVAudioSession.AudioSessionRouteDescription?
private var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(
for: AVAudioSession.routeChangeNotification
)
.map { _ in AVAudioSession.sharedInstance().currentRoute }
.assign(to: &$currentRoute) // Auto-cleanup with publisher chain
}
}swift
@MainActor
class PlayerViewModel: ObservableObject {
@Published var currentRoute: AVAudioSession.AudioSessionRouteDescription?
private var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(
for: AVAudioSession.routeChangeNotification
)
.map { _ in AVAudioSession.sharedInstance().currentRoute }
.assign(to: &$currentRoute) // 随发布链自动清理
}
}Pattern 3: Closure Capture Leaks (Collection/Array)
模式3:闭包捕获泄漏(集合/数组)
❌ Leak — Closure captured in array, captures self
❌ 泄漏——闭包被数组捕获,闭包捕获self
swift
@MainActor
class PlaylistViewController: UIViewController {
private var tracks: [Track] = []
private var updateCallbacks: [(Track) -> Void] = [] // LEAK SOURCE
func addUpdateCallback() {
// LEAK: Closure captures 'self'
updateCallbacks.append { [self] track in
self.refreshUI(with: track) // Strong capture of self
}
// updateCallbacks grows and never cleared
}
// No mechanism to clear callbacks
deinit {
// updateCallbacks still references self
}
}swift
@MainActor
class PlaylistViewController: UIViewController {
private var tracks: [Track] = []
private var updateCallbacks: [(Track) -> Void] = [] // 泄漏源
func addUpdateCallback() {
// 泄漏:闭包捕获'self'
updateCallbacks.append { [self] track in
self.refreshUI(with: track) // 强引用self
}
// updateCallbacks不断增长且从未被清理
}
// 没有清理回调的机制
deinit {
// updateCallbacks仍引用self
}
}Leak mechanism
泄漏机制
ViewController
↓ strongly owns
updateCallbacks array
↓ contains
Closure captures self
↓ CYCLE
Back to ViewController (can't deallocate)ViewController
↓ 强持有
updateCallbacks数组
↓ 包含
闭包捕获self
↓ 循环
回到ViewController(无法释放)✅ Fix 1: Use weak self in closure
✅ 修复方案1:在闭包中使用弱引用self
swift
@MainActor
class PlaylistViewController: UIViewController {
private var tracks: [Track] = []
private var updateCallbacks: [(Track) -> Void] = []
func addUpdateCallback() {
updateCallbacks.append { [weak self] track in
self?.refreshUI(with: track) // Weak capture
}
}
deinit {
updateCallbacks.removeAll() // Clean up array
}
}swift
@MainActor
class PlaylistViewController: UIViewController {
private var tracks: [Track] = []
private var updateCallbacks: [(Track) -> Void] = []
func addUpdateCallback() {
updateCallbacks.append { [weak self] track in
self?.refreshUI(with: track) // 弱引用
}
}
deinit {
updateCallbacks.removeAll() // 清理数组
}
}✅ Fix 2: Use unowned (when you're certain self lives longer)
✅ 修复方案2:使用unowned(当确定self生命周期更长时)
swift
@MainActor
class PlaylistViewController: UIViewController {
private var updateCallbacks: [(Track) -> Void] = []
func addUpdateCallback() {
updateCallbacks.append { [unowned self] track in
self.refreshUI(with: track) // Unowned is faster
}
// Use unowned ONLY if callback always destroyed before ViewController
}
deinit {
updateCallbacks.removeAll()
}
}swift
@MainActor
class PlaylistViewController: UIViewController {
private var updateCallbacks: [(Track) -> Void] = []
func addUpdateCallback() {
updateCallbacks.append { [unowned self] track in
self.refreshUI(with: track) // Unowned更快
}
// 仅当回调始终在ViewController之前销毁时使用unowned
}
deinit {
updateCallbacks.removeAll()
}
}✅ Fix 3: Cancel callbacks when done (Reactive)
✅ 修复方案3:完成后取消回调(响应式)
swift
@MainActor
class PlaylistViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
func addUpdateCallback(_ handler: @escaping (Track) -> Void) {
// Use PassthroughSubject instead of array
Just(())
.sink { [weak self] in
handler(/* track */)
}
.store(in: &cancellables)
}
// When done:
func clearCallbacks() {
cancellables.removeAll() // Cancels all subscriptions
}
}swift
@MainActor
class PlaylistViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
func addUpdateCallback(_ handler: @escaping (Track) -> Void) {
// 使用PassthroughSubject代替数组
Just(())
.sink { [weak self] in
handler(/* track */)
}
.store(in: &cancellables)
}
// 完成时:
func clearCallbacks() {
cancellables.removeAll() // 取消所有订阅
}
}Test the fix
测试修复效果
swift
func testCallbacksNotLeak() {
var viewController: PlaylistViewController? = PlaylistViewController()
viewController?.addUpdateCallback { _ in }
// Verify callback registered
XCTAssert(viewController?.updateCallbacks.count ?? 0 > 0)
// Clear and deallocate
viewController?.updateCallbacks.removeAll()
viewController = nil
// ✅ Should deallocate
}swift
func testCallbacksNotLeak() {
var viewController: PlaylistViewController? = PlaylistViewController()
viewController?.addUpdateCallback { _ in }
// 验证回调已注册
XCTAssert(viewController?.updateCallbacks.count ?? 0 > 0)
// 清理并释放
viewController?.updateCallbacks.removeAll()
viewController = nil
// ✅ 应被释放
}Pattern 4: Strong Reference Cycles (Closures + Properties)
模式4:强引用循环(闭包 + 属性)
❌ Leak — Two objects strongly reference each other
❌ 泄漏——两个对象互相强引用
swift
@MainActor
class Player: NSObject {
var delegate: PlayerDelegate? // Strong reference
var onPlaybackEnd: (() -> Void)? // ← Closure captures self
init(delegate: PlayerDelegate) {
self.delegate = delegate
// LEAK CYCLE:
// Player → (owns) → delegate
// delegate → (through closure) → owns → Player
}
}
class PlaylistController: PlayerDelegate {
var player: Player?
override init() {
super.init()
self.player = Player(delegate: self) // Self-reference cycle
player?.onPlaybackEnd = { [self] in
// LEAK: Closure captures self
// self owns player
// player owns delegate (self)
// Cycle!
self.playNextTrack()
}
}
}swift
@MainActor
class Player: NSObject {
var delegate: PlayerDelegate? // 强引用
var onPlaybackEnd: (() -> Void)? // ← 闭包捕获self
init(delegate: PlayerDelegate) {
self.delegate = delegate
// 泄漏循环:
// Player → 持有 → delegate
// delegate → 通过闭包 → 持有 → Player
}
}
class PlaylistController: PlayerDelegate {
var player: Player?
override init() {
super.init()
self.player = Player(delegate: self) // 自引用循环
player?.onPlaybackEnd = { [self] in
// 泄漏:闭包捕获self
// self持有player
// player持有delegate(self)
// 循环!
self.playNextTrack()
}
}
}✅ Fix: Break cycle with weak self
✅ 修复方案:使用弱引用self打破循环
swift
@MainActor
class PlaylistController: PlayerDelegate {
var player: Player?
override init() {
super.init()
self.player = Player(delegate: self)
player?.onPlaybackEnd = { [weak self] in
// Weak self breaks the cycle
self?.playNextTrack()
}
}
deinit {
player?.onPlaybackEnd = nil // Optional cleanup
player = nil
}
}swift
@MainActor
class PlaylistController: PlayerDelegate {
var player: Player?
override init() {
super.init()
self.player = Player(delegate: self)
player?.onPlaybackEnd = { [weak self] in
// 弱引用self打破循环
self?.playNextTrack()
}
}
deinit {
player?.onPlaybackEnd = nil // 可选清理
player = nil
}
}Pattern 5: View/Layout Callback Leaks
模式5:视图/布局回调泄漏
❌ Leak — View layout callback retains view controller
❌ 泄漏——视图布局回调持有视图控制器
swift
@MainActor
class DetailViewController: UIViewController {
let customView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// LEAK: layoutIfNeeded closure captures self
customView.layoutIfNeeded = { [self] in
// Every layout triggers this, keeping self alive
self.updateLayout()
}
}
}swift
@MainActor
class DetailViewController: UIViewController {
let customView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
// 泄漏:layoutIfNeeded闭包捕获self
customView.layoutIfNeeded = { [self] in
// 每次布局都会触发此回调,保持self存活
self.updateLayout()
}
}
}✅ Fix: Use @IBAction or proper delegation pattern
✅ 修复方案:使用@IBAction或适当的委托模式
swift
@MainActor
class DetailViewController: UIViewController {
@IBOutlet weak var customView: CustomView!
override func viewDidLoad() {
super.viewDidLoad()
customView.delegate = self // Weak reference through protocol
}
deinit {
customView?.delegate = nil // Clean up
}
}
protocol CustomViewDelegate: AnyObject { // AnyObject = weak by default
func customViewDidLayout(_ view: CustomView)
}swift
@MainActor
class DetailViewController: UIViewController {
@IBOutlet weak var customView: CustomView!
override func viewDidLoad() {
super.viewDidLoad()
customView.delegate = self // 协议弱引用
}
deinit {
customView?.delegate = nil // 清理
}
}
protocol CustomViewDelegate: AnyObject { // AnyObject = 默认弱引用
func customViewDidLayout(_ view: CustomView)
}Pattern 6: PhotoKit Image Request Leaks
模式6:PhotoKit图片请求泄漏
❌ Leak — PHImageManager requests accumulate without cancellation
❌ 泄漏——PHImageManager请求未取消,不断累积
This pattern is specific to photo/media apps using PhotoKit or similar async image loading APIs.
swift
// LEAK: Image requests not cancelled when cells scroll away
class PhotoViewController: UIViewController {
let imageManager = PHImageManager.default()
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let asset = photos[indexPath.item]
// LEAK: Requests accumulate - never cancelled
imageManager.requestImage(
for: asset,
targetSize: thumbnailSize,
contentMode: .aspectFill,
options: nil
) { [weak self] image, _ in
cell.imageView.image = image // Still called even if cell scrolled away
}
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// Each scroll triggers 50+ new image requests
// Previous requests still pending, accumulating in queue
}
}该模式特定于使用PhotoKit或类似异步图片加载API的照片/媒体应用。
swift
// 泄漏:单元格滚动离开时图片请求未取消
class PhotoViewController: UIViewController {
let imageManager = PHImageManager.default()
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let asset = photos[indexPath.item]
// 泄漏:请求不断累积 - 从未取消
imageManager.requestImage(
for: asset,
targetSize: thumbnailSize,
contentMode: .aspectFill,
options: nil
) { [weak self] image, _ in
cell.imageView.image = image // 即使单元格滚动离开仍会被调用
}
return cell
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
// 每次滚动触发50+新图片请求
// 之前的请求仍在等待,在队列中累积
}
}Symptoms
症状
- Memory jumps 50MB+ when scrolling long photo lists
- Crashes happen after scrolling through 100+ photos
- Specific operation causes leak (photo scrolling, not other screens)
- Works fine locally with 10 photos, crashes on user devices with 1000+ photos
Root cause returns a that must be explicitly cancelled. Without cancellation, pending requests queue up and hold memory.
PHImageManager.requestImage()PHImageRequestID- 滚动长照片列表时内存骤增50MB+
- 滚动100+张照片后崩溃
- 特定操作导致泄漏(照片滚动,而非其他屏幕)
- 本地10张照片正常,用户设备1000+张照片时崩溃
根本原因 返回的必须显式取消。如果不取消,待处理的请求会排队并占用内存。
PHImageManager.requestImage()PHImageRequestID✅ Fix: Store request ID and cancel in prepareForReuse()
✅ 修复方案:存储请求ID并在prepareForReuse()中取消
swift
class PhotoCell: UICollectionViewCell {
@IBOutlet weak var imageView: UIImageView!
private var imageRequestID: PHImageRequestID = PHInvalidImageRequestID
func configure(with asset: PHAsset, imageManager: PHImageManager) {
// Cancel previous request before starting new one
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()
// CRITICAL: Cancel pending request when cell is reused
if imageRequestID != PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(imageRequestID)
imageRequestID = PHInvalidImageRequestID
}
imageView.image = nil // Clear stale image
}
deinit {
// Safety check - shouldn't be needed if prepareForReuse called
if imageRequestID != PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(imageRequestID)
}
}
}
// Controller
class PhotoViewController: UIViewController, UICollectionViewDataSource {
let imageManager = PHImageManager.default()
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell",
for: indexPath) as! PhotoCell
let asset = photos[indexPath.item]
cell.configure(with: asset, imageManager: imageManager)
return cell
}
}swift
class PhotoCell: UICollectionViewCell {
@IBOutlet weak var imageView: UIImageView!
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 // 清除旧图片
}
deinit {
// 安全检查 - 如果prepareForReuse被调用则不需要
if imageRequestID != PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(imageRequestID)
}
}
}
// 控制器
class PhotoViewController: UIViewController, UICollectionViewDataSource {
let imageManager = PHImageManager.default()
func collectionView(_ collectionView: UICollectionView,
cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCell",
for: indexPath) as! PhotoCell
let asset = photos[indexPath.item]
cell.configure(with: asset, imageManager: imageManager)
return cell
}
}Key points
关键点
- Store in cell (not in view controller)
PHImageRequestID - Cancel BEFORE starting new request (prevents request storms)
- Cancel in (critical for collection views)
prepareForReuse() - Check before cancelling
imageRequestID != PHInvalidImageRequestID
- 在单元格中存储(而非视图控制器)
PHImageRequestID - 开始新请求前取消(防止请求风暴)
- 在中取消(对集合视图至关重要)
prepareForReuse() - 取消前检查
imageRequestID != PHInvalidImageRequestID
Other async APIs with similar patterns
其他具有类似模式的异步API
- → call
AVAssetImageGenerator.generateCGImagesAsynchronously()cancelAllCGImageGeneration() - → call
URLSession.dataTask()on taskcancel() - Custom image caches → implement or
invalidate()methodcancel()
- → 调用
AVAssetImageGenerator.generateCGImagesAsynchronously()cancelAllCGImageGeneration() - → 调用
URLSession.dataTask()取消任务cancel() - 自定义图片缓存 → 实现或
invalidate()方法cancel()
Debugging Non-Reproducible Memory Issues
调试不可重现的内存问题
Challenge Memory leak only happens with specific user data (large photo collections, complex data models) that you can't reproduce locally.
挑战 内存泄漏仅在特定用户数据(大照片集、复杂数据模型)下发生,无法在本地重现。
Step 1: Enable Remote Memory Diagnostics
步骤1:启用远程内存诊断
Add MetricKit diagnostics to your app:
swift
import MetricKit
class MemoryDiagnosticsManager {
static let shared = MemoryDiagnosticsManager()
private let metricManager = MXMetricManager.shared
func startMonitoring() {
metricManager.add(self)
}
}
extension MemoryDiagnosticsManager: MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
if let memoryMetrics = payload.memoryMetrics {
let peakMemory = memoryMetrics.peakMemoryUsage
// Log if exceeding threshold
if peakMemory > 400_000_000 { // 400MB
print("⚠️ High memory: \(peakMemory / 1_000_000)MB")
// Send to analytics
}
}
}
}
}在应用中添加MetricKit诊断:
swift
import MetricKit
class MemoryDiagnosticsManager {
static let shared = MemoryDiagnosticsManager()
private let metricManager = MXMetricManager.shared
func startMonitoring() {
metricManager.add(self)
}
}
extension MemoryDiagnosticsManager: MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
if let memoryMetrics = payload.memoryMetrics {
let peakMemory = memoryMetrics.peakMemoryUsage
// 超过阈值时记录
if peakMemory > 400_000_000 { // 400MB
print("⚠️ High memory: \(peakMemory / 1_000_000)MB")
// 发送至分析平台
}
}
}
}
}Step 2: Ask Users for Device Logs
步骤2:请求用户提供设备日志
When user reports crash:
- iPhone → Settings → Privacy & Security → Analytics → Analytics Data
- Look for latest crash log (named like )
YourApp_2024-01-15-12-45-23 - Email or upload to your support system
- Xcode → Window → Devices & Simulators → select device → View Device Logs
- Search for "Memory" or "Jetsam" in logs
当用户报告崩溃时:
- iPhone → 设置 → 隐私与安全性 → 分析与改进 → 分析数据
- 查找最新的崩溃日志(名称类似)
YourApp_2024-01-15-12-45-23 - 通过电子邮件或上传至支持系统
- Xcode → Window → Devices & Simulators → 选择设备 → View Device Logs
- 在日志中搜索"Memory"或"Jetsam"
Step 3: TestFlight Beta Testing
步骤3:TestFlight Beta测试
Before App Store release:
swift
#if DEBUG
// Add to AppDelegate
import os.log
let logger = os.log(subsystem: "com.yourapp.memory", category: "lifecycle")
// Log memory milestones
func logMemory(_ event: String) {
let memoryUsage = ProcessInfo.processInfo.physicalMemory / 1_000_000
os.log("🔍 [%s] Memory: %dMB", log: logger, type: .info, event, memoryUsage)
}
#endifSend TestFlight build to affected users:
- Build → Archive → Distribute App
- Select TestFlight
- Add affected user email
- In TestFlight, ask user to:
- Reproduce the crash scenario
- Check if memory stabilizes (logs to system.log)
- Report if crash still happens
在App Store发布前:
swift
#if DEBUG
// 添加到AppDelegate
import os.log
let logger = os.log(subsystem: "com.yourapp.memory", category: "lifecycle")
// 记录内存里程碑
func logMemory(_ event: String) {
let memoryUsage = ProcessInfo.processInfo.physicalMemory / 1_000_000
os.log("🔍 [%s] Memory: %dMB", log: logger, type: .info, event, memoryUsage)
}
#endif向受影响用户发送TestFlight版本:
- Build → Archive → Distribute App
- 选择TestFlight
- 添加受影响用户的邮箱
- 在TestFlight中,要求用户:
- 重现崩溃场景
- 检查内存是否稳定(日志记录到system.log)
- 报告崩溃是否仍发生
Step 4: Verify Fix Production Deployment
步骤4:验证修复的生产部署
After deploying fix:
- Monitor MetricKit metrics for 24-48 hours
- Check crash rate drop in App Analytics
- If still seeing high memory users:
- Add more diagnostic logging for next version
- Consider lower memory device testing (iPad with constrained memory)
部署修复后:
- 监控MetricKit指标24-48小时
- 检查App Analytics中的崩溃率下降情况
- 如果仍有高内存用户:
- 在下一版本中添加更多诊断日志
- 考虑在低内存设备上测试(内存受限的iPad)
Systematic Debugging Workflow
系统化调试流程
Phase 1: Confirm Leak (5 minutes)
阶段1:确认泄漏(5分钟)
1. Open app in simulator
2. Xcode → Product → Profile → Memory
3. Record baseline memory
4. Repeat action 10 times
5. Check memory graph:
- Flat line = NOT a leak (stop here)
- Steady climb = LEAK (go to Phase 2)1. 在模拟器中打开应用
2. Xcode → Product → Profile → Memory
3. 记录基准内存
4. 重复操作10次
5. 检查内存图表:
- 水平线 = 无泄漏(到此为止)
- 稳定上升 = 泄漏(进入阶段2)Phase 2: Locate Leak (10-15 minutes)
阶段2:定位泄漏(10-15分钟)
1. Close Instruments
2. Xcode → Debug → Memory Graph Debugger
3. Wait for graph (5-10 sec)
4. Look for purple/red circles with ⚠
5. Click on leaked object
6. Read the retain cycle chain:
PlayerViewModel (leak)
↑ retained by progressTimer
↑ retained by TimerClosure
↑ retained by [self] capture1. 关闭Instruments
2. Xcode → Debug → Memory Graph Debugger
3. 等待图表生成(5-10秒)
4. 查找带有⚠的紫色/红色圆圈
5. 点击泄漏的对象
6. 读取循环引用链:
PlayerViewModel (leak)
↑ retained by progressTimer
↑ retained by TimerClosure
↑ retained by [self] captureCommon leak locations (in order of likelihood)
常见泄漏位置(按可能性排序)
- Timers (50% of leaks)
- Notifications/KVO (25%)
- Closures in arrays/collections (15%)
- Delegate cycles (10%)
- 定时器(50%的泄漏)
- 通知/KVO(25%)
- 数组/集合中的闭包(15%)
- 委托循环(10%)
Phase 3: Test Hypothesis (5 minutes)
阶段3:验证假设(5分钟)
Apply fix from "Common Patterns" section above, then:
swift
// Add deinit logging
class PlayerViewModel: ObservableObject {
deinit {
print("✅ PlayerViewModel deallocated - leak fixed!")
}
}Run in Xcode, perform operation, check console for dealloc message.
应用"常见模式"部分的修复方案,然后:
swift
// 添加deinit日志
class PlayerViewModel: ObservableObject {
deinit {
print("✅ PlayerViewModel deallocated - leak fixed!")
}
}在Xcode中运行,执行操作,检查控制台中的释放消息。
Phase 4: Verify Fix with Instruments (5 minutes)
阶段4:使用Instruments验证修复(5分钟)
1. Product → Profile → Memory
2. Repeat action 10 times
3. Confirm: Memory stays flat (not climbing)
4. If climbing continues, go back to Phase 2 (second leak)1. Product → Profile → Memory
2. 重复操作10次
3. 确认:内存保持平稳(不上升)
4. 如果仍上升,返回阶段2(存在第二个泄漏)Compound Leaks (Multiple Sources)
复合泄漏(多个泄漏源)
Real apps often have 2-3 leaks stacking:
Leak 1: Timer in PlayerViewModel (+10MB/minute)
Leak 2: Observer in delegate (+5MB/minute)
Result: +15MB/minute → Crashes in 13 minutes实际应用通常存在2-3个泄漏叠加:
泄漏1:PlayerViewModel中的定时器(+10MB/分钟)
泄漏2:委托中的观察者(+5MB/分钟)
结果:+15MB/分钟 → 13分钟后崩溃How to find compound leaks
如何查找复合泄漏
1. Fix obvious leak (Timer)
2. Run Instruments again
3. If memory STILL growing, there's a second leak
4. Repeat Phase 1-3 for each leak
5. Test each fix in isolation (revert one, test another)1. 修复明显的泄漏(定时器)
2. 再次运行Instruments
3. 如果内存仍增长,说明存在第二个泄漏
4. 对每个泄漏重复阶段1-3
5. 单独测试每个修复(恢复一个,测试另一个)Memory Leak Detection — Testing Checklist
内存泄漏检测——测试清单
swift
// Pattern 1: Verify object deallocates
@Test func viewModelDeallocates() {
var vm: PlayerViewModel? = PlayerViewModel()
vm?.startPlayback(Track(id: "1", title: "Test"))
// Cleanup
vm?.stopPlayback()
vm = nil
// If no crash, object deallocated
}
// Pattern 2: Verify timer stops
@Test func timerStopsOnDeinit() {
var vm: PlayerViewModel? = PlayerViewModel()
let startCount = Timer.activeCount()
vm?.startPlayback(Track(id: "1", title: "Test"))
XCTAssertGreater(Timer.activeCount(), startCount)
vm?.stopPlayback()
vm = nil
XCTAssertEqual(Timer.activeCount(), startCount)
}
// Pattern 3: Verify observer unregistered
@Test func observerRemovedOnDeinit() {
var vc: DetailViewController? = DetailViewController()
let startCount = NotificationCenter.default.observers().count
// Perform action that adds observer
_ = vc
vc = nil
XCTAssertEqual(NotificationCenter.default.observers().count, startCount)
}
// Pattern 4: Memory stability over time
@Test func memoryStableAfterRepeatedActions() {
let vm = PlayerViewModel()
var measurements: [UInt] = []
for _ in 0..<10 {
vm.startPlayback(Track(id: "1", title: "Test"))
vm.stopPlayback()
let memory = ProcessInfo.processInfo.physicalMemory
measurements.append(memory)
}
// Check last 5 measurements are within 10% of each other
let last5 = Array(measurements.dropFirst(5))
let average = last5.reduce(0, +) / UInt(last5.count)
for measurement in last5 {
XCTAssertLessThan(
abs(Int(measurement) - Int(average)),
Int(average / 10) // 10% tolerance
)
}
}swift
// 模式1:验证对象被释放
@Test func viewModelDeallocates() {
var vm: PlayerViewModel? = PlayerViewModel()
vm?.startPlayback(Track(id: "1", title: "Test"))
// 清理
vm?.stopPlayback()
vm = nil
// 如果无崩溃,对象已被释放
}
// 模式2:验证定时器停止
@Test func timerStopsOnDeinit() {
var vm: PlayerViewModel? = PlayerViewModel()
let startCount = Timer.activeCount()
vm?.startPlayback(Track(id: "1", title: "Test"))
XCTAssertGreater(Timer.activeCount(), startCount)
vm?.stopPlayback()
vm = nil
XCTAssertEqual(Timer.activeCount(), startCount)
}
// 模式3:验证观察者已注销
@Test func observerRemovedOnDeinit() {
var vc: DetailViewController? = DetailViewController()
let startCount = NotificationCenter.default.observers().count
// 执行添加观察者的操作
_ = vc
vc = nil
XCTAssertEqual(NotificationCenter.default.observers().count, startCount)
}
// 模式4:内存随时间稳定
@Test func memoryStableAfterRepeatedActions() {
let vm = PlayerViewModel()
var measurements: [UInt] = []
for _ in 0..<10 {
vm.startPlayback(Track(id: "1", title: "Test"))
vm.stopPlayback()
let memory = ProcessInfo.processInfo.physicalMemory
measurements.append(memory)
}
// 检查最后5次测量值彼此相差在10%以内
let last5 = Array(measurements.dropFirst(5))
let average = last5.reduce(0, +) / UInt(last5.count)
for measurement in last5 {
XCTAssertLessThan(
abs(Int(measurement) - Int(average)),
Int(average / 10) // 10%容差
)
}
}Command Line Tools for Memory Debugging
命令行工具用于内存调试
bash
undefinedbash
undefinedMonitor memory in real-time
实时监控内存
Connect device, then
连接设备后
xcrun xctrace record --template "Memory" --output memory.trace
xcrun xctrace record --template "Memory" --output memory.trace
Analyze with command line
命令行分析
xcrun xctrace dump memory.trace
xcrun xctrace dump memory.trace
Check for leaked objects
检查泄漏对象
instruments -t "Leaks" -a YourApp -p 1234
instruments -t "Leaks" -a YourApp -p 1234
Memory pressure simulator
内存压力模拟器
xcrun simctl spawn booted launchctl list | grep memory
xcrun simctl spawn booted launchctl list | grep memory
Check malloc statistics
检查malloc统计信息
leaks -atExit -excludeNoise YourApp
undefinedleaks -atExit -excludeNoise YourApp
undefinedCommon Mistakes
常见错误
❌ Using [weak self] but never calling invalidate()
- Weak self prevents immediate crash but doesn't stop timer
- Timer keeps running and consuming CPU/battery
- ALWAYS call or
invalidate()on timers/subscriberscancel()
❌ Invalidating timer but keeping strong reference
swift
// ❌ Wrong
timer?.invalidate() // Stops firing but timer still referenced
// ❌ Should be:
timer?.invalidate()
timer = nil // Release the reference❌ Assuming AnyCancellable auto-cleanup is automatic
swift
// ❌ Wrong - if cancellable goes out of scope, subscription ends immediately
func setupListener() {
let cancellable = NotificationCenter.default
.publisher(for: .myNotification)
.sink { _ in }
// cancellable is local, goes out of scope immediately
// Subscription dies before any notifications arrive
}
// ✅ Right - store in property
@MainActor
class MyClass: ObservableObject {
private var cancellables = Set<AnyCancellable>()
func setupListener() {
NotificationCenter.default
.publisher(for: .myNotification)
.sink { _ in }
.store(in: &cancellables) // Stored as property
}
}❌ Not testing the fix
- Apply fix → Assume it's correct → Deploy
- ALWAYS run Instruments after fix to confirm memory flat
❌ Fixing the wrong leak first
- Multiple leaks = fix largest first (biggest memory impact)
- Use Memory Graph to identify what's actually leaking
❌ Adding deinit with only logging, no cleanup
swift
// ❌ Wrong - just logs, doesn't clean up
deinit {
print("ViewModel deallocating") // Doesn't stop timer!
}
// ✅ Right - actually stops the leak
deinit {
timer?.invalidate()
timer = nil
NotificationCenter.default.removeObserver(self)
}❌ Using Instruments Memory template instead of Leaks
- Memory template: Shows memory usage (not leaks)
- Leaks template: Detects actual leaks
- Use both: Memory for trend, Leaks for detection
❌ 使用[weak self]但从未调用invalidate()
- 弱引用防止立即崩溃,但不会停止定时器
- 定时器仍在运行,消耗CPU/电池
- 始终对定时器/订阅调用或
invalidate()cancel()
❌ 使定时器失效但保持强引用
swift
// ❌ 错误
timer?.invalidate() // 停止触发但仍持有引用
// ❌ 正确做法:
timer?.invalidate()
timer = nil // 释放引用❌ 假设AnyCancellable自动清理是自动的
swift
// ❌ 错误 - 如果cancellable超出作用域,订阅立即结束
func setupListener() {
let cancellable = NotificationCenter.default
.publisher(for: .myNotification)
.sink { _ in }
// cancellable是局部变量,立即超出作用域
// 订阅在收到任何通知前就结束
}
// ✅ 正确 - 存储为属性
@MainActor
class MyClass: ObservableObject {
private var cancellables = Set<AnyCancellable>()
func setupListener() {
NotificationCenter.default
.publisher(for: .myNotification)
.sink { _ in }
.store(in: &cancellables) // 存储为属性
}
}❌ 不测试修复效果
- 应用修复 → 假设正确 → 部署
- 修复后始终运行Instruments确认内存平稳
❌ 先修复错误的泄漏
- 多个泄漏 → 先修复最大的泄漏(对内存影响最大)
- 使用Memory Graph识别实际泄漏的对象
❌ 添加仅包含日志的deinit,不进行清理
swift
// ❌ 错误 - 仅记录,不清理
deinit {
print("ViewModel deallocating") // 不会停止定时器!
}
// ✅ 正确 - 实际停止泄漏
deinit {
timer?.invalidate()
timer = nil
NotificationCenter.default.removeObserver(self)
}❌ 使用Instruments Memory模板而非Leaks模板
- Memory模板:显示内存使用情况(不是泄漏)
- Leaks模板:检测实际泄漏
- 同时使用两者:Memory查看趋势,Leaks检测泄漏
Instruments Quick Reference
Instruments快速参考
| 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 | Find objects consuming most memory |
| Cache behavior | Allocations | Find objects allocated but not freed |
| 场景 | 工具 | 需要关注的内容 |
|---|---|---|
| 渐进式内存增长 | Memory | 线条稳定上升 = 泄漏 |
| 特定对象泄漏 | Memory Graph | 紫色/红色圆圈 = 泄漏对象 |
| 直接泄漏检测 | Leaks | 红色"! Leak"标记 = 确认泄漏 |
| 按类型划分内存 | VM Tracker | 查找占用内存最多的对象 |
| 缓存行为 | Allocations | 查找已分配但未释放的对象 |
Real-World Impact
实际影响
Before 50+ PlayerViewModel instances created/destroyed
- Each uncleared timer fires every second
- Memory: 50MB → 100MB (1min) → 200MB (2min) → Crash (13min)
- Developer spends 2+ hours debugging
After Timer properly invalidated in all view models
- One instance created/destroyed = memory flat
- No timer accumulation
- Memory: 50MB → 50MB → 50MB (stable for hours)
Key insight 90% of leaks come from forgetting to stop timers, observers, or subscriptions. Always clean up in or use reactive patterns that auto-cleanup.
deinit修复前 创建/销毁50+个PlayerViewModel实例
- 每个未清理的定时器每秒触发一次
- 内存:50MB → 100MB(1分钟)→ 200MB(2分钟)→ 崩溃(13分钟)
- 开发者花费2+小时调试
修复后 所有视图模型中的定时器都被正确失效
- 创建/销毁一个实例 = 内存平稳
- 无定时器累积
- 内存:50MB → 50MB → 50MB(稳定数小时)
关键见解 90%的泄漏源于忘记停止定时器、观察者或订阅。始终在中清理,或使用自动清理的响应式模式。
deinitSimulator Verification
模拟器验证
After fixing memory leaks, verify your app's UI still renders correctly and doesn't introduce visual regressions.
修复内存泄漏后,验证应用UI仍能正确渲染,且不会引入视觉回归。
Why Verify After Memory Fixes
为什么修复后需要验证
Memory fixes can sometimes break functionality:
- Premature cleanup — Object deallocated while still needed
- Broken bindings — Weak references become nil unexpectedly
- State loss — Data cleared too early in lifecycle
Always verify:
- UI still renders correctly
- No blank screens or missing content
- Animations still work
- App doesn't crash on navigation
内存修复有时会破坏功能:
- 过早清理 —— 对象在仍需要时被释放
- 绑定断裂 —— 弱引用意外变为nil
- 状态丢失 —— 数据在生命周期中被过早清除
始终验证:
- UI仍正确渲染
- 无空白屏幕或缺失内容
- 动画仍正常工作
- 应用导航时不崩溃
Quick Visual Verification
快速视觉验证
bash
undefinedbash
undefined1. Build with memory fix
1. 构建包含内存修复的版本
xcodebuild build -scheme YourScheme
xcodebuild build -scheme YourScheme
2. Launch in simulator
2. 在模拟器中启动
xcrun simctl launch booted com.your.bundleid
xcrun simctl launch booted com.your.bundleid
3. Navigate to affected screen
3. 导航到受影响的屏幕
xcrun simctl openurl booted "debug://problem-screen"
sleep 1
xcrun simctl openurl booted "debug://problem-screen"
sleep 1
4. Capture screenshot
4. 捕获截图
/axiom:screenshot
/axiom:screenshot
5. Verify UI looks correct (no blank views, missing images, etc.)
5. 验证UI正确(无空白视图、缺失图片等)
undefinedundefinedStress Testing with Screenshots
截图压力测试
Test the screen that was leaking, repeatedly:
bash
undefined反复测试曾经泄漏的屏幕:
bash
undefinedNavigate to screen multiple times, capture at each iteration
多次导航到屏幕,每次捕获截图
for i in {1..10}; do
xcrun simctl openurl booted "debug://player-screen?id=$i"
sleep 2
xcrun simctl io booted screenshot /tmp/stress-test-$i.png
done
for i in {1..10}; do
xcrun simctl openurl booted "debug://player-screen?id=$i"
sleep 2
xcrun simctl io booted screenshot /tmp/stress-test-$i.png
done
All screenshots should look correct (not degraded)
所有截图应显示正确(无退化)
undefinedundefinedFull Verification Workflow
完整验证流程
bash
/axiom:test-simulatorThen describe:
- "Navigate to PlayerView 10 times and verify UI doesn't degrade"
- "Open and close SettingsView repeatedly, screenshot each time"
- "Check console logs for deallocation messages"
bash
/axiom:test-simulator然后描述:
- "导航到PlayerView 10次,验证UI不会退化"
- "反复打开和关闭SettingsView,每次截图"
- "检查控制台日志中的释放消息"
Before/After Example
修复前后示例
Before fix (timer leak):
bash
undefined修复前(定时器泄漏):
bash
undefinedAfter navigating to PlayerView 20 times:
导航到PlayerView 20次后:
- Memory at 200MB
- 内存200MB
- UI sluggish
- UI卡顿
- Screenshot shows normal UI (but app will crash soon)
- 截图显示正常UI(但应用很快会崩溃)
**After fix** (timer cleanup added):
```bash
**修复后**(添加定时器清理):
```bashAfter navigating to PlayerView 20 times:
导航到PlayerView 20次后:
- Memory stable at 50MB
- 内存稳定在50MB
- UI responsive
- UI响应迅速
- Screenshot shows normal UI
- 截图显示正常UI
- Console logs show: "PlayerViewModel deinitialized" after each navigation
- 控制台日志显示每次导航后"PlayerViewModel deinitialized"
**Key verification**: Screenshots AND memory both stable = fix is correct
---
**关键验证**:截图和内存均稳定 = 修复正确
---Resources
资源
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
Last Updated: 2026-01-16
Frameworks: UIKit, SwiftUI, Combine, Foundation, MetricKit
Status: Production-ready patterns for leak detection, prevention, and jetsam handling
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
最后更新:2026-01-16
框架:UIKit, SwiftUI, Combine, Foundation, MetricKit
状态:可用于生产环境的泄漏检测、预防和Jetsam处理模式