更新记录
时间 | 版本修改 |
---|---|
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变量的地址(传地址,而非传值)
- block用结构体
- 就是这个细微的差别,可以做到使后续修改了变量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局部变量的底层原理》。