iOS Block 知识点

3 阅读12分钟

🧱 Block的底层本质

面试题:block的本质是什么?它的内部结构是怎样的?

Block 本质上是一个封装了函数调用及其执行环境(捕获的变量)的 OC 对象。它内部也有一个 isa 指针

通过 clang -rewrite-objc 命令将 OC 代码编译为 C++ 后,可以看到 Block 的底层结构如下

c

// Block 的基础结构
struct __block_impl {
    void *isa;          // 指向 Class 对象,表明 Block 的类型
    int Flags;          // 标志位,包含引用计数等信息
    int Reserved;       // 保留字段
    void *FuncPtr;      // 函数指针,指向 Block 内部代码的执行函数
};

// Block 的最终结构 (以 main 函数中的 block 为例)
struct __main_block_impl_0 {
    struct __block_impl impl;          // 上述基础结构
    struct __main_block_desc_0* Desc;  // 描述信息,如 Block 大小
    // 捕获的变量会在这里展开...
    int age; // 示例:捕获的 int 类型变量
};

其核心关系可以用下图表示:

要点

  • isa 指针证明了 Block 的对象本质
  • FuncPtr 指向的函数存放着我们在 {} 中编写的代码
  • 捕获的变量会成为结构体中的成员变量

🏠 Block 的三种类型与内存管理

面试题:Block 有哪几种类型?分别在什么情况下使用?它们存在哪里?

根据 isa 指针和存储位置的不同,Block 分为三种

Block 类型存储域典型场景是否需手动管理
__NSGlobalBlock数据段Block 没有捕获任何自动变量(如局部变量)时。无需。整个程序生命周期有效。
__NSStackBlock栈区在 MRC 下,Block 捕获了自动变量。栈区内存由系统管理,函数返回即可能销毁危险!  需立即 copy 到堆上使用。
__NSMallocBlock堆区对 __NSStackBlock 执行了 copy 操作后产生。ARC 下编译器会自动进行此操作需手动管理引用计数(MRC),或依赖 ARC。

核心要点

  • ARC 下的自动 copy:在 ARC 中,当 Block 被赋值给一个 strong 或 copy 修饰的变量,或者作为方法/函数的返回值时,编译器会自动将其从栈复制到堆上,保证其安全性
  • 为什么属性要用 copy  这是 MRC 时代留下的最佳实践。在 ARC 下,strong 和 copy 对 Block 的作用一样(编译器会优化),但使用 copy 能更清晰地表明意图,并与 MRC 代码保持兼容

🔗 变量捕获机制与 __block

面试题:block 内部为什么不能修改普通局部变量的值?__block 的作用是什么?

  • 值捕获:对于普通局部变量(auto变量) ,Block 在内部只会捕获其在定义瞬间的(一份拷贝)。因此,之后在外部修改变量,不影响 Block 内部已捕获的值;在 Block 内部修改,也会导致编译错误
  • 指针捕获:对于静态局部变量(static)  和全局变量,Block 会捕获其指针,因此可以在 Block 内部修改外部变量的值
  • __block 的本质__block 的作用是让一个栈上的自动变量也能在 Block 内部被修改。其原理是将该变量包装成一个结构体对象(如 __Block_byref_age_0),这个结构体包含了变量的原始值以及指向它自己的指针。当 Block 从栈复制到堆时,这个 __block 变量也会被一同复制到堆上,从而实现 Block 内外对该变量的共享与修改

思考题:在 block 中修改 NSMutableArray,需要添加 __block 吗?
不需要。  修改 NSMutableArray 的内容(如 addObject:)是使用了数组变量,而不是修改数组变量本身(即指针指向)。Block 捕获的是数组变量的指针(值拷贝),通过这个指针去操作数组内容是允许的。只有当你试图修改数组指针本身(如 array = otherArray)时,才需要 __block

🚨 循环引用 (Retain Cycle)

面试题:什么是循环引用?为什么 block 容易导致循环引用?如何解决?

  • 成因:当对象(如 self)持有了一个 Block 属性(强引用) ,而这个 Block 内部又强引用了该对象(如直接使用 self 时,就会形成循环引用,导致内存无法释放

  • 解决方案

    1. __weak + __strong 舞蹈

      objectivec

      __weak typeof(self) weakSelf = self;
      self.block = ^{
          __strong typeof(self) strongSelf = weakSelf;
          if (strongSelf) {
              // 在这里安全地使用 strongSelf
              strongSelf.property = ...;
          }
      };
      

      __weak 打破了循环引用,而 Block 内部的 __strong 确保了在执行期间,即使 self 在其他地方被释放,也能在这个 Block 的作用域内保持存活,避免崩溃

    2. 使用 __block 并在执行后手动置 nil(MRC 常见,ARC 下不推荐,需确保 Block 被执行)。

    3. 使用 NSProxy 等更高级的方法(用于复杂场景)。

✨ ARC 与 MRC 下 Block 的内存差异

面试题:ARC 和 MRC 下,Block 的内存管理有哪些关键区别?

  • 默认存储位置

    • MRC:捕获了自动变量的 Block 默认是 __NSStackBlock,存储在栈上。
    • ARC:捕获了自动变量的 Block 默认会被编译器自动 copy 到堆上,成为 __NSMallocBlock
  • 对对象类型变量的影响

    • MRC:Block 捕获对象类型的变量时,不会自动管理其引用计数,需要开发者手动处理。
    • ARC:Block 捕获对象类型的变量时,会自动根据所有权修饰符(__strong__weak 等)管理引用计数。当 Block 从栈复制到堆时,它会 retain 所捕获的对象;当堆上的 Block 被释放时,它会 release 这些对象

1. 底层本质:__block 变量是如何包装的?

当使用 __block 修饰一个对象类型的变量时,编译器会将这个变量转换为一个  __Block_byref_xxx 结构体的实例。这个结构体不仅保存了原始变量的值(即对象指针),还包含了管理其内存所需的元数据和函数指针。

以 __block id obj = self; 为例,通过 clang -rewrite-objc 可以看到类似如下的结构:

c

// 生成的 __block 结构体
struct __Block_byref_obj_0 {
    void *__isa;                     // 保留,通常为0
    __Block_byref_obj_0 *__forwarding; // 指向自身(栈上时指向自己,堆上时指向堆上的拷贝)
    int __flags;                      // 标志位
    int __size;                        // 结构体大小
    void (*__Block_byref_id_object_copy)(void*, void*); // 拷贝辅助函数
    void (*__Block_byref_id_object_dispose)(void*);     // 释放辅助函数
    id obj;                            // 真正的对象指针,被 __strong 持有(默认)
};

关键点:

  • __forwarding 指针:确保无论在栈上还是堆上,都能访问到正确的变量。当 Block 从栈复制到堆时,__forwarding 会指向堆上的拷贝,保证修改的一致性。
  • __Block_byref_id_object_copy 和 __Block_byref_id_object_dispose:这两个函数是专门为对象类型生成的,用于在 Block 复制和销毁时,对捕获的对象进行内存管理(retain/release 或对应操作)。

2. ARC 下 __block 对象的内存管理流程

2.1 默认强引用(__strong)

在 ARC 下, __block 修饰的对象默认会被强引用。这意味着当 Block 从栈复制到堆时,__block 变量结构体也会被复制到堆,并且它所持有的对象会被 retain 一次;当堆上的 Block 被销毁时,该对象会被 release

具体过程:

  • Block 从栈复制到堆(例如将 Block 赋值给一个 strong 属性,或作为返回值):

    • 调用 _Block_copy 函数。
    • _Block_copy 会递归处理 Block 捕获的所有对象类型变量,包括 __block 变量。
    • 对于 __block 对象,会调用 __Block_byref_obj_0 中的 __Block_byref_id_object_copy 函数。这个函数内部最终调用 _Block_object_assign,它会根据变量的修饰符(这里是默认的 __strong)对对象执行 retain 操作。
    • 同时,__forwarding 指针被更新,指向堆上的 __block 结构体。
  • 堆上的 Block 被释放(引用计数变为0):

    • 调用 _Block_release
    • 对于 __block 对象,会调用 __Block_byref_id_object_dispose 函数,其内部通过 _Block_object_dispose 对对象执行 release 操作。

因此,在 ARC 下,__block 对象的行为类似于一个自动管理的强引用,无需开发者手动干预其基本内存管理。

2.2 与其他修饰符组合

__block 也可以与 __weak__unsafe_unretained 等修饰符组合使用,以改变对对象的持有方式。

  • __block __weak id weakObj = self;

    • 此时生成的 __Block_byref 结构体中的 obj 成员是 __weak 类型的。
    • 当 Block 复制到堆时,_Block_object_assign 会对 weakObj 进行弱引用处理(不会 retain),只记录一个指向对象的弱指针。
    • 当 Block 销毁时,_Block_object_dispose 也不会 release(因为本来就是弱引用)。
    • 注意:在 Block 执行期间,如果对象被释放,weakObj 会变成 nil,需要像常规 __weak 那样在内部转换为 __strong 使用,以防止中途被释放。
  • __block __unsafe_unretained id unretainedObj = self;

    • 结构体中的 obj 是 __unsafe_unretained 类型,不进行任何 retain/release,仅作指针赋值。
    • 这类似于 MRC 的行为,需确保对象在 Block 执行期间一定存活,否则会造成野指针崩溃。

3. 循环引用的陷阱与解决

尽管 ARC 自动管理了 __block 对象的引用计数,但  __block 本身仍然可能导致循环引用,因为:

  • 对象(如 self)持有了 Block(如通过 strong 属性)。
  • Block 捕获了 __block 变量,而该变量又强引用了 self(默认 __strong)。
  • __block 结构体被 Block 持有,__block 结构体中的对象指针又指向 self,形成闭环。

示例

objectivec

self.block = ^{
    __block id blockSelf = self; // 默认强引用 self
    dispatch_async(..., ^{
        // 使用 blockSelf
    });
};

这里,self → block → __block 结构体 → self,循环引用无法释放。

解决办法:

  1. 使用 __weak 打破循环

    objectivec

    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(self) strongSelf = weakSelf;
        if (strongSelf) {
            // 安全使用
        }
    };
    

    这是最推荐的方式,利用 __weak 避免强引用,内部再用 __strong 保证执行期间对象存活。

  2. 手动置 nil(需确保 Block 一定会被执行):

    objectivec

    __block id blockSelf = self;
    self.block = ^{
        // 使用 blockSelf
        blockSelf = nil; // 执行完毕后断开引用
    };
    self.block(); // 必须调用,否则循环依旧
    

    这种方法依赖 Block 执行并手动置 nil,不够安全,仅适用于明确调用 Block 的场景。

  3. 利用 __block + __weak 的组合

    objectivec

    __block __weak id weakSelf = self;
    self.block = ^{
        id strongSelf = weakSelf;
        if (strongSelf) {
            // ...
        }
    };
    

    此时 __block 变量持有的是弱引用,不会造成循环。但要注意,weakSelf 在 Block 外部也可能被置 nil,不过只要在 Block 内部转换一次强引用即可。


4. ARC 与 MRC 的关键差异

特性MRCARC
__block 对象的内存管理不自动 retain/release,需开发者手动管理(如避免提前释放)。默认强引用__strong),自动 retain/release,行为与普通 __strong 变量类似。
循环引用风险同样存在,但需手动置 nil 或使用 __block 后置 nil。同样存在,推荐使用 __weak 打破循环。
__forwarding 的作用保证堆栈同步。相同,但 ARC 下自动管理对象,__forwarding 依然重要。

在 MRC 中,__block 对象不会自动 retain,所以如果 Block 从栈复制到堆后,原栈上对象可能被销毁,需确保对象存活。而在 ARC 中,复制时自动 retain,保证了对象在 Block 生命周期内一定存活,除非存在循环引用。


5. 示例代码深入分析

objectivec

// ARC 环境
- (void)testBlock {
    NSObject *obj = [NSObject new]; // 局部变量,默认 __strong
    __block NSObject *blockObj = obj; // __block 包装,强引用 obj

    void (^block)(void) = ^{
        NSLog(@"%@", blockObj); // blockObj 指向 obj
    };

    block(); // 此时 obj 仍被 blockObj 强引用,不会释放
}

内存过程

  1. obj 是局部强引用,指向新对象。

  2. blockObj 是 __block 变量,其底层结构体中的 obj 成员是 __strong 类型,所以 blockObj 也强引用同一对象。此时对象引用计数为2(obj 和 blockObj)。

  3. Block 捕获 blockObj(注意 Block 捕获的是 __block 变量本身的结构体指针,而不是直接捕获对象)。Block 从栈复制到堆时,会复制 __block 结构体到堆,并且结构体中的 obj 再次被 retain?实际上,复制过程:

    • Block 复制:_Block_copy 复制 Block 到堆。
    • 对于 __block 变量,调用其 copy 辅助函数,该函数内部调用 _Block_object_assign
    • _Block_object_assign 会根据 __block 变量的修饰符处理对象。由于 __block 变量是默认 __strong,它会 retain 对象一次。所以对象引用计数增加到3(obj 局部强引用 + 栈上 __block 结构体中的强引用 + 堆上 __block 结构体中的强引用?注意这里有点特殊:__block 变量复制时,实际上是将整个结构体复制到堆,堆上的结构体中的对象指针指向同一对象,同时 retain 一次。原来的栈上结构体中的对象指针仍然指向对象,但由于栈上的 __block 结构体即将被销毁(Block 复制完成后,栈上 Block 和 __block 结构体仍存在,但通常我们只持有堆上的 Block),所以最终对象引用计数取决于堆上 __block 结构体的持有。
    • 更准确的简化理解:__block 变量被 Block 捕获后,其生命周期与持有它的 Block 相关,对象被 __block 结构体强引用,直到该结构体被释放。
  4. 当 testBlock 执行完毕,局部变量 obj 被释放,对象引用计数减1。

  5. 如果 block 还被其他地方持有(如赋值给属性),则对象仍存活;如果 block 被释放(例如是局部 Block 未传出),则 Block 释放时会释放 __block 结构体,进而 release 对象,对象最终销毁。


总结

  • ARC 下 __block 对对象的内存管理是自动的,默认 __strong,开发者无需手动 retain/release。
  • __block 变量本身是一个结构体,通过 __forwarding 保证堆栈一致性,通过内置的 copy/dispose 函数管理对象。
  • 循环引用依然可能发生,因为 __block 默认强引用对象。解决方法是使用 __weak 修饰 __block 变量,或者在 Block 内部手动置 nil(不推荐)。
  • 理解 __block 底层有助于解释一些奇怪的内存问题,例如为什么用 __block 修饰对象后,在 Block 外部将变量置为 nil 并不会影响 Block 内部的引用(因为内部持有的是结构体中的对象指针,而非外部变量本身)。

💡 总结与建议

准备 Block 相关的面试时,建议你:

  1. 动手编译:用 clang -rewrite-objc 命令亲自查看不同场景下 Block 转换后的 C++ 代码,这是理解底层最直接的方式。
  2. 多问为什么:思考每种修饰符(copy__weak__block)背后设计的根本原因,而不仅仅是死记硬背。
  3. 关注内存:Block 的考察重点几乎总是围绕着内存管理(类型、生命周期、循环引用)  展开。