再探函数与Block
函数与block的区别
函数是我们在C语言中就接触到的概念,是指一段执行特定任务的代码,结构上包括返回值,函数名,参数,方法体等,在OC或C语言中它的定义形式为
void functionName(int param){
return;
}
函数的指针的声明形式为
void (*functionName)(int)
而OC中还定义了一种封装了函数的对象类型,即Block 它的定义形式为:
void (^blockName)(int) = ^void (int param){
return;
};
观察到block左侧的声明形式与函数十分相似,唯一的区别在于前者在变量名前用的是*指针字符,而后者使用特指block的 ^ 字符(脱字符)
而右侧的定义部分则有两处不同,一是block在返回值类型前使用了 ^ 字符,其次是block在定义中没有指定名称,因此它也被称为匿名函数。
说了这么多废话,也只是在形式上区分了函数和block,那么究竟为什么要设计出这么一个东西呢,接下来就说说block的关键特性。
block是一种OC对象
如上所述,正因为block是一种对象,因此它可以在函数的内部定义,而block本身又是一个函数的封装,因此block中的函数是可以在函数内部嵌套实现的,这是一般函数做不到的,像这样:
void (^blk)(void);
void functionName(int param){
int a = 1;
blk = ^ (void){
NSLog(a);
return;
};
return;
}
我们在一个函数的内部实现了一个作为全局变量的block,并且在block的内部使用了其上下文中的一个变量a,函数则不能像这样实现。
聪明的你看到这里一定发现了一个问题,a是一个局部变量,而blk是一个全局变量,假如在函数的生命周期结束之后,也就是a变量被释放掉之后,在其他的地方再调用blk时,还能将a打印出来吗?答案当然是肯定的,不然block这种对象设计出来就没有意义了,因此这便是block的关键特性,**在block中使用的上下文中的局部变量,会被它捕获,以至于即使局部变量被释放,block中仍然可以使用当初捕获到的变量。 **
那么大家应该猜想到了,block很可能是将这些局部变量复制了一份,与原本的局部变量相独立。如此一来便又引发了两个疑问:
- 既然是两个独立的数据,那么在blk外部对a的值进行修改,会影响到blk里面的值吗?
- 在blk里面对a进行修改,又会影响到外面吗?
首先来验证第一个问题
void (^blk)(void);
void functionName(int param){
int a = 1;
blk = ^ (void){
NSLog(@"%d", a);
return;
};
a = 2;
return;
}
functionName(0);
blk();
打印出来的值为1,因此对于第一问题的答案是,blk在捕获了局部变量后,这些捕获的值就不再受外部影响了。这个也比较符合我们的直觉,毕竟已经是两个独立的元素了嘛。
而尝试在blk内部修改a时,编译器则会提示一个error: “Variable is not assignable (missing __block type specifier)”。说变量缺少一个__block修饰符,也就是说默认情况下是不允许我们修改被捕获元素的,总之我们先将__block添加到a的声明前。
void (^blk)(void);
void functionName(int param){
__block int a = 1;
blk = ^ (void){
a = 2;
NSLog(@"%d", a);
return;
};
blk();
NSLog(@"%d", a);
return;
}
functionName(0);
此次我们将blk放入函数中执行,发现打印出来的结果是2 2,即外部的a也随着内部的a一起改变了。
那这种情况下外部修改的变量值是否会影响到内部呢?也来验证看看
void (^blk)(void);
void functionName(int param){
__block int a = 1;
blk = ^ (void){
NSLog(@"%d", a);
return;
};
a = 2;
NSLog(@"%d", a);
return;
}
functionName(0);
blk();
发现结果依旧是2 2,证明在添加了__block修饰之后被block捕获的变量与外部的变量是互相影响的,那么这又是如何做到的呢,明明是两个独立的元素才对啊?
block的底层实现
实际上__block修饰符对block及它捕获的局部变量的影响远比表面上看起来大,在llvm中使用Clang -rewirte-objc 命令将.m文件转译为c++文件,可一窥block的真身。
将如下的main.m通过 Clang -rewirte-objc main.m转换成main.cpp
int main(int argc, char * argv[]) {
void (^blockName)(int) = ^void (int param){
return;
};
return 0;
}
得到了一个130多行的cpp文件,先找到最后的main函数部分:
int main(int argc, char * argv[]) {
void (*blockName)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
return 0;
}
可以发现原本的block对象变成了一个函数指针类型,并且这个函数指针的内容是通过调用一个名为__main_block_impl_0的函数返回而来,它传递了两个参数,分别为__main_block_func_0和__main_block_desc_0_DATA。
我们一步一步看,首先是__main_block_impl_0函数,就在上方
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;
}
};
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)};
发现其实是一个结构体的构造函数,也就说block的对象引用实质上是block结构体构造函数的一个指针。
而构造函数的第一个参数__main_block_func_0则是
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int param) {
return;
}
不难猜测这就是block封装的函数,只是多了一个__main_block_impl_0结构体类型的参数__cself,这个可以稍后再提。
这个函数被赋给了__block_impl结构体中的函数指针成员FuncPtr。
__block_impl中还有一个isa指针成员,其值为&_NSConcreteStackBlock,代表当前block的类,block还有另外两种类型,这个也是稍后再提。
而__main_block_desc_0则是一个保存了__main_block_impl_0所占空间的结构体。
接着我们在oc代码中添加一行block的执行
blockName(0);
转换后发现它变成了
((void (*)(__block_impl *, int))((__block_impl *)blockName)->FuncPtr)((__block_impl *)blockName, 0);
去除掉类型转换的部分,简化就是
(blockName->FuncPtr)(blockName, 0);
那么其实就是调用了block结构体中的FuncPtr所指的函数。
至此实际上没有什么复杂的点,那么再看看它对捕获的局部变量的处理,在main.m中这样写:
int main(int argc, char * argv[]) {
int a = 1;
void (^blockName)(int) = ^void (int param){
printf("%d", a);
printf("%d", param);
return;
};
blockName(0);
return 0;
}
这里还需要导入一个包含printf函数的头文件,例如stdio.h或者foundation.h,转译后的代码会变得很长,但依旧是找到最后面的main函数部分,发现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;
}
};
新增的成员即为被捕获的变量a,作为了__main_block_impl_0的成员,且在初始化时就将a的值赋给了结构体内的成员a,这与我们之前想象的一致,即外部的局部变量与block内部使用的局部变量实际上是两个元素。
而block函数部分变为:
static void __main_block_func_0(struct __main_block_impl_0 *__cself, int param) {
int a = __cself->a; // bound by copy
printf("%d", a);
printf("%d", param);
return;
}
系统自动生成了一句注释bound by copy,提醒我们在block函数中使用的“局部变量”仅仅只是一个复制的值,而没有获取它的地址,因此无法改变原本的局部变量值。这里大家也可以尝试一下其他类型的变量,比如oc对象类型,本质上都是生成了一个与局部变量类型完全相同的拷贝作为block结构体的新成员。
至此应该没有任何问题,那么为了解答我们之前的疑问,在为局部变量添加了__block修饰符后,block是如何联系内外的局部变量的。
我们在a前加上__block修饰符
int main(int argc, char * argv[]) {
__block int a = 1;
void (^blockName)(int) = ^void (int param){
a = 2;
printf("%d", a);
return;
};
blockName(0);
return 0;
}
然后转译得到:
int main(int argc, char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 1};
void (*blockName)(int) = ((void (*)(int))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *, int))((__block_impl *)blockName)->FuncPtr)((__block_impl *)blockName, 0);
return 0;
}
此时发现,加上了__block的int型变量a,在转译后竟然变成了一个结构体类型__Block_byref_a_0。
其声明为:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};