基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

__block 修饰符的作用是什么?它是如何实现修改外部变量的?

知识点图片

__block 修饰符是 Objective-C 中 Block 编程的一个核心概念。它的主要作用是允许 Block 内部修改外部定义的局部变量,并且处理变量在内存中的生命周期管理。

以下是关于 __block 的作用及其底层实现原理的详细解析。


一、 __block 的作用

1. 允许在 Block 内部修改变量

默认情况下,Block 捕获外部局部变量是值拷贝(Capture by Value)。这意味着 Block 内部持有的是变量被捕获时的“快照”,是只读的。如果你尝试在 Block 内部修改一个没有 __block 修饰的局部变量,编译器会报错。

加上 __block 后,Block 捕获的是该变量的引用(地址),从而允许修改。

代码示例:

plaintext
// ❌ 错误示范
int a = 10;
void (^block)(void) = ^{
    a = 20; // 编译报错:Variable is not assignable (missing __block type specifier)
};

// ✅ 正确示范
__block int b = 10;
void (^block)(void) = ^{
    b = 20; // 正常修改
};
block();
NSLog(@"%d", b); // 输出 20

2. 延长变量的生命周期

当 Block 从栈(Stack)拷贝到堆(Heap)时(例如 Block 被赋值给属性或作为返回值),它捕获的 __block 变量也会被移动到堆上。这样即使原来的作用域结束了(栈帧销毁),Block 依然可以安全地访问和修改这个变量。


二、 底层实现原理:它是如何修改外部变量的?

__block 的魔法在于:编译器会将 __block 修饰的变量包装成一个对象(结构体)。

1. 结构体的转换

当编译器看到 __block int val = 10; 时,它不仅仅是在栈上分配一个 int,而是生成了一个结构体(struct)。

大致的结构体定义如下(简化版):

plaintext
struct __Block_byref_val_0 {
    void *__isa;                   //以此表明它是一个对象
    struct __Block_byref_val_0 *__forwarding; // 指向自身的指针(关键!)
    int __flags;
    int __size;
    int val;                       // 变量的真实值
};

2. __forwarding 指针(核心机制)

这个 __forwarding 指针是实现“修改外部变量”以及“栈堆同步”的关键。

  • 初始状态(在栈上):
    __block 变量刚创建在栈上时,结构体里的 __forwarding 指针指向它自己(栈上的这个结构体地址)。

  • Block 拷贝到堆上(Copy):
    当 Block 被拷贝到堆上时,它持有的 __block 变量结构体也会被拷贝一份到堆上。
    此时发生了一件非常重要的事情:栈上那个结构体的 __forwarding 指针,会被修改指向堆上的那个新结构体。 而堆上的结构体的 __forwarding 指向它自己。

  • 统一访问入口:
    无论是在 Block 内部,还是在 Block 外部,代码中对该变量的读写,编译器都会转换成通过 __forwarding 指针来访问。

    转换后的访问逻辑大致如下:

    plaintext
    // 访问 val
    (valStruct->__forwarding->val) = 20;

3. 为什么这样就能修改了?

通过 __forwarding 指针,保证了操作的一致性:

  1. 如果 Block 在栈上: __forwarding 指向栈上的结构体,修改的是栈上的值。
  2. 如果 Block 移到了堆上:
    • Block 内部持有的是堆上的结构体,通过 __forwarding 修改堆上的值。
    • Block 外部(原来的栈作用域)持有的虽然是栈上的结构体,但因为栈上结构体的 __forwarding 已经指向了堆,所以外部修改时,实际上也是顺着指针修改了堆上的那个值。

结论: 无论你在哪里修改,最终修改的都是堆上那份唯一的内存数据,从而实现了变量的共享和修改。


三、 内存管理细节 (ARC 环境)

  1. 对象类型的 __block
    在 ARC 下,如果 __block 修饰的是一个对象(如 __block NSObject *obj),Block 会对该对象进行强引用(Retain)

    • 注意: 这与 MRC 时代不同(MRC 下 __block 不会 retain 对象,常用来避免循环引用)。在 ARC 下,要避免循环引用,通常需要配合 __weak 使用,即 __weak 打破循环,__block 负责修改(虽然现在很少为了修改对象指针本身而用 __block,更多是修改基本数据类型)。
  2. 循环引用风险:
    如果你在 Block 内部使用了 __block 修饰的 self,在 ARC 下会导致循环引用(Self -> Block -> __block变量 -> Self)。

    • 解决方案: 使用 __weak
    plaintext
    __weak typeof(self) weakSelf = self;
    self.block = ^{
        __strong typeof(weakSelf) strongSelf = weakSelf;
        [strongSelf doSomething];
    };

总结

  1. 作用: __block 允许 Block 内部修改外部变量,并负责将变量从栈拷贝到堆,延长其生命周期。
  2. 原理:
    • 将变量包装成一个 __Block_byref 结构体
    • 利用结构体内部的 __forwarding 指针
    • 当 Block 发生 Copy 时,栈上的 __forwarding 指向堆上的结构体,确保无论从哪里访问,操作的都是同一块内存地址。
00:00
00:00