关于 axiom-objc-block-retain-cycles
面向 Objective-C 开发者的技能指南,用于诊断和修复 block 引用循环导致的内存问题。内容按诊断、模式选择和验证步骤组织,涵盖弱引用、嵌套 block 与隐藏的 self 引用。
主要功能
- 01诊断 block 引用循环
- 02应用 weak-strong 模式
- 03排查宏中的隐藏引用
- 04逐层处理嵌套 block
- 05验证对象释放情况
适用场景
- 01视图控制器关闭后仍未释放
- 02异步回调导致内存持续增长
- 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 循环,应从其他方面排查
必须遵循的判定
修改任何代码之前,必须确认以下情况之一:
- 如果内存未恢复到基线,且 ViewController 仍处于分配状态 → 存在 Block 保留循环
- 如果内存恢复到基线 → 不是保留循环,应排查其他原因
- 如果存在循环,但找不到 self 引用 → 检查隐藏引用(宏、间接属性访问)
- 如果找到了循环,但不理解引用链 → 在 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 开始(Weak-Strong 基础)
- 如果 block 被赋给 self 或 self 的属性 → 模式 1
- 如果 block 被传递给会保留它的 operation/request → 模式 1
- 只有在模式 1 之后仍然泄漏,才能继续尝试模式 2
-
然后尝试模式 2(宏中隐藏的 self)
- 只有应用模式 1 后内存仍然泄漏,才尝试此模式
- 检查 NSAssert、NSLog、字符串格式化
- 如果发现问题,应用模式 2
-
然后尝试模式 3(嵌套 Block)
- 只有 block 存在嵌套回调时,才尝试此模式
- 每个嵌套 block 都需要各自的保护
- 如果发现问题,应用模式 3
-
然后尝试模式 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];
}
}];
为什么这样有效
__weak typeof(self) weakSelf = self;在代码块外创建弱引用- 代码块捕获 weakSelf(弱引用),而不是 self(强引用)
- 执行代码块时,将 weakSelf 转换为 strongSelf(临时强引用)
- 检查 strongSelf 是否为 nil(对象已被释放)
- 在代码块执行期间使用 strongSelf
- 代码块退出时释放 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, ...)→ 改用 strongSelfNSLog(@"Value: %@", self.property)→ 改用 strongSelf.propertyNSError *error = [NSError errorWithDomain:@"MyApp" ...]→ 安全,不会捕获 self- 字符串格式化:
@"Name: %@", self.name→ 改用 strongSelf.name - 内联条件表达式:
self.flag ? @"yes" : @"no"→ 改用 strongSelf.flag
如何查找
- 搜索代码块中的所有
self. - 标记这些引用:
[self method]、self.property、self->ivar - 检查其中是否有引用位于宏调用中(NSAssert、NSLog 等)
- 替换为 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
}];
为什么这样有效
if (strongSelf)检查对象是否仍然存在- 如果存在,strongSelf 是强引用(安全)
- 如果不存在(对象已释放),则跳过代码块
- 始终使用 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 分钟,泄漏仍然存在:
停下来。可能是以下情况
- 跳过了必需的诊断步骤(最常见)
- 没有对所有代码块应用弱-强引用(遗漏了嵌套代码块)
- 存在隐藏的 self 引用(NSAssert、NSLog、字符串格式化)
- 应用了模式,但混入了直接的
self引用 - 存在其他类型的泄漏(与代码块无关)
声称“技能不起作用”前必须完成的检查清单
- 我运行了全部 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