mcpField
A
axiom-objc-block-retain-cycles

axiom-objc-block-retain-cycles

github.com/charleswiltgen开发1.2k169 次安装今天更新
代码仓库

关于 axiom-objc-block-retain-cycles

面向 Objective-C 开发者的技能指南,用于诊断和修复 block 引用循环导致的内存问题。内容按诊断、模式选择和验证步骤组织,涵盖弱引用、嵌套 block 与隐藏的 self 引用。

主要功能

  1. 01诊断 block 引用循环
  2. 02应用 weak-strong 模式
  3. 03排查宏中的隐藏引用
  4. 04逐层处理嵌套 block
  5. 05验证对象释放情况

适用场景

  1. 01视图控制器关闭后仍未释放
  2. 02异步回调导致内存持续增长
  3. 03NSLog 或 NSAssert 中存在 self 引用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-objc-block-retain-cycles

安装后在 Claude Code 中输入/axiom-objc-block-retain-cycles确认已加载。

详细介绍

Objective-C Block 保留循环

概述

Block 保留循环是 Objective-C 内存泄漏的头号原因。当一个 block 捕获 self,并存储在同一个对象上(直接存储,或通过 operation/request 间接存储)时,就会形成循环引用:self → block → self。核心原则 90% 的 block 内存泄漏都源于遗漏或错误使用 weak-strong 模式,而非 Apple 框架本身的缺陷。

危险信号——怀疑存在 Block 保留循环

如果看到以下任意情况,请怀疑是 block 保留循环,而不是其他问题:

  • 应用正常使用时,内存持续增长
  • UIViewController 实例未释放(已通过 Instruments 确认)
  • 崩溃:网络/异步回调中出现“Sending message to deallocated instance”
  • 网络请求或动画导致视图控制器无法关闭
  • block 中的弱引用意外变为 nil
  • NSLog、NSAssert 或字符串格式化隐藏了对 self 的引用
  • 视图控制器“本该消失”后,完成处理程序仍被调用
  • ❌ 禁止 将其解释为“可能只是正常的内存使用”
    • 内存泄漏绝不“正常”
    • 用户关闭某个界面后,应用的内存应恢复到基线
    • 不要以“还可以”或“之后再监控”为由自我安慰

关键区别 Block 保留循环会悄无声息地累积。单个循环可能占用 100KB,但浏览 50 个界面后,就会有 5MB 无用内存。必须:修复后在真实设备(支持范围内最旧的机型)上测试,不能只在模拟器上测试。

必须执行的首要步骤

始终先运行以下步骤(再修改代码):

// 1. Identify the leak with Allocations instrument
// In Xcode: Xcode > Open Developer Tool > Instruments
// Choose Allocations template
// Perform an action (open/close a screen with the suspected block)
// Check if memory doesn't return to baseline
// Record: "Memory baseline: X MB, after action: Y MB, still allocated: Z objects"

// 2. Use Memory Debugger to trace the cycle
// Run app, pause at suspected code location
// Debug > Debug Memory Graph
// Search for the view controller that should be deallocated
// Right-click > Show memory graph
// Look for arrows pointing back to self (the cycle)
// Record: "ViewController retained by: [operation/block/property]"

// 3. Check if block is assigned to self or self's properties
// Search for: setBlock:, completion:, handler:, callback:
// Check: Is the block stored in self.property?
// Check: Is the block passed to something that retains it (network operation)?
// Record: "Block assigned to: [property or operation]"

// 4. Search for self references in the block
// Look for: [self method], self.property, self-> access
// Look for HIDDEN self references:
//   - NSLog(@"Value: %@", self.property)
//   - NSAssert(self.isValid, @"message")
//   - Format strings: @"Name: %@", self.name
// Record: "self references found in block: [list]"

// Example output:
// Memory not returning to baseline ✓
// ViewController retained by: AFHTTPRequestOperation
// Operation retains: successBlock
// Block references self: [self updateUI], NSLog with self.property
// → DIAGNOSIS: Block retain cycle confirmed
这些检查结果说明什么
  • 内存保持在高位 → 已确认存在泄漏,并非误报
  • ViewController 被 operation 保留 → 问题出在 block
  • Block 引用了 self → 需要使用 weak-strong 模式
  • NSLog/NSAssert 中隐藏了 self → 需要检查所有宏调用
  • 未发现 self 引用 → 可能不是 block 循环,应从其他方面排查
必须遵循的判定

修改任何代码之前,必须确认以下情况之一:

  1. 如果内存未恢复到基线,且 ViewController 仍处于分配状态 → 存在 Block 保留循环
  2. 如果内存恢复到基线 → 不是保留循环,应排查其他原因
  3. 如果存在循环,但找不到 self 引用 → 检查隐藏引用(宏、间接属性访问)
  4. 如果找到了循环,但不理解引用链 → 在 Memory Graph 中沿着被保留的对象反向追踪
如果诊断结果相互矛盾或不明确
  • 停止。此时不要继续尝试修复模式
  • 添加更多诊断:打印对象图,列出被保留的对象
  • 思考:“如果内存占用很低,为什么 ViewController 仍处于分配状态?”
  • 如果内存图难以判断,请运行 Instruments > Leaks 工具

决策树

Block memory leak suspected?
├─ Memory stays high after dismiss?
│  ├─ YES
│  │  ├─ ViewController still allocated in Memory Graph?
│  │  │  ├─ YES → Proceed to patterns
│  │  │  └─ NO → Not a block cycle, check other leaks
│  │  └─ NO → Not a leak, normal memory usage
│  │
│  └─ Crash: "Sending message to deallocated instance"?
│     ├─ Happens in block/callback?
│     │  ├─ YES → Block captured weakSelf but it became nil
│     │  │  └─ Apply Pattern 4 (Guard condition is wrong or missing)
│     │  └─ NO → Different crash, not block-related
│     └─ Crash is timing-dependent (only on device)?
│        └─ YES → Weak reference timing issue, apply Pattern 2
│
├─ Block assigned to self or self.property?
│  ├─ YES → Apply Pattern 1 (weak-strong mandatory)
│  ├─ Assigned through network operation/timer/animation?
│  │  └─ YES → Apply Pattern 1 (operation retains block indirectly)
│  └─ Block called immediately (inline execution)?
│     ├─ YES → Optional to use weak-strong (no cycle possible)
│     │  └─ But recommend for consistency with other blocks
│     └─ NO → Block stored or passed to async method → Use Pattern 1
│
├─ Multiple nested blocks?
│  └─ YES → Apply Pattern 3 (must guard ALL nested blocks)
│
├─ Block contains NSAssert, NSLog, or string format with self?
│  └─ YES → Apply Pattern 2 (macro hides self reference)
│
└─ Implemented weak-strong pattern but still leaking?
   ├─ Check: Is weakSelf used EVERYWHERE?
   ├─ Check: No direct `self` references mixed in?
   ├─ Check: Nested blocks also guarded?
   └─ Check: No __unsafe_unretained used?

常见模式

模式选择规则(必须遵守)
一次只应用一种模式,并按以下顺序进行
  1. 始终从模式 1 开始(Weak-Strong 基础)

    • 如果 block 被赋给 self 或 self 的属性 → 模式 1
    • 如果 block 被传递给会保留它的 operation/request → 模式 1
    • 只有在模式 1 之后仍然泄漏,才能继续尝试模式 2
  2. 然后尝试模式 2(宏中隐藏的 self)

    • 只有应用模式 1 后内存仍然泄漏,才尝试此模式
    • 检查 NSAssert、NSLog、字符串格式化
    • 如果发现问题,应用模式 2
  3. 然后尝试模式 3(嵌套 Block)

    • 只有 block 存在嵌套回调时,才尝试此模式
    • 每个嵌套 block 都需要各自的保护
    • 如果发现问题,应用模式 3
  4. 然后尝试模式 4(保护条件边界情况)

    • 只有使用 weakSelf 方案时发生崩溃,才尝试此模式
    • 检查保护条件是否正确
    • 确认所有地方都使用了 strongSelf
禁止事项
  • ❌ 一次应用多种模式
  • ❌ 因为“我已经懂 weak-strong 了”而跳过模式 1
  • ❌ 使用 __unsafe_unretained 作为变通方法
  • ❌ “就这一次”使用强引用 self
  • ❌ 自我安慰:“这个 block 太小,不会造成泄漏”

模式 1:弱-强模式(必须使用)

原则 任何会捕获 self 的代码块,只要会被 self(直接或间接)保留,就必须使用弱-强模式。

❌ 错误(会创建保留环)
[self.networkManager GET:@"url" success:^(id response) {
    self.data = response;  // self is retained by block
    [self updateUI];       // block is retained by operation
} failure:^(NSError *error) {
    [self handleError:error];  // CYCLE!
}];
✅ 正确(打破保留环)
__weak typeof(self) weakSelf = self;
[self.networkManager GET:@"url" success:^(id response) {
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        strongSelf.data = response;
        [strongSelf updateUI];
    }
} failure:^(NSError *error) {
    __weak typeof(self) weakSelf2 = self;
    typeof(self) strongSelf = weakSelf2;
    if (strongSelf) {
        [strongSelf handleError:error];
    }
}];
为什么这样有效
  1. __weak typeof(self) weakSelf = self; 在代码块外创建弱引用
  2. 代码块捕获 weakSelf(弱引用),而不是 self(强引用)
  3. 执行代码块时,将 weakSelf 转换为 strongSelf(临时强引用)
  4. 检查 strongSelf 是否为 nil(对象已被释放)
  5. 在代码块执行期间使用 strongSelf
  6. 代码块退出时释放 strongSelf → 不会形成保留环
重要细节
  • 在代码块外声明 weakSelf,而不是在代码块内
  • 使用 typeof(self) 确保类型安全(在 ARC 和非 ARC 下都适用)
  • 必须使用 if (strongSelf) 作为保护条件,不能只声明变量
  • 声明 weakSelf 后,绝不要在代码块内直接使用 self
  • 对每个捕获 self 的代码块都应用此模式
  • 任何捕获 self 的代码块都必须使用弱-强模式
    • 包括:[self method]、self.property、self->ivar
    • 属性访问(self.property = value)和方法调用一样会捕获 self
  • 传递给框架的代码块:
    • 如果框架文档说明“代码块会异步调用” → 使用弱-强模式(框架会保留代码块)
    • 如果框架文档说明“代码块会立即调用” → 使用弱-强模式仍然安全(也是更好的做法)
    • 如果不确定框架的行为 → 始终使用弱-强模式(没有坏处)
捕获变量(避免间接引用 self)
// ✅ SAFE: Capture simple values extracted from self
__weak typeof(self) weakSelf = self;
[self.manager fetch:^(id response) {
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        NSString *name = strongSelf.name;  // Extract value
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"Name: %@", name);  // Captured the STRING, not self
        });
    }
}];

// ❌ WRONG: Capture properties directly in nested blocks
__weak typeof(self) weakSelf = self;
[self.manager fetch:^(id response) {
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"Name: %@", strongSelf.name);  // Captures strongSelf again!
        });
    }
}];

嵌套代码块时,先提取简单值,再将其传递给内部代码块。这样可以避免通过属性访问间接捕获 self。

耗时 每个代码块 30 秒


模式 2:宏中隐藏的 self

原则 NSAssert、NSLog 和字符串格式化等宏可能会暗中捕获 self。必须检查它们。

❌ 错误(NSAssert 会捕获 self)
[self.button setTapAction:^{
    NSAssert(self.isValidState, @"State must be valid");  // self captured!
    [self doWork];  // Another self reference
}];
// Leak exists even though you think only [self doWork] captures self
✅ 正确(检查隐藏捕获)
__weak typeof(self) weakSelf = self;
[self.button setTapAction:^{
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        // NSAssert still references self indirectly through strongSelf
        NSAssert(strongSelf.isValidState, @"State must be valid");
        [strongSelf doWork];
    }
}];
常见的隐藏 self 引用
  • NSAssert(self.condition, ...) → 改用 strongSelf
  • NSLog(@"Value: %@", self.property) → 改用 strongSelf.property
  • NSError *error = [NSError errorWithDomain:@"MyApp" ...] → 安全,不会捕获 self
  • 字符串格式化:@"Name: %@", self.name → 改用 strongSelf.name
  • 内联条件表达式:self.flag ? @"yes" : @"no" → 改用 strongSelf.flag
如何查找
  1. 搜索代码块中的所有 self.
  2. 标记这些引用:[self method]、self.property、self->ivar
  3. 检查其中是否有引用位于宏调用中(NSAssert、NSLog 等)
  4. 替换为 strongSelf

耗时 每个代码块审核 1 分钟


模式 3:嵌套代码块(每个都需要保护)

原则 嵌套代码块会形成一条引用链:外层代码块捕获 self,内层代码块捕获外层代码块变量(其中持有 strongSelf),从而形成新的保留环。每个嵌套代码块都需要各自的弱-强模式。

❌ 错误(仅保护外层代码块)
__weak typeof(self) weakSelf = self;
[self.manager fetchData:^(NSArray *result) {
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        // Inner block captures strongSelf!
        [strongSelf.analytics trackEvent:@"Fetched"
                              completion:^{
            strongSelf.cachedData = result;  // Still strong reference!
            [strongSelf updateUI];
        }];
    }
}];
✅ 正确(保护每个嵌套代码块)
__weak typeof(self) weakSelf = self;
[self.manager fetchData:^(NSArray *result) {
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        // Declare new weak reference for inner block
        __weak typeof(strongSelf) weakSelf2 = strongSelf;

        [strongSelf.analytics trackEvent:@"Fetched"
                              completion:^{
            typeof(strongSelf) strongSelf2 = weakSelf2;
            if (strongSelf2) {
                strongSelf2.cachedData = result;
                [strongSelf2 updateUI];
            }
        }];
    }
}];
为什么这样有效
  • 每个嵌套层级都需要自己的一对 weakSelf/strongSelf
  • 外层代码块:weakSelf → strongSelf
  • 内层代码块:weakSelf2 → strongSelf2
  • 每个层级相互独立且安全
重要细节
  • 不要在嵌套代码块中重复使用同一个 weakSelf 变量
  • 每个嵌套层级都要使用一对新变量(weakSelf2、strongSelf2)
  • 每个层级都必须使用保护条件
  • 使用一致的命名方式:weakSelf、weakSelf2、weakSelf3(便于阅读)
需要使用模式 3 的常见嵌套代码块
  • 回调中的完成处理程序
  • dispatch_async(queue, ^{ ... })
  • dispatch_after(time, queue, ^{ ... })
  • [NSTimer scheduledTimerWithTimeInterval:... block:^{ ... }]
  • [UIView animateWithDuration:... animations:^{ ... }]

以上每种情况都包含一个可能捕获 strongSelf 的代码块,因此都需要各自使用弱-强模式。

使用 dispatch_async 的示例
__weak typeof(self) weakSelf = self;
[self.manager fetchData:^(NSArray *result) {
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        __weak typeof(strongSelf) weakSelf2 = strongSelf;

        dispatch_async(dispatch_get_main_queue(), ^{
            typeof(strongSelf) strongSelf2 = weakSelf2;
            if (strongSelf2) {
                strongSelf2.data = result;
                [strongSelf2 updateUI];
            }
        });
    }
}];

耗时 每个嵌套层级 1 分钟


模式 4:守卫条件边界情况

原则 守卫条件 if (strongSelf) 必须正确。常见错误:忘记守卫、条件错误,或混用 self 和 strongSelf。

❌ 错误(多个守卫错误)
__weak typeof(self) weakSelf = self;
[self.button setTapAction:^{
    typeof(self) strongSelf = weakSelf;
    // MISTAKE 1: Forgot guard condition
    self.counter++;  // CRASH! self is deallocated, accessing freed object

    // MISTAKE 2: Guard exists but used wrong variable
    if (weakSelf) {
        [weakSelf doWork];  // weakSelf is weak, might become nil again
    }

    // MISTAKE 3: Mixed self and strongSelf
    if (strongSelf) {
        self.flag = YES;  // Used self instead of strongSelf!
        [strongSelf doWork];
    }
}];
✅ 正确(守卫正确且用法一致)
__weak typeof(self) weakSelf = self;
[self.button setTapAction:^{
    typeof(self) strongSelf = weakSelf;
    if (strongSelf) {
        // CORRECT: Use strongSelf everywhere, never self
        strongSelf.counter++;
        strongSelf.flag = YES;
        [strongSelf doWork];
    }
    // If strongSelf is nil, entire block skips gracefully
}];
为什么这样有效
  1. if (strongSelf) 检查对象是否仍然存在
  2. 如果存在,strongSelf 是强引用(安全)
  3. 如果不存在(对象已释放),则跳过代码块
  4. 始终使用 strongSelf 可避免意外引用 self
关键规则(必须遵守,不得例外)
  • ✅ 使用前始终检查 if (strongSelf)
  • ✅ if 代码块内始终使用 strongSelf,绝不直接使用 self
  • ✅ 在整个代码块作用域内,strongSelf 都保证有效
  • ❌ 绝不要使用 if (!strongSelf) return;(会混淆逻辑)
  • ❌ 绝不要为了“省代码”而跳过守卫
  • ❌ 绝不要混用 weakSelf 和 strongSelf
  • ❌ 绝不要在没有守卫的情况下使用 strongSelf(必然崩溃)
出错后会发生什么
  • 没有守卫:崩溃,并显示“向已释放的实例发送消息”
  • 条件错误:对象仍已释放,依然会崩溃
  • 混用 self/strongSelf:一次意外使用 self 就会破坏整个模式
  • 未经守卫就使用 strongSelf:对象释放时必然崩溃
守卫内部
if (strongSelf) {
    strongSelf.data1 = value1;
    [strongSelf doWork1];
    [strongSelf doWork2];  // All safe
}
// ❌ WRONG: Using strongSelf after guard ends
strongSelf.data = value2;  // CRASH! Outside guard
不要这样做
// ❌ FORBIDDEN: strongSelf without guard guarantees crash
typeof(self) strongSelf = weakSelf;
strongSelf.data = value;  // CRASH if weakSelf is nil!

// ✅ MANDATORY: Always guard before using strongSelf
if (strongSelf) {
    strongSelf.data = value;  // Safe
}

耗时 每个代码块花 10 秒验证守卫是否正确


快速参考表

问题检查项修复方法
内存未恢复到基线ViewController 在内存图中是否仍然存在?应用模式 1(弱-强引用)
崩溃:“向已释放的实例发送消息”是否缺少守卫条件或条件错误?应用模式 4(正确的守卫)
已应用弱-强引用,但仍然泄漏所有 self 引用是否都使用 strongSelf?检查是否混用了 self/strongSelf
代码块包含 NSAssert 或 NSLog它们是否引用了 self?应用模式 2(在宏中使用 strongSelf)
嵌套代码块是否对每一层都应用了弱-强引用?应用模式 3(为每个代码块添加守卫)
不确定代码块是否会形成循环引用代码块是否赋值给 self 或 self.property?如果是,则应用模式 1

30 分钟后仍无头绪时

如果你已花费超过 30 分钟,泄漏仍然存在:

停下来。可能是以下情况
  1. 跳过了必需的诊断步骤(最常见)
  2. 没有对所有代码块应用弱-强引用(遗漏了嵌套代码块)
  3. 存在隐藏的 self 引用(NSAssert、NSLog、字符串格式化)
  4. 应用了模式,但混入了直接的 self 引用
  5. 存在其他类型的泄漏(与代码块无关)
声称“技能不起作用”前必须完成的检查清单
  • 我运行了全部 4 个诊断步骤(Allocations、Memory Graph、代码块搜索、self 引用搜索)
  • 我在 Instruments 中确认内存没有恢复到基线
  • 我确认 ViewController 仍已分配(尚未释放)
  • 我追踪了保留链(是什么持有 ViewController?)
  • 我找到了所有捕获 self 的代码块(在文件中全局搜索:[self)
  • 我检查了隐藏的 self 引用(NSAssert、NSLog、字符串格式化)
  • 我对外层代码块应用了弱-强引用模式
  • 我对嵌套代码块应用了弱-强引用模式(每个嵌套层级)
  • 我确认不再有任何直接的 self 引用(只有 strongSelf)
  • 我再次运行 Instruments,并确认内存恢复到基线
  • 我在真实设备上进行了测试,而不仅仅是在模拟器上
  • 我在各次运行之间清除了 Xcode 派生数据
如果所有项都已勾选但仍然泄漏
  • 这是非代码块导致的泄漏(Core Data、计时器、代理、通知)
  • 使用 Instruments > Leaks 工具查找实际的循环引用
  • 分析 2-3 分钟:打开屏幕、关闭屏幕,重复 5 次
  • 查看“Leaks”面板——它会准确显示哪些内容没有释放
  • 耗时:识别真正原因需要 15-30 分钟
如果你确认这不是块导致的泄漏
  • 不要自我安慰:“也许块没问题,照样发布吧。”
  • 找出实际的循环(可能来自 delegate、timer、属性观察器或通知)
  • 修复真正的问题,而不是误报
时间成本说明
  • 模式 1:每个块 30 秒
  • 模式 2:每个块 1 分钟(检查是否存在隐藏的 self)
  • 模式 3:每层嵌套 1 分钟
  • 如果卡住,需要进行嵌套诊断:15-30 分钟
  • 简单泄漏的总耗时:5-10 分钟

常见错误

❌ 忘记守卫条件

  • 使用 strongSelf.property = value;,却没有 if (strongSelf)
  • 对象释放后会发生崩溃
  • 修复方法:始终使用 if (strongSelf) { ... }

❌ 在同一个块中混用 self 和 strongSelf

  • self.flag = YES; [strongSelf doWork];
  • 直接引用一次 self 就会破坏整个模式
  • 修复方法:块内只能使用 strongSelf

❌ 只对外层块应用模式

  • 嵌套块仍会强引用 strongSelf
  • 依然会泄漏
  • 修复方法:对每个块应用弱引用-强引用模式

❌ 把 __unsafe_unretained 当作“变通方法”

  • ❌ 禁止使用此模式——不安全且会导致崩溃
  • 对象释放时会发生崩溃
  • 这不是解决方案,反而会让问题更严重
  • 修复方法:改用弱引用-强引用模式

❌ 未检查隐藏的 self 引用

  • 在块中使用 NSLog(@"Value: %@", self.property)
  • 即使应用了弱引用-强引用模式,泄漏仍然存在
  • 修复方法:检查 NSAssert、NSLog 和字符串格式化

❌ 自我安慰“这只是小泄漏”

  • 单个块泄漏可能达到 100KB
  • 经过 50 个屏幕后,累积到 5MB
  • 最终应用会因内存压力而崩溃
  • 修复方法:修复每个块泄漏,不要自我安慰

❌ 以为系统框架中的块是安全的

  • UIView 动画、AFNetworking、dispatch、计时器
  • 都可能保留引用 self 的块
  • 修复方法:无论来源如何,都应用弱引用-强引用模式

❌ 只在模拟器中测试

  • 模拟器的内存压力与真实设备不同
  • 负载下在真实设备上才可能出现泄漏
  • 修复方法:在真实设备和支持的最旧机型上测试

实际影响

之前 每个问题的块内存泄漏调试需要 2-3 小时

  • 运行 Allocations,却不知道该看什么
  • 到处搜索,没有明确的诊断路径
  • 随机尝试各种修复,寄希望于其中一个有效
  • 因为沉没成本谬误而照样发布
  • 客户报告崩溃或运行变慢

之后 通过系统化诊断,只需 5-10 分钟

  • 运行 Allocations,确认内存没有恢复到基线
  • Memory Graph 会准确显示哪些内容被保留
  • 通过全局搜索找出所有捕获 self 的块
  • 应用弱引用-强引用模式(每个块 30 秒)
  • 在 Instruments 中测试,确认内存恢复到基线
  • 完成

关键要点 使用弱引用-强引用模式可以 100% 避免块的引用循环。没有例外,也不存在强引用 self 可以接受的“特殊情况”。


最后更新:2025-11-30 状态:已通过压力场景 TDD 测试 框架:Objective-C、块(闭包)、ARC

评论

支持 Markdown