阅读本文前,请先思考如下问题
- 为什么Block可以截获变量
- 为什么Block外定义的基本数据类型,在Block内部不能修改
- 为什么用__block修饰后,在Block内部可以修改
本文将对Block底层探索并解答如上三个问题
什么是Block
带有自动变量值的匿名函数
Block截获变量
int main(int argc, const char * argv[]) { @autoreleasepool { void(^blk)(void) = ^{ printf("Block\n"); }; blk(); } return 0; }
编译成成cpp
代码, 代码非常多,我们精简如下
//1. 结构体 struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; //2. struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; //3. static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Block\n"); } //4. 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)}; //5. main函数代码块 int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); } return 0; }
总共4个结构体和一个main函数代码块
查看5. main函数代码块
可见,block对象被编译成了__main_block_impl_0
类型的结构体, 这个结构体由两个成员结构体和一个构造函数组成,两个结构体分别是__block_impl
和__main_block_desc_0
类型的,其中__block_impl
结构体中有一个函数指针, 指针指向__main_block_func_0
类型的结构体,总结关系图如下:
Block在定义的时候:
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA))
Block在调用的时候:
((__block_impl *)blk)->FuncPtr
Block内部的函数打印,很显然放在了__main_block_func_0
,那么block内部截获的数据存放在哪呢?同样 我们对如下代码进行编译
int main(int argc, const char * argv[]) { @autoreleasepool { int a = 10; void(^blk)(void) = ^{ printf(" Block\n a = %d\n", a); }; blk(); } return 0; }
编译成cpp
//1. struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int a; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy printf(" Block\n a = %d\n", a); } 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, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int a = 10; void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); } return 0; }
很显然的是,__main_block_impl_0
结构体增加了成员变量int a;
并且在结构体的构造函数__main_block_func_0
中对变量进行赋值int a = __cself->a
,而这一赋值操作,在Block定义的时候就已完成(并非在Block调用的时候),这也是Block截获变量的原理(文章开头问题1:为什么Block可以截获变量)。Block对不同数据类型截获方式请查看我之前写的iOS - Block变量截获
为什么Block中不能修改变量值
我们先把代码做微小的修改,即对 block外定义的变量'int a = 10', 分别在block定义前后及block内部打印其地址
int main(int argc, const char * argv[]) { @autoreleasepool { int a = 10; printf("before block &a = %p \n", &a); void(^blk)(void) = ^{ printf(" Block\n a = %d\n in block &a = %p \n ", a, &a); }; printf("after block &a = %p \n\n", &a); blk(); } return 0; }
打印如下:
before block &a = 0x7ffeefbff4ec after block &a = 0x7ffeefbff4ec Block a = 10 in block &a = 0x1004385f0
很明显的是,外block外部打印的int a
地址一致,但在block内部却不一样了,即block内部的a
并不是我们外部定义的int a
(此时作者想起了一首歌:你说的黑不是黑,你说的白是神魔TM的白...)
这里问题二的答案已经很明显了,为什么block内部无法修改外部的变量,因为就不是同一个变量啊,只是长的一样而已
有人就问了,那block内部的那个a究竟是谁从哪里来?请看前边编译的cpp
代码中block方法的结构体__main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy printf(" Block\n a = %d\n", a); }
此时你应该恍然大悟,这个a是block内部重新定义的a
,取值自block外部定义的int a = 10
,至此,block内部无法修改外部变量的问题显而易见:
为什么无法修改:因为不是同一个值,地址不一样
内部的
a
变量哪来的:block底层重新定义的,取值自外部(相当于副本)
为什么用__block修饰后,在Block内部可以修改
先附上__block修饰前编译的main函数源码(用于下文做比较)
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int a = 10; printf("before block &a = %p \n", &a); void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a)); printf("after block &a = %p \n\n", &a); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); } return 0; }
不废话,改代码加__block修饰,先打印看看
int main(int argc, const char * argv[]) { @autoreleasepool { __block int a = 10; printf("before block &a = %p \n", &a); void(^blk)(void) = ^{ printf(" Block\n a = %d\n in block &a = %p \n ", a, &a); }; printf("after block &a = %p \n\n", &a); blk(); } return 0; }
before block &a = 0x7ffeefbff4e8 after block &a = 0x103009f98 Block a = 10 in block &a = 0x103009f98
根据打印,很明显的能看到,a
在__block修饰定义时的地址,与block内部及block定义后的地址不一致,此处大胆猜测,__block修饰的变量,在block定义时,会生成新的对象(下文得知是结构体),在block外部获取、更改该变量时,获取的是这个新生成的对象
我们编译一下
//1. struct __Block_byref_a_0 { void *__isa; __Block_byref_a_0 *__forwarding; int __flags; int __size; int a; }; //2. struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __Block_byref_a_0 *a; // by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; //3. static void __main_block_func_0(struct __main_block_impl_0 *__cself) { __Block_byref_a_0 *a = __cself->a; // bound by ref printf(" Block\n a = %d\n in block &a = %p \n ", (a->__forwarding->a), &(a->__forwarding->a)); } //4. int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; __attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10}; printf("before block &a = %p \n", &(a.__forwarding->a)); void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344)); printf("after block &a = %p \n\n", &(a.__forwarding->a)); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); } return 0; }
编译后源码先找不同
- 定义
int a = 10
变成了__Block_byref_a_0 a = 10(精简)
- 多了各结构体
__Block_byref_a_0
,而此结构体内部有int a
__main_block_impl_0
结构体中的int a
不见了,多了个__Block_byref_a_0 *a
__main_block_func_0
结构体中的int a = __cself->a
变成了__Block_byref_a_0 *a = __cself->a
block
外部的printf("after block &a = %p \n\n", &a)
变成了printf("after block &a = %p \n\n", &(a.__forwarding->a))
上文不同翻译总结一下就是答案:
变量添加__block修饰后,变量会被封装称结构体,结构体内部包含变量,
在block内部修改变量时,修改的是结构体__Block_byref_a_0
内部的变量数据(a->__forwarding->a)
(所以可以修改)
出了block作用域后,修改数据修改的仍然是__Block_byref_a_0
内部的变量数据(a.__forwarding->a)
疑问:
printf("before block &a = %p \n", &(a.__forwarding->a)); printf("after block &a = %p \n\n", &(a.__forwarding->a)); before block &a = 0x7ffeefbff4e8 after block &a = 0x100474798 `
查看编译后底层代码,打印地址查找都是&(a.__forwarding->a))
,为什么打印出来的地址不同