在 Objective-C 中,Block 捕获 self 的真实语义可以概括为一句话:它在 Block 的结构体内部创建了一个强引用指针,指向了 self 所代表的对象。
这里有三个核心层面的含义,直接决定了你会不会写出 Bug 或内存泄漏。
1. 结构体层面的“成员变量”
从底层内存布局来看,当你进入 Block 的代码块并使用了 self(无论是直接使用 self 还是访问成员变量 _property),编译器就会在 Block 的 Captured Variables 区域添加一个字段。
- 字段类型:
id __strong self; - 赋值时机: 在 Block 创建时,它会将当前的
self指针赋值给这个内部字段。 - 语义: 这是一次显式的强引用。Block 的引用计数增加不会影响
self,但self的引用计数会因为被 Block 持有而加 1。
2. “捕获 self”与“捕获成员变量”的等价性
这是一个最容易产生误解的语义。在 Block 里使用 _name 和使用 self.name 实际上都会捕获 self。
Objective-C
// 场景 A
self.block = ^{
NSLog(@"%@", _name); // 隐式捕获 self
};
// 场景 B
self.block = ^{
NSLog(@"%@", self.name); // 显式捕获 self
};
- 真实语义: 成员变量(Ivar)是存储在
self对象的内存空间里的。要访问_name,Block 必须先通过self指针找到对象的基地址,再加上偏移量。因此,访问成员变量等同于强引用整个self。
3. 内存闭环(循环引用)的成因
为什么捕获 self 经常导致 Retain Cycle?
self拥有(持有)一个block属性。block被创建并拷贝到堆上。- 根据捕获语义,
block内部结构体持有了self的强引用。 - 结果:
self→block→self。双方都在等对方释放,导致谁也无法销毁。
4. 解决语义:__weak 的介入
当我们使用 __weak typeof(self) weakSelf = self; 时,语义发生了变化:
- Block 的内部结构体字段变成了
id __weak weakSelf;。 - 真实语义: Block 不再增加
self的引用计数。它仅仅记录了self的地址。如果self被销毁,这个内部指针会自动置为nil。
总结:真实语义清单
- 强持有: 只要 Block 不销毁,
self就一定不会被销毁。 - 全对象捕获: 捕获
self的任何一部分(属性、变量、方法)都等同于捕获整个self对象。 - 延迟释放: 如果 Block 被放到了后台队列(如 GCD),它会延长
self的生命周期,直到任务执行完毕。