Block是C语言的扩充功能。用一句话来形容Block的扩展功能:带有自动变量(局部变量)的匿名函数。
Block概述
Block类型变量
Block语法单从其记述方式上来看,除了没有名称以及带有^外,其他的都与C语言的函数定义相同。在C语言定义函数时,就可以将函数的地址赋值给函数指针类型的变量。
int function(int count) {
return count+1;
}
int (* functionptr)(int) = &function;
声明Block类型的变量的示例如下:
int (^block)(int par);
通过和函数指针的对比可以知道,声明Block类型的变量仅仅是将函数指针类型变量的""修改为^,所以该Block类型的变量与一般的C*语言变量完全相同,可以作为以下用途使用。
自动变量(局部变量)。
函数参数。
静态变量。
静态全局变量。
全局变量(属性)。
接下来我们定义一个Block并将其赋值给Block类型的变量。
int (^block)(int) = ^(int count){
return count+1;
};
由^开始的Block语法生成的Block被赋值给变量block中,因为和普通变量相同,所以当然也可以由一个Block类型的变量向另一个Block类型的变量赋值。
int (^block1)(int) = block;
int (^block2)(int);
block2 = block1;
另外我们也可以在函数的参数和函数的返回值中使用Block。
通过上述我们可以看出,在声明Block类型的变量和用Block作为函数的参数和返回值中,记述的方式及其复杂。这是我们可以像使用函数指针那样使用typedef来解决问题。
typedef int (^block_t)(int);
原来的声明方式
int (^block)(int);
现在的声明方式
block_t blk;
通过使用typedef,Block类型的变量的定义变得更简单了。
捕获局部变量的值
int var = 10;
const char *fmt = "var = %d\n";
void(^block)(void) = ^(){
printf(fmt, var);
};
var = 2;
fmt = "These value ware change. val=%d.";
block();
上述代码运行后的结果。
var = 10
执行的结果并不是修改之后的值,而是Block自动捕获的值。
Block实现
Block的实质
通过“clang -rewrite-objc ”将含有Block语法的代码转换为C++代码。说是C++,其实也就是使用了struct而已,其本质是C语言代码。
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
void (^block)(void) = ^(){
printf("block\n");
};
block();
}
return 0;
}
接下来我们通过clang将这份代码转换为C++的形式。
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("block\n");
}
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;
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
接下来我们来逐步分析这段代码。
首先我们可以看到转换后的源代码也有着相同的表达式。
__main_block_func_0
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("block\n");
}
如转换后的代码所示,使用Block使用的匿名函数,实际上被当做C语言中的简单函数处理。
该函数参数中的__cself相当于OC实例方法中指向自身的实例变量self,即参数__cself为指向Block值的变量。
struct __main_block_impl_0 *__cself
__main_block_impl_0
和OC中的self相同,参数__cself是__main_block_impl_0结构体的指针。该结构体声明如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
};
由于转换后的代码一并写入了构造函数(可以简单的认为是OC中的初始化方法)。在这里我们将其去掉,那么看起来将会十分简单。第一个成员变量为impl,我们看下impl的声明。
__block_impl
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
__main_block_desc_0
第二个成员变量是Desc指针,以下为其__main_block_desc_0结构体的声明:
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)};
__main_block_impl_0构造函数
接下来我们来看看__main_block_impl_0结构体的构造函数。
__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;
}
当前这个构造函数接收参数:
- fp:是由Block语法转换的C语言函数指针,也就是我们外界传入的__main_block_impl_0的C语言函数。
- desc:类型为__main_block_desc_0结构体的实例。
接下来我们来看看改构造函数的调用:
void (*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
由于转化后的代码较长,所以我们去掉了一部分,具体如下:
struct __main_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *block = &tmp;
这样就容易理解了,该源代码是将__main_block_impl_0类型的局部变量,也就是栈上生成的__main_block_impl_0结构体实例的指针,赋值给__main_block_impl_0结构体指针类型的变量block,以下为这部分源代码对应的最初源代码。
void (^block)(void) = ^(){
printf("block\n");
};
由此可以该源代码使用的block变量,是由__main_block_impl_0结构体实例的大小,来进行初始化的。
Block的调用
block();
这部分代码被转换成了如下部分:
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
去掉类型强转部分:
(*block->impl.FuncPtr)(block);
这就是使用了函数指针来调用函数,由Block语法转换的__main_block_func_0的函数的指针赋值给了成员变量impl的成员变量FuncPtr。
__main_block_func_0的参数__cself指向Block的值(也就是实例)。在函数调用的源代码中,我们可以看到Block正是作为参数传递的。
_NSConcreteStackBlock
在我们的__main_block_impl_0的构造函数中我们看到这样一句代码。
impl.isa = &_NSConcreteStackBlock;
看到这句代码时,是不是觉得和我们OC中类的结构很相似。所以说把Block作为OC对象处理时关于该类的信息都会被放置于_NSConcreteStackBlock中。
所以说Block其实就是OC对象。
自动捕获局部变量的值
构造函数探索
int var = 10;
const char *fmt = "var = %d\n";
void (^block)(void) = ^(){
printf(fmt, var);
};
和之前一样,我们将这段代码通过clang编译后来进行研究:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int var;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _var, int flags=0) : fmt(_fmt), var(_var) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int var = __cself->var; // bound by copy
printf(fmt, var);
}
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语法表达式中使用的局部变量被作为成员变量追加到了__main_block_impl_0结构体中。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int var;
};
__main_block_impl_0结构体中声明的成员变量的类型和局部变量的类型完全相同。
注意:Block代码块中没有使用到的局部变量不会被自动追加。Block只捕获在其代码块中使用到的局部变量。
接下来我们来看看初始化该结构体实例的构造函数之间的差异:
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _var, int flags=0) : fmt(_fmt), var(_var)
在初始化结构体实例的同时,根据传递给构造函数的参数对局部变量追加的成员变量进行初始化。
: fmt(_fmt), var(_var)
这句代码就是对结构体中自动追加的成员变量的初始化。
匿名函数
接下来我们来看看使用Block的匿名函数的实现,最初的源代码的Block语法的实现如下:
^(){
printf(fmt, var);
}
该源代码被转换成了以下函数:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
const char *fmt = __cself->fmt; // bound by copy
int var = __cself->var; // bound by copy
printf(fmt, var);
}
总的来说,所谓的自动捕获局部变量就是在执行Block时,Block代码块使用到的局部变量被保存到了Block的结构体实例(也就是Block自身)中。
__block修饰符
众所周知当我们的局部变量没有任何修饰符的时候,是不能在Block的代码块中被修改的,如果对局部变量执行了修改操作,那么它将会报出一个编译时错误。
Variable is not assignable (missing __block type specifier)
解决这个问题有两种办法。第一种:使用下面几种类型的变量。
- 静态变量
- 静态全局变量
- 全局变量
接下来我们来看看这段代码:
int global_var = 1;
static int static_global_var = 2;
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
static int static_val = 3;
void (^block)(void) = ^(){
global_var += 1;
static_global_var += 2;
static_val += 3;
};
block();
}
return 0;
}
该代码在Block代码块的内部改变了global_var、static_global_var、static_val三个变量的值。该源代码转换后的代码如下:
int global_var = 1;
static int static_global_var = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_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 *static_val = __cself->static_val; // bound by copy
global_var += 1;
static_global_var += 2;
(*static_val) += 3;
}
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)};
对静态全局变量和全局变量的访问与转换前相同,那么局部静态变量又要如何转换呢?
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
global_var += 1;
static_global_var += 2;
(*static_val) += 3;
}
使用静态变量static_val的指针对其进行访问,将静态变量static_val的指针传递给__main_block_impl_0结构体的构造函数并保存。这是超出作用域使用变量最简单的办法。
为什么我们普通的局部变量不使用传递指针的方式呢?
因为一般的局部变量它是存储在栈区的,而栈区的变量出了作用域之后就会被系统回收,如果我们再变量的作用域外访问了这个指针,那就不会我们想要的结果了。
第二种方法是使用__block:
接下来我们就使用__block来修饰我们的局部变量:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
__block int var = 10;
void (^block)(void) = ^(){
var = 1;
};
block();
}
return 0;
}
该源代码转换后的代码如下:
struct __Block_byref_var_0 {
void *__isa;
__Block_byref_var_0 *__forwarding;
int __flags;
int __size;
int var;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_var_0 *var; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_var_0 *var = __cself->var; // bound by ref
(var->__forwarding->var) = 1;
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->var, (void*)src->var, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->var, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
只是增加了__block代码量就急剧增加,我们来看看它是如何转换的。
__block int var = 10;
转换后的代码:
__Block_byref_var_0 var = {
0,
&var,
0,
sizeof(__Block_byref_var_0),
10
};
我们发现被__block修饰的变量被转换成了结构体实例,这个实例存储在栈区。
__Block_byref_var_0结构体声明如下:
struct __Block_byref_var_0 {
void *__isa;
__Block_byref_var_0 *__forwarding;
int __flags;
int __size;
int var;
};
该结构体的最后一个成员变量var就相当于原来的局部变量var。
那么下面这段个__block修饰的变量赋值时的情况怎么样呢?
^(){
var = 1;
};
该源代码的转换如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_var_0 *var = __cself->var; // bound by ref
(var->__forwarding->var) = 1;
}
__Block_byref_var_0结构体实例的成员变量__forwarding持有指向该实例自身的指针,通过成员变量__forwarding访问成员变量var。(成员变量var是该实例自身持有的变量,相当于原来的局部变量var)。
如下图所示:
总结
- Block其实就是OC对象。
- Block会自动捕获局部变量,全局变量不会捕获。
- 如果要在Block的代码块里修改变量的值,有两种方案:
- 使用全局静态变量、静态变量、全局变量。
- 使用__block修饰符来修饰局部变量。