iOS - __block 修饰符底层探索
Block技术合集
阅读本文前,请先思考如下问题
- 为什么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))
,为什么打印出来的地址不同