Objective-C Block 的变量捕获规则是其最核心的机制。Block 为了保证在未来某个时刻执行时变量依然可用,会根据变量的类型和修饰符,采取截然不同的“打包”策略。
我们可以将其归纳为以下四大核心规则:
1. 基础局部变量:值捕获(Value Capture)
当你捕获一个普通局部变量(如 int、float)时,Block 会在定义的那一刻,将该变量的值拷贝到自己的结构体中。
-
特点:Block 内部持有的是原变量的一个“快照”。
-
后果:
- 外部修改不影响内部:在 Block 定义后修改原变量,Block 内部的值不变。
- 内部只读:默认情况下,你不能在 Block 内部修改这个变量(编译器会报错),因为你修改的只是快照,这在逻辑上容易引起混淆。
2. 对象类型变量:所有权捕获(Reference Capture)
当捕获一个对象指针(如 NSString *、NSObject *)时,Block 结构体会持有一个指向该对象的指针。
-
ARC 下的行为:Block 会根据外部指针的修饰符(
__strong或__weak)来决定如何持有这个对象。- 如果是
__strong,Block 会对该对象执行retain,确保 Block 存在时对象不被销毁。 - 如果是
__weak,Block 只是记录地址,不增加引用计数。
- 如果是
-
后果:这是导致 循环引用(Retain Cycle) 的根源。如果
self强持有 Block,而 Block 内部又直接使用了self,双方就会互相持有,永不释放。
3. __block 修饰的变量:引用捕获(By-Ref Capture)
如果你希望在 Block 内部修改外部变量,或者希望 Block 内部能感知到外部变量的后续变化,必须使用 __block。
-
底层变化:编译器会将该变量包装成一个 结构体对象(
__Block_byref_xxx_0)。 -
内存重定向:
- 当 Block 从栈拷贝到堆时,
__block变量也会被拷贝到堆上。 - 结构体内部有一个
__forwarding指针。无论你在栈上还是堆上访问该变量,最终都会通过__forwarding指向堆上的那份唯一数据。
- 当 Block 从栈拷贝到堆时,
-
后果:Block 内部和外部现在共享同一个内存地址,可以自由读写。
4. 静态变量与全局变量:地址访问
这类变量(如 static int 或全局变量)不属于“捕获”范畴,因为它们的生命周期本就是贯穿整个程序运行的。
- 静态变量 (
static) :Block 结构体内部会捕获该变量的地址(指针) 。因为地址不变,所以 Block 内部可以修改它的值。 - 全局变量:Block 直接访问,不进行任何形式的捕获。
总结:捕获规则判定表
| 变量类型 | 捕获内容 | 是否可修改 | 存储位置 |
|---|---|---|---|
| 局部自动变量 | 值 (Value) | 否 | Block 内部成员变量 |
| 局部静态变量 | 指针 (Address) | 是 | Block 内部成员变量 |
| __block 变量 | 指向包装结构体的指针 | 是 | 堆上的结构体 |
| 对象变量 | 指针 (含所有权) | 是(修改内容) / 否(修改指针) | 随修饰符而定 |
| 全局变量 | 不捕获 | 是 | 全局数据区 |
💡 一个经典的面试坑:
问:为什么 Block 内部不能修改普通局部变量,却能修改静态变量?
答: 因为局部变量在函数结束后会销毁,Block 只能靠拷贝一份值(快照)存起来;而静态变量在内存中位置固定,Block 只需要存一个地址指针,随时顺着地址回去改就行了。