2-16.【OC】【内存管理】Block 的三种存储类型分别是什么?

1 阅读2分钟

在 Objective-C 中,Block 本质上是一个封装了函数及其执行上下文(变量捕获)的 OC 对象。根据其内存存储位置的不同,Block 分为三种主要的存储类型。

你可以通过查看 Block 对象的 isa 指针指向来判定它的类型。


1. _NSConcreteStackBlock(栈 Block)

这是 Block 的“原始形态”。在开启 ARC 之后,虽然我们很难直接观察到它(因为它经常被自动拷贝到堆上),但在底层逻辑中它依然存在。

  • 存储位置:函数执行的栈内存中。
  • 特征:只在定义它的函数作用域内有效。一旦函数返回,栈帧销毁,该 Block 也会被立即回收。
  • 触发条件:Block 访问了外部变量(捕获了变量),但还没有被执行 copy 操作。

风险:在 MRC 时代,直接返回一个栈 Block 是非常危险的,因为函数返回后,外部访问该 Block 会直接导致野指针崩溃。


2. _NSConcreteGlobalBlock(全局 Block)

这是最省心的一种类型,它的行为类似于普通的全局函数。

  • 存储位置:程序的数据区(Data Segment / .data)

  • 特征:生命周期贯穿整个程序运行期间,不会被销毁。

  • 触发条件

    • Block 内部没有访问任何外部变量(包括局部变量、self 等)。
    • 或者是定义在函数体之外的全局 Block。

3. _NSConcreteMallocBlock(堆 Block)

这是我们在开发中最常打交道的类型。在 ARC 下,编译器会为了保证安全,频繁地将栈 Block “搬”到堆上。

  • 存储位置堆内存(Heap)

  • 特征:引用计数管理。只有当它的引用计数降为 0 时,才会被销毁。它可以在不同的作用域之间传递。

  • 触发条件:对一个栈 Block 执行了 copy 操作。

    • 在 ARC 下,以下情况会自动执行 copy:

      1. 调用 strong 属性指向 Block。
      2. 将 Block 作为函数返回值返回。
      3. 将 Block 传给 Cocoa 框架中带有 usingBlock 的方法(如 GCD API 或集合遍历)。

三种类型对比汇总

类型存储位置变量捕获情况生命周期
全局 Block数据区不捕获任何外部变量永久存在
栈 Block栈区捕获了变量,未被拷贝随函数作用域结束
堆 Block堆区捕获了变量,且执行了 copy随引用计数清零

为什么我们要关心这个?

了解这个分类能帮你解开很多 “为什么”

  1. 为什么 Block 属性用 copy 为了确保如果是栈 Block,能被正确搬到堆上,防止作用域结束后失效。
  2. 为什么 Block 能捕获 __block 变量? 当 Block 从栈拷贝到堆时,它会顺带着把引用的 __block 变量也拷贝到堆上,从而实现跨作用域的变量修改。