在iOS
开发中,经常会使用到Block
,那Block
到底是什么?它的实现方式是什么?通过阅读《Objective-C高级编程:iOS与OS X多线程和内存管理》,会对Block
有个更深的了解。
Block
是“带有自动变量值的匿名函数”。
本质
首先通过clang
,需要对我们的源文件进行转换。例如clang -rewrite-objc BlockTest.c
:
int main () {
void (^BlockTest)(void ) = ^{
printf ("执行Block" );
};
BlockTest();
}
转换结果如下:
struct __b lock_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __ main_block_impl_0 {
struct __b lock_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" );
}
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 () {
void (*BlockTest)(void ) = ((void (*)())&__ main_block_impl_0((void *)__ main_block_func_0, &__ main_block_desc_0_DATA));
((void (*)(__b lock_impl *))((__b lock_impl *)BlockTest)->FuncPtr)((__b lock_impl *)BlockTest);
}
可以看到,这里只有struct
,而^{printf("执行Block");}
函数被转换成了:
static void __ main_block_func_0(struct __ main_block_impl_0 *__ cself) {
printf ("执行Block" );
}
这个函数需要一个__main_block_impl_0
类型的参数,改结构体声明如下:
struct __ main_block_impl_0 {
struct __b lock_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;
}
};
改结构体有两个成员变量impl
和Desc
,以及一个构造函数。再来看__block_impl
结构体声明:
struct __b lock_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
知道runtime
的小伙伴对isa
一定不陌生,只不过这里使用的是void*
。改结构体的成员变量我们等会再说。
再来看__main_block_desc_0
结构体声明:
static struct __ main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
这些也如同成员变量名称所示,其结构体为今后版本升级所需要的区域和Block
的大小。
再来看__main_block_impl_0
的构造函数:
接着,我们再来看main
函数的第一行代码:
void (*BlockTest)(void ) = ((void (*)())&__ main_block_impl_0((void *)__ main_block_func_0, &__ main_block_desc_0_DATA));
这里转换太多,我们转义一下:
struct __ main_block_impl_0 BlockTest = __ main_block_impl_0((void *)__ main_block_func_0, &__ main_block_desc_0_DATA);
以上就对应我们最初源代码:
void (^BlockTest)(void ) = ^{
printf ("执行Block" );
};
这里,将__main_block_impl_0
结构体实例的指针赋值给变量BlockTest
,源代码中的Block
就是__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 );
参数1为函数指针,这里我们传入了__main_block_func_0
的函数指针。参数2为__main_block_desc_0
结构体类型的参数,这里直接传入全局变量__main_block_desc_0_DATA
,它的初始化如下:
__ main_block_desc_0_DATA = { 0 , sizeof (struct __ main_block_impl_0)};
__main_block_desc_0_DATA
即初始化__main_block_impl_0
。
如此,__main_block_impl_0
的初始化如下:
impl.isa = &_ NSConcreteStackBlock;
impl.Flags = 0 ;
impl.FuncPtr = __ main_block_func_0;
Desc = desc;
即,__block_impl
的初始化如下:
isa=&_ NSConcreteStackBlock;
Flags=0 ;
Reserved=0 ;
FunPtr=__ main_block_func_0;
接着,在来看源代码BlockTest()
的转换:
((void (*)(__b lock_impl *))((__b lock_impl *)BlockTest)->FuncPtr)((__b lock_impl *)BlockTest);
去掉转换部分如下:
(*BlockTest->FuncPtr)(BlockTest);
由Block
语法转换的__main_block_func_0
函数的指针被赋值到成员变量FuncPtr
中,将BlockTest
作为参数,传递到__main_block_func_0
函数中。
但是,这里还有一个我们没有搞清楚,就是isa
:
isa=&_ NSConcreteStackBlock;
_NSConcreteStackBlock
相当于class_t
结构体实例,在将Block
作为Objective-C
的对象处理时,关于该类的信息放置于_NSConcreteStackBlock
中,
至此,我们已经明白了Block
的本质,Block
经过clang
转换以后,会生成结构体。该结构体包括:
isa
,结构体信息
FuncPtr
,函数地址,即Block
代码块
Desc
、Flags
和其他。
调用Block
,即调用对象的方法。
截获自动变量
int main () {
int age = 10 ;
void (^BlockTest)(void ) = ^{
printf ("执行Block%d" ,age);
};
BlockTest();
}
这里我们增加一个变量,转换后的代码如下:
struct __b lock_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __ main_block_impl_0 {
struct __b lock_impl impl;
struct __ main_block_desc_0* Desc;
int age;
__ main_block_impl_0(void *fp, struct __ main_block_desc_0 *desc, int _ age, int flags=0 ) : age(_ age) {
impl.isa = &_ NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __ main_block_func_0(struct __ main_block_impl_0 *__ cself) {
int age = __ cself->age;
printf ("执行Block%d" ,age);
}
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 age = 10 ;
void (*BlockTest)(void ) = ((void (*)())&__ main_block_impl_0((void *)__ main_block_func_0, &__ main_block_desc_0_DATA, age));
((void (*)(__b lock_impl *))((__b lock_impl *)BlockTest)->FuncPtr)((__b lock_impl *)BlockTest);
}
通过以上转换代码,我们能很清楚的看到,__main_block_impl_0
结构体多了一个成员变量age
,并在初始化的时候将age
的值传递过去。在调用FuncPtr
的时候,通过__cself
将age
取出来。
通过以上分析,可以指定,Block
只所以能截获自动变量,是因为Block
将自动变量作为自己的成员变量,并在初始化的时候赋值,在使用的时候取出来。
__block 修饰符
有时候,我们需要在Block
中修改截获的自动变量,这时候就需要在自动变量前加入__block
修饰符,否则编译出错。那么,我们也通过clang
来看一下转换后的代码:
int main () {
__b lock int age = 10 ;
void (^BlockTest)(void ) = ^{
age = 20 ;
printf ("修改后的age%d" ,age);
};
BlockTest();
printf ("再次输出age%d" ,age);
}
struct __b lock_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __B lock_byref_age_0 {
void *__ isa;
__B lock_byref_age_0 *__f orwarding;
int __f lags;
int __ size;
int age;
};
struct __ main_block_impl_0 {
struct __b lock_impl impl;
struct __ main_block_desc_0* Desc;
__B lock_byref_age_0 *age;
__ main_block_impl_0(void *fp, struct __ main_block_desc_0 *desc, __B lock_byref_age_0 *_ age, int flags=0 ) : age(_ age->__f orwarding) {
impl.isa = &_ NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __ main_block_func_0(struct __ main_block_impl_0 *__ cself) {
__B lock_byref_age_0 *age = __ cself->age;
(age->__f orwarding->age) = 20 ;
printf ("修改后的age%d" ,(age->__f orwarding->age));
}
static void __ main_block_copy_0(struct __ main_block_impl_0*dst, struct __ main_block_impl_0*src) {
_B lock_object_assign((void *)&dst->age, (void *)src->age, 8 );
}
static void __ main_block_dispose_0(struct __ main_block_impl_0*src) {
_B lock_object_dispose((void *)src->age, 8 );
}
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};
int main () {
__ attribute__((__b locks__(byref))) __B lock_byref_age_0 age = {(void *)0 ,(__B lock_byref_age_0 *)&age, 0 , sizeof (__B lock_byref_age_0), 10 };
void (*BlockTest)(void ) = ((void (*)())&__ main_block_impl_0((void *)__ main_block_func_0, &__ main_block_desc_0_DATA, (__B lock_byref_age_0 *)&age, 570425344 ));
((void (*)(__b lock_impl *))((__b lock_impl *)BlockTest)->FuncPtr)((__b lock_impl *)BlockTest);
printf ("再次输出age%d" ,(age.__f orwarding->age));
}
我们会发现,只是增加__block
,代码量就急剧增加。而且我们竟然发现,__block int age = 10;
竟然转化成了__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
;
即age
变成了__Block_byref_age_0
结构体对象。并且初始化为10,等到我们再次调用age
的时候,竟然是使用结构体来调用。所以,这里我们猜测,源代码中的int
型的age
,已经被包装成__Block_byref_age_0
结构体类型的age
。
我们来看__Block_byref_age_0
的声明:
struct __B lock_byref_age_0 {
void *__ isa;
__B lock_byref_age_0 *__f orwarding;
int __f lags;
int __ size;
int age;
};
age
相当于原自动变量,其中isa
表示结构体的信息,__forwarding
其实是指向自身的指针。
另外增加的函数__main_block_copy_0
相当于调用reatain
实例方法的函数,将__Block_byref_age_0
结构体对象赋值到__main_block_impl_0
结构体对象中,而__main_block_dispose_0
函数相当于release
实例方法的函数。主要是释放赋值在__main_block_impl_0
结构体对象中的成员变量__Block_byref_age_0
。
至此,我们可以明白__block
的作用,例如age
:
被__block
修饰的变量age
,会通过__Block_byref_age_0
结构体进行包装,并初始化结构体。其中age
是初始化的值,__forwarding
是指向其自身的指针
而Block
结构体中,会增加age
结构体的引用,并通过构造函数进行初始化,其引用变量的生命周期是通过__main_block_copy_0
函数和__main_block_dispose_0
函数来管理。
之后,在调用Block
块执行修改自动变量的时候,会通过age
结构体的__forwarding
找到自身,之后找到age
变量进行赋值
之后,假如再次使用age
,这时候我们要明白,age
已经不是int
类型,而是__Block_byref_age_0
结构体类型。
Block存储域
之前我们看到Blcok
结构体中的isa
指针指向了&_NSConcreteStackBlock
,这里,一共有三种类型:
_NSConcreteStackBlock 即该类的对象Block
设置在栈上
_NSConcreteGlobalBlock 与全局变量一样,设置在程序的数据区域中
_NSConcreteMallocBlock 此类的实例对象则设置在由malloc
函数分配的内存块(即堆)中
上述例子中的Block
都是_NSConcreteStackBlock
类,且都设置在栈上。这是因为我们定义的Block
在main
函数体中,假如定义在全局,则生成的Block
为_NSConcreteGlobalBlock
,例如:
int (^GlobalBlockTest)(void ) = ^int {
printf ("GlobalBlockTest" );
return 20 ;
};
int main () {
}
转换后为:
struct __ GlobalBlockTest_block_impl_0 {
struct __b lock_impl impl;
struct __ GlobalBlockTest_block_desc_0* Desc;
__ GlobalBlockTest_block_impl_0(void *fp, struct __ GlobalBlockTest_block_desc_0 *desc, int flags=0 ) {
impl.isa = &_ NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
该Blcok
的类为_NSConcreteGlobalBlock
类,此Block
即该Block
用结构体实例设置在程序的数据区域中,因为在使用全局变量的地方不能使用自动变量,所以不存在对自动变量进行截获。由此Block
用结构体实例的内容不依赖于执行时的状态,所以整个程序中值需要一个实例。因此将Block
用结构体实例设置在与全局变量相同的数据区域中即可。
其实,只要Block
语法的表达式中不使用应截获的自动变量时,都会使用_NSConcreteGlobalBlock
类。
那么,_NSConcreteMallocBlock
又是什么时候使用呢?另外,为什么我们一般再声明Block
成员变量的时候,使用copy
修饰。
配置在全局变量上的Block
,从变量作用域外也可以通过指针安全的访问,但设置在栈上的Block
,如果其所属的变量作用域结束,该Block
就被废弃。由于__block
变量也配置在栈上,同样的,如果其所属的变量作用域结束,则该__block
变量也会被废弃。
Blcoks
提供了将Block
和__block
变量从栈复制到堆上的方法来解决这个问题,将配置在栈上的Block
复制到堆上,这样即使Block
语法记述的变量作用域结束,堆上的Block
还可以继续存在。
而__block
变量用结构体成员变量__forwarding
可以实现无论__block
变量配置在栈上还是堆上时,都能够正确的访问__block
变量。这就是我们在声明属性的时候,为什么使用copy
而不使用strong
。
所以,在调用Block
的copy
的方法时,如下:
而对于__block
变量,如下:
若在1个Block
中使用__block
变量,则当该Block
从栈复制到堆上时,使用到的所有__block
变量也必定配置在栈上,这些__block
变量也全部被从栈复制到堆中,此时Block
持有__block
变量。
而堆上的Block
对_block
的引用,完全符合引用计数管理。
循环引用
使用Block
需要特别注意的就是循环引用。通过刚才的例子,我们知道,如果在Block
中使用附有__strong
修饰符的对象类型自动变量,那么当Block
从栈复制到堆时,该对象为Block
所持有。这样容易引起循环引用。
通常,我们的解决办法是将变量声明成__weak
。