axiom-memory-debugging

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Memory 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
undefined

1. 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次,查看内存是否持续增长

undefined
undefined

What 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 chain
1. 在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 leak
1. 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  5

Step 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 somewhere
1. 添加上述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与内存超限的区别

TerminationCauseSolution
Memory Limit ExceededYour app used too much memory (foreground OR background)Reduce peak memory footprint
JetsamSystem needed memory for other appsReduce 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:
DeviceApprox. Memory LimitSafe Target
iPhone 6s~200MB150MB
iPhone X~400MB300MB
iPhone 12~500MB400MB
iPhone 14 Pro~600MB500MB
iPad (varies)~300-800MBCheck device
Note: Limits are NOT documented by Apple and vary by iOS version. Test on oldest supported device.
内存限制因设备而异。旧设备的限制更严格:
设备大致内存限制安全目标
iPhone 6s~200MB150MB
iPhone X~400MB300MB
iPhone 12~500MB400MB
iPhone 14 Pro~600MB500MB
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终止后需要恢复的状态

StatePriorityExample
Navigation positionHighCurrent tab, navigation stack
User input (drafts)HighText field content, unsent messages
Media playback positionHighVideo/audio timestamp
Scroll positionMediumTable/collection scroll offset
Search queryMediumActive search text
Filter selectionsLowSort 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
undefined
bash
undefined

In Simulator

在模拟器中

Debug > Simulate Memory Warning

Debug > Simulate Memory Warning

On device (Instruments)

在设备上(使用Instruments)

Use Memory template, trigger warnings manually

使用Memory模板,手动触发警告

undefined
undefined

Jetsam 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 Timer
ViewController → 强持有 ViewModel
ViewModel → 强持有 Timer
Timer → 强持有 闭包
Closure → 捕获[weak self]但仍持有Timer的引用

Closure captures
self
weakly BUT

闭包捕获
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

修复方案的工作原理

  • invalidate()
    : Stops timer immediately, breaks retain cycle
  • cancellable
    : Automatically invalidates when released
  • [weak self]
    : If ViewModel released before timer, timer becomes no-op
  • deinit cleanup
    : Ensures timer always cleaned up
  • invalidate()
    :立即停止定时器,打破循环引用
  • cancellable
    :被释放时自动失效
  • [weak self]
    :如果ViewModel在定时器前被释放,定时器变为空操作
  • 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
PHImageManager.requestImage()
returns a
PHImageRequestID
that must be explicitly cancelled. Without cancellation, pending requests queue up and hold memory.
  • 滚动长照片列表时内存骤增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
    PHImageRequestID
    in cell (not in view controller)
  • Cancel BEFORE starting new request (prevents request storms)
  • Cancel in
    prepareForReuse()
    (critical for collection views)
  • Check
    imageRequestID != PHInvalidImageRequestID
    before cancelling
  • 在单元格中存储
    PHImageRequestID
    (而非视图控制器)
  • 开始新请求前取消(防止请求风暴)
  • prepareForReuse()
    中取消(对集合视图至关重要)
  • 取消前检查
    imageRequestID != PHInvalidImageRequestID

Other async APIs with similar patterns

其他具有类似模式的异步API

  • AVAssetImageGenerator.generateCGImagesAsynchronously()
    → call
    cancelAllCGImageGeneration()
  • URLSession.dataTask()
    → call
    cancel()
    on task
  • Custom image caches → implement
    invalidate()
    or
    cancel()
    method
  • 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:
  1. iPhone → Settings → Privacy & Security → Analytics → Analytics Data
  2. Look for latest crash log (named like
    YourApp_2024-01-15-12-45-23
    )
  3. Email or upload to your support system
  4. Xcode → Window → Devices & Simulators → select device → View Device Logs
  5. Search for "Memory" or "Jetsam" in logs
当用户报告崩溃时:
  1. iPhone → 设置 → 隐私与安全性 → 分析与改进 → 分析数据
  2. 查找最新的崩溃日志(名称类似
    YourApp_2024-01-15-12-45-23
  3. 通过电子邮件或上传至支持系统
  4. Xcode → Window → Devices & Simulators → 选择设备 → View Device Logs
  5. 在日志中搜索"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)
}
#endif
Send TestFlight build to affected users:
  1. Build → Archive → Distribute App
  2. Select TestFlight
  3. Add affected user email
  4. 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版本:
  1. Build → Archive → Distribute App
  2. 选择TestFlight
  3. 添加受影响用户的邮箱
  4. 在TestFlight中,要求用户:
    • 重现崩溃场景
    • 检查内存是否稳定(日志记录到system.log)
    • 报告崩溃是否仍发生

Step 4: Verify Fix Production Deployment

步骤4:验证修复的生产部署

After deploying fix:
  1. Monitor MetricKit metrics for 24-48 hours
  2. Check crash rate drop in App Analytics
  3. If still seeing high memory users:
    • Add more diagnostic logging for next version
    • Consider lower memory device testing (iPad with constrained memory)
部署修复后:
  1. 监控MetricKit指标24-48小时
  2. 检查App Analytics中的崩溃率下降情况
  3. 如果仍有高内存用户:
    • 在下一版本中添加更多诊断日志
    • 考虑在低内存设备上测试(内存受限的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] capture
1. 关闭Instruments
2. Xcode → Debug → Memory Graph Debugger
3. 等待图表生成(5-10秒)
4. 查找带有⚠的紫色/红色圆圈
5. 点击泄漏的对象
6. 读取循环引用链:
   PlayerViewModel (leak)
     ↑ retained by progressTimer
       ↑ retained by TimerClosure
         ↑ retained by [self] capture

Common 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
undefined
bash
undefined

Monitor 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
undefined
leaks -atExit -excludeNoise YourApp
undefined

Common 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
    invalidate()
    or
    cancel()
    on timers/subscribers
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快速参考

ScenarioToolWhat to Look For
Progressive memory growthMemoryLine steadily climbing = leak
Specific object leakingMemory GraphPurple/red circles = leak objects
Direct leak detectionLeaksRed "! Leak" badge = confirmed leak
Memory by typeVM TrackerFind objects consuming most memory
Cache behaviorAllocationsFind 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
deinit
or use reactive patterns that auto-cleanup.

修复前 创建/销毁50+个PlayerViewModel实例
  • 每个未清理的定时器每秒触发一次
  • 内存:50MB → 100MB(1分钟)→ 200MB(2分钟)→ 崩溃(13分钟)
  • 开发者花费2+小时调试
修复后 所有视图模型中的定时器都被正确失效
  • 创建/销毁一个实例 = 内存平稳
  • 无定时器累积
  • 内存:50MB → 50MB → 50MB(稳定数小时)
关键见解 90%的泄漏源于忘记停止定时器、观察者或订阅。始终在
deinit
中清理,或使用自动清理的响应式模式。

Simulator 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
undefined
bash
undefined

1. 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正确(无空白视图、缺失图片等)

undefined
undefined

Stress Testing with Screenshots

截图压力测试

Test the screen that was leaking, repeatedly:
bash
undefined
反复测试曾经泄漏的屏幕:
bash
undefined

Navigate 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)

所有截图应显示正确(无退化)

undefined
undefined

Full Verification Workflow

完整验证流程

bash
/axiom:test-simulator
Then 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
undefined

After 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

**修复后**(添加定时器清理):
```bash

After 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处理模式