本质
先写一个简单的block
int main(int argc, const char * argv[]) { @autoreleasepool { int a = 10; void (^block) (void) = ^{ NSLog(@"this is block"); NSLog(@"this is block %d", a); }; } }复制代码
使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m 命令将其OC代码转化为底层的C++代码,观察block的底层结构。 打开main.cpp代码,直接搜索main(找到main函数,代码如下:
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int a = 10; void (*block) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a)); } return 0;}复制代码
可能看到这大家有点不太明白,那么一堆怎么看啊,咱们简化一下,把那些强制转换去掉
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; int a = 10; *block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a); } return 0;}复制代码
大家可以看到block就相当于是__main_block_impl_0,那么__main_block_impl_0是什么呢?在main.cpp代码里搜索
__main_block_impl_0
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; }};复制代码
很清晰,__main_block_impl_0是个结构体,那么相当于block就是一个结构体. 结构体中包含两个不同的结构体变量__block_impl 和 __main_block_desc_0,并且__main_block_impl_0结构体内有一个同名构造函数__main_block_impl_0,构造函数中对一些变量进行了赋值最终会返回一个结构体,那么也就是说最终将一个__main_block_imp_0结构体的地址赋值给了block变量.
__main_block_impl_0结构体内可以发现__main_block_impl_0构造函数中传入了四个参数.(void *)__main_block_func_0、&__main_block_desc_0_DATA、a、flags,其中flags有默认值,也就是说flags参数在调用的时候可以省略不传.而最后的a则表示传入的_a参数会自动赋值给a成员,相当于a=_a,那么咱们看看前两个参数分别代表什么?
由上面的简化代码:
*block = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, a);复制代码
可以看出fp是__main_block_func_0,desc是__main_block_desc_0_DATA
__main_block_func_0
在__main_block_func_0函数中首先取出block中a的值,紧接着可以看到两个熟悉的NSLog,可以发现这两段代码恰恰是我们在block块中写下的代码。 那么__main_block_func_0函数中其实存储着我们block中写下的代码。而__main_block_impl_0函数中传入的是(void *)__main_block_func_0,也就说将我们写在block块中的代码封装成__main_block_func_0函数,并将__main_block_func_0函数的地址传入了__main_block_impl_0的构造函数中保存在结构体内。
&__main_block_desc_0_DATA
可以看到_main_block_desc_0存储着两个参数,reserved和Block_size,并且reserved赋值为0,而Block_size则存储着__main_block_impl_0的占用空间大小。最终将__main_block_desc_0结构体的地址传入__main_block_func_0中赋值给Desc。__block_impl
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr;};复制代码
__block_impl包含isa指针,说明block本质上也是一个OC对象,咱们再看看结构体里的FuncPtr是什么?前面实际上可以看出FuncPtr指向block所封装的代码块地址,但咱们从另一方面也说明一下
首先按照它源码里的结构来写block底层,首先block相当于__main_block_impl_0,而__main_block_impl_0里面有两个结构体变量,如下代码
struct __block_imp { void *isa; int Flags; int Reserved; void *FuncPtr;};struct __main_block_desc_0 { size_t reserved; size_t Block_size;};struct __main_block_imp_0 { struct __block_imp impl; struct __main_block_desc_0* Desc; int a;};复制代码
重新编写一下main.m文件
int main(int argc, const char * argv[]) { @autoreleasepool { int a = 10; void (^block) (void) = ^{ NSLog(@"this is block"); NSLog(@"this is block %d", a); }; //因为上面已经说了block相当于__main_block_imp_0,那么这里我给它赋值 struct __main_block_imp_0 *blockStruct = (__bridge struct __main_block_imp_0 *)block; NSLog(@"===="); block(); } return 0;}复制代码
打断点调试一下
可以看到__block_imp的FuncPtr内存地址是0x100000ca0,接下来执行block里面的代码 Debug->Debug Workflow->Always Show Disassembly 可以看出block块里面的代码开始的内存地址也是0x100000ca0,由此可以知道__block_imp的FuncPtr指向block所封装的代码块地址,等执行block时会通过FuncPtr寻找将要执行的代码块,并且调用当然上面的示例从侧面也证明block的本质就是__main_block_impl_0结构体类型
总结一下__main_block_impl_0结构体构造函数:
- _block_impl结构体中isa指针存储着&_NSConcreteStackBlock地址,可以暂时理解为其类对象地址,block就是_NSConcreteStackBlock类型的。
- block代码块中的代码被封装成__main_block_func_0函数,FuncPtr则存储着__main_block_func_0函数的地址。
- Desc指向__main_block_desc_0结构体对象,其中存储__main_block_impl_0结构体所占用的内存。
总结
下面用一张图来展示block底层结构体的关系
block底层的数据结构:
block的变量捕获
为了保证block内部能够正常访问外部的变量,block有一个变量捕获机制。
局部(auto)变量
回顾最开始的代码
int main(int argc, const char * argv[]) { @autoreleasepool { int a = 10; void (^block) (void) = ^{ NSLog(@"this is block"); NSLog(@"this is block %d", a); }; } }复制代码
在block内部引用外部变量,它是一个局部变量,看看其转化后的代码
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; }};复制代码
结构体内部有一个int类型的变量a,用于存储所引用的外部变量的值.因为是值存储,所以在block生成之后,无论外部变量做何修改,a依然是之前所定义的值
static变量
咱们把示例的代码改造一下,变量前面加个static关键字
int main(int argc, const char * argv[]) { @autoreleasepool { static int a = 10; void (^block) (void) = ^{ NSLog(@"this is block %d", a); }; block(); } return 0;}复制代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m转成c++代码
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修饰的变量在block内部为指针传递,block直接捕获外部变量的内部地址,此时若外部变量在block声明之后修改,再block内部使用也会同步修改,因为其是从同一内部地址取的值
这儿大家想一个问题,为什么两种变量会有差异呢?
因为自动变量可能会被销毁,block在执行的时候有可能自动变量已经被销毁了,那么此时如果再去访问被销毁的地址肯定会发生坏内存访问,因此对于自动变量一定是值传递而不可能是指针传递。而静态变量不会被销毁,所以完全可以传递地址。而因为传递的是地址,所以在block调用之前修改地址中保存的值,在block中使用值会随之改变
全局变量
通过上面的讲解,大家肯定有一定的基础了,咱们就来个全的
int age = 60;int main(int argc, const char * argv[]) { @autoreleasepool { int a = 10; static int b = 20; void (^block) (void) = ^{ NSLog(@"age is %d a is %d b is %d", age, a, b); }; block(); } return 0;}复制代码
上面的代码包含了全局变量age,局部变量a,static变量b,咱们来看其底层是什么样的
int age = 60;struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int a; int *b; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int *_b, int flags=0) : a(_a), b(_b) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; }};复制代码
通过上述代码可以发现,__main_block_imp_0并没有添加age变量,因此block不需要捕获全局变量,因为全局变量无论在哪里都可以访问。
总结一句话:局部变量因为跨函数访问所以需要捕获,全局变量在哪里都可以访问 ,所以不用捕获。
局部变量都会被block捕获,自动变量是值捕获,静态变量为地址捕获。全局变量则不会被block捕获