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\n";

void (^blk)(void) = ^{

printf(fmt,val);

};

val = 2;

fmt = "These value were changed. val = %d\n";

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\n";

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\n";

((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\n";

void (^blk)(void) = ^{

++val;

printf(fmt,val);

};

val = 2;

fmt = "These value were changed. val = %d\n";

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\n";

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\n";

((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局部变量的底层原理》。

友情链接:
Copyright © 2022 86年世界杯_世界杯预选赛阿根廷 - fjyfzz.com All Rights Reserved.