Block中修改局部变量的值为什么必须声明为__block类型

更新记录

时间 版本修改
2020年4月12日 初稿
2020年5月7日 纠正错误:其实在使用__block变量的时候,实际的源代码变得复杂更多。考虑到篇幅和结构问题,本文后续只采用了Block捕获静态局部变量的例子,来查看Block捕获静态局部变量的实现。
2020年5月8日 使用小标题序号,提升可读性。添加了关于char指针重新赋值的细节描述。

1. 前言

最近在重新且仔细地阅读《Objective-C 高级编程 iOS与OS X多线程和内存管理》,在阅读到 2.2 Blocks模式 这章时,看到Block中截获自动变量,对其进行重新赋值,会报“缺失__block修饰符”的编译错误。这引起了我的一些思考,在此叙述一下我的思考。

2. 思考

2.1 举书上的一个例子

2.1.1 block中使用该对象
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
  id obj = [[NSObject alloc] init];  
  [array addObject:obj];
};
  • 上述代码是没有任何问题的
2.1.2 block中对对象进行重新赋值
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
    array = [[NSMutableArray alloc] init];
};
  • 编译报错:Variable is not assignable (missing__block type specifier)
  • 网上很多参考资料上都说,给该变量加上__block修饰符就可以解决问题了。但是都没有谈到这个问题的深入之处

2.2 Block捕获变量代码示例说明

2.2.1 block不修改局部变量
  • block的使用代码:
int main(int argc, const char * argv[]) {
    int val = 10;
    const char *fmt = "val = %d
";
    void (^blk)(void) = ^{
        printf(fmt,val);
    };
    val = 2;
    fmt = "These value were changed. val = %d
";
    blk();
    return 0;
}
  • 输出结果为: val = 10
  • 转换之后的代码及对应的运行结果,很好理解:
    • 捕获了val这个局部变量,用以输出(Blocks的实质可参考我之前写的Blocks的实质学习总结
    • 也符合日常学习的认知:block捕获的非__block局部变量不受外部的改变
    • char* 类型的指针,再重新赋值时,指针变量会重新指向一片新的内存。而原来指针变量指向的内存并不受任何影响,仍然保持之前的值。所以该代码的输出结果是"val = %d",而不是"These value were changed. val = %d"。
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {//最终的函数指针调用
  const char *fmt = __cself->fmt; // bound by copy
  int val = __cself->val; // bound by copy
  printf(fmt,val);
}

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  const char *fmt;
  int val;              //block捕获的变量 val
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0) : fmt(_fmt), val(_val) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

int main(int argc, const char * argv[]) {
    int dmy = 256;
    int val = 10;
    const char *fmt = "val = %d
";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, fmt, val));//结构体带着参数val初始化并赋值
    val = 2;
    fmt = "These value were changed. val = %d
";
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);//函数指针调用
    return 0;
}
2.2.2 block捕获静态局部变量并修改
  • block的使用代码
int main(int argc, char * argv[]) {
    static int val = 10;
    const char *fmt = "val = %d
";
    void (^blk)(void) = ^{
        ++val;
        printf(fmt,val);
    };
    val = 2;
    fmt = "These value were changed. val = %d
";
    blk();
    return 0;
}
  • 运行结果:val = 3
  • 转换之后,代码和之前大致一样,但是有唯一的、细微的差别。
    • block用结构体__main_block_impl_0捕获的是val变量的地址(传地址,而非传值)
  • 就是这个细微的差别,可以做到使后续修改了变量val的值,block调用时也使用了更新之后的值,这是因为记录了val变量的地址(即静态存储区中),用地址访问当然是获取到最新的值。
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int *val;   //block捕获的变量 val,注意,这里捕获的是指针!!!
  const char *fmt;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_val, const char *_fmt, int flags=0) : val(_val), fmt(_fmt) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { //最终的函数指针调用
  int *val = __cself->val; // bound by copy
  const char *fmt = __cself->fmt; // bound by copy
  //这样就可以实现,在block中改变静态局部变量的值,是使用指针访问的
  ++(*val);
  printf(fmt,(*val));
}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
    static int val = 10;
    const char *fmt = "val = %d
";
    void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &val, fmt));  //结构体传递参数为val变量的地址!!!
    val = 2;
    fmt = "These value were changed. val = %d
";
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);  //函数指针调用
    return 0;
}
2.2.3 代码总结
  • 对于普通的auto局部变量(栈变量),Block捕获时,将值拷贝进Block用结构体的成员变量中。因此后续对局部变量的改变就再也影响不了Block内部。
  • 对于__block修饰的局部变量,Block捕获时,记录了该变量的地址。所以后续该变量的值改变了,block调用时,通过地址获取到的值仍然是最新的值。
  • 说明
    • 考虑到篇幅,没有介绍Block捕获__block局部变量的转换后的C++源代码。但是其本质和捕获局部静态变量是一致的,都是在Block用结构体中记录下了该变量的地址。
    • Block捕获__block局部变量的值的转换后C++代码会比,上述捕获静态局部变量的代码复杂很多。在后续的文章《Block捕获__block局部变量的底层原理》中有介绍Block捕获__block局部变量的底层原理。

2.3 底层思考

  • 参考《Objective-C 高级编程 iOS与OS X多线程和内存管理》后续章节对Blocks的实现,我们可以知道,Blocks生成的结构体会捕获所用到的变量。
  • 内存指示图
  • 对于局部变量,Blocks默认捕获的是这个局部变量的值(即图中的MemoryObj变量), 可以通过对MemroyObj这个地址上的内容进行修改(本质是运用了C语言的*运算符)
  • 而添加了__block说明符,则Blocks捕获的是这个局部变量的内存地址,即Memroy值(C语言中使用&操作取得一个变量的地址),这样Blocks在内部就可以通过对Memory上的数据对修改(*memroy = xxx),且可以影响到Blocks外部。
  • 没有用__block修饰的局部变量,在Blocks内部捕获了,即使修改了也没有任何意义(外部不受影响),所以编译器当初就设计了这个编译报错,避免产生不可预知的bug。
  • 鉴于篇幅和结构,这里没有介绍Block捕获__block修饰的变量的C++代码情况,关于该知识,可参考下一篇文章《Block捕获__block局部变量的底层原理》
原文地址:https://www.cnblogs.com/HelloGreen/p/12684033.html