2-17.【OC】【内存管理】 Block 的变量捕获规则是怎样的?

7 阅读3分钟

Objective-C Block 的变量捕获规则是其最核心的机制。Block 为了保证在未来某个时刻执行时变量依然可用,会根据变量的类型修饰符,采取截然不同的“打包”策略。

我们可以将其归纳为以下四大核心规则:


1. 基础局部变量:值捕获(Value Capture)

当你捕获一个普通局部变量(如 intfloat)时,Block 会在定义的那一刻,将该变量的值拷贝到自己的结构体中。

  • 特点:Block 内部持有的是原变量的一个“快照”。

  • 后果

    1. 外部修改不影响内部:在 Block 定义后修改原变量,Block 内部的值不变。
    2. 内部只读:默认情况下,你不能在 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)。

  • 内存重定向

    1. 当 Block 从栈拷贝到堆时,__block 变量也会被拷贝到堆上
    2. 结构体内部有一个 __forwarding 指针。无论你在栈上还是堆上访问该变量,最终都会通过 __forwarding 指向堆上的那份唯一数据。
  • 后果:Block 内部和外部现在共享同一个内存地址,可以自由读写。


4. 静态变量与全局变量:地址访问

这类变量(如 static int 或全局变量)不属于“捕获”范畴,因为它们的生命周期本就是贯穿整个程序运行的。

  • 静态变量 (static) :Block 结构体内部会捕获该变量的地址(指针) 。因为地址不变,所以 Block 内部可以修改它的值。
  • 全局变量:Block 直接访问,不进行任何形式的捕获。

总结:捕获规则判定表

变量类型捕获内容是否可修改存储位置
局部自动变量值 (Value)Block 内部成员变量
局部静态变量指针 (Address)Block 内部成员变量
__block 变量指向包装结构体的指针堆上的结构体
对象变量指针 (含所有权)是(修改内容) / 否(修改指针)随修饰符而定
全局变量不捕获全局数据区

💡 一个经典的面试坑:

问:为什么 Block 内部不能修改普通局部变量,却能修改静态变量?

答: 因为局部变量在函数结束后会销毁,Block 只能靠拷贝一份值(快照)存起来;而静态变量在内存中位置固定,Block 只需要存一个地址指针,随时顺着地址回去改就行了。