5-3.【OC】【Block】Stack Block 为什么不能跨作用域使用?它的生命周期是如何结束的?

2 阅读2分钟

简单来说,Stack Block(栈上的 Block) 最大的局限性在于它的生命周期是与它所在的函数或作用域(Scope) 强绑定的。

在 Objective-C 中,Block 本质上是一个对象,但它有三种不同的内存类型。理解 Stack Block 的局限性,关键在于理解它在内存中的位置。


1. 为什么不能跨作用域使用?

当一个 Block 捕获了外部局部变量(即 __block 之外的局部变量)时,如果它没有被显式地执行 copy 操作,它通常就是一个 Stack Block (_NSConcreteStackBlock)

  • 存储位置: 它存储在程序的栈区(Stack)
  • 作用域依赖: 栈内存是由系统自动管理的。一旦该 Block 所在的函数执行完毕,或者所在的代码块 { ... } 运行结束,该作用域对应的栈帧(Stack Frame)就会被弹出(Pop)并销毁
  • 后果: 如果你在作用域外部继续持有这个 Block 的指针并尝试调用它,你实际上是在访问一块已经被释放或可能被其他函数重新占用的内存(野指针),这会导致程序崩溃或不可预知的行为。

2. 它的生命周期是如何结束的?

Stack Block 的生命周期遵循栈的 “后进先出” (LIFO) 原则。

  1. 分配: 当代码运行到 Block 定义的位置时,系统在当前的栈帧中划出一块空间存放 Block 的结构体和捕获的变量。
  2. 存活: 只要程序计数器(PC)还在该作用域的括号 {} 内,这块内存就是有效的。
  3. 销毁: 当函数返回(return)或作用域结束时,该栈帧被视为无效。系统并不需要像堆内存那样执行 dealloc,而是直接通过移动栈指针来忽略这块区域。 此时,原本存储 Block 的数据可能瞬间就被后续调用的其他函数覆盖。

3. 如何跨作用域使用?

为了解决这个问题,我们需要将 Block 从“栈”搬到“堆(Heap)”。

  • 手动管理 (MRC): 调用 [block copy]。这会在堆上创建一个副本,你必须在不需要时手动 release

  • 自动管理 (ARC): 在大多数情况下,ARC 会自动帮你完成 copy 操作。例如:

    • 将 Block 作为函数的返回值时。
    • 将 Block 赋值给 __strong 修饰的变量或属性时。
    • 作为 Cocoa Touch 框架中某些特定的 API 参数时。

注意: 在 ARC 时代,我们很少直接看到 _NSConcreteStackBlock 了,因为编译器非常聪明,经常会提前帮你把它升格为 _NSConcreteMallocBlock(堆 Block)。


总结对照

特性Stack BlockMalloc Block
位置栈 (Stack)堆 (Heap)
生命周期随作用域结束自动销毁由引用计数管理 (ARC/copy)
跨作用域❌ 安全风险极高✅ 安全,可长期持有