在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。