前言
说到外部变量,C语言中变量一般可以分为5种:
- 自动变量
- 函数参数
- 静态变量(指局部静态变量)
- 静态全局变量
- 全局变量
我们知道,Objective-C的block会捕获自动变量。在计算机编程领域,自动变量(Automatic Variable)指的是局部作用域变量,即局部变量。相对于全局变量。如下,在main函数中声明一个局部变量val = 1;
block中打印val的值,然后在执行block前修改val = 2;
, 但是block依旧输出1。这就是所谓的block会捕获自动变量。
本篇文章主要探究block捕获局部变量的底层原理。除去函数参数外,关于block和静态变量、全局变量、静态全局变量的关系将在后面的文章展开讨论。
Objective-C转C
代码语言:javascript复制int main() {
// block会捕获局部变量,下面执行执行block前后val分别输出2和1
int val = 1;
void (^block)(void) = ^{
printf("执行block后val = %d, val地址 = %pn", val, &val);
};
val = 2;
printf("执行block前val = %d, val地址 = %pn", val, &val);
block();
return 0;
<!-- 控制台打印:-->
<!-- 执行block前val = 2, val地址 = 0x7ffeefbff598-->
<!-- 执行block后val = 1, val地址 = 0x100713770-->
<!-- Program ended with exit code: 0-->
}
这里有两个疑问:
- block是如何实现捕获block外部局部变量的?
- 为什么block执行前后val的内存地址不同?
以上源码经过Clang转为C 代码,如下:
代码语言:javascript复制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 val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
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
printf("执行block后val = %d, val地址 = %pn", val, &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 val = 1;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
val = 2;
printf("执行block前val = %d, val地址 = %pn", val, &val);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
C 源码分析
如果你读过笔者的利用Clang探究block的本质和利用Clang探究__block的本质,那么本篇文章理解起来将会是切菜一样easy。甚至本篇文章变得有些多余。
以上代码看起来很熟悉,和利用Clang探究block的本质一样,依旧包括3个结构体和1个函数。唯一不同的是结构体__main_block_impl_0中多了一个成员变量int val;
而函数__main_block_func_0中使用__cself->val又初始化了另一个局部变量val。
构造函数实现:
代码语言:javascript复制__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
block函数指针实现:
代码语言:javascript复制static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy
printf("执行block后val = %d, val地址 = %pn", val, &val);
}
以上两段代码体现了block捕获局部变量的本质:编译器会根据上下文动态生成block实现的结构体__main_block_impl_0(之前文章有讲)。也会动态的在结构体中分配一个和局部变量同名同类型的变量val。在main函数中调用__main_block_impl_0构造函数时将局部变量val作为参数传递给__main_block_impl_0。__main_block_impl_0会把参数val保存在自己的成员变量val中。当执行block的函数实现__main_block_func_0时会把__main_block_impl_0实例作为入参传递给__main_block_func_0,__main_block_func_0将__main_block_impl_0的成员变量val保存的值取出后赋值给另一个临时变量val。这样就实现了block捕获外部变量。因为block在函数内部又创建了另一个临时变量val,这也验证了,为什么在block外和block内打印的val的内存地址不同。
也许你会疑问,为什么把val变量封装到了结构体__main_block_impl_0中,而不是结构体__block_impl中?
原因__block_impl这个结构体是“稳定的”、“不变的”。利用Clang探究block的本质 中说过,__block_impl这个结构体用来描述block的底层结构,包含4个成员变量。__block_impl是一个通用结构体,所谓通用是指其他block的底层结构依旧是__block_impl。如果一个Objective-C的文件中存在多个block,那么对应的C 文件依旧只存在一个__block_impl。而每个block的具体实现均对应一个类似于__main_block_impl_0的结构体,所以,把val封装到block对应的实现结构体中。
敬请期待~