iOS开发中,经常会使用到Block,那Block到底是什么?它的实现方式是什么?通过阅读《Objective-C高级编程:iOS与OS X多线程和内存管理》,会对Block有个更深的了解。

Block是“带有自动变量值的匿名函数”。

本质


首先通过clang,需要对我们的源文件进行转换。例如clang -rewrite-objc BlockTest.c:

//BlockTest.c
int main() {
void(^BlockTest)(void) = ^{
printf("执行Block");
};
BlockTest();
}

转换结果如下:

//BlockTest.cpp
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;
__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 (*)(__block_impl *))((__block_impl *)BlockTest)->FuncPtr)((__block_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 __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;
}
};

改结构体有两个成员变量implDesc,以及一个构造函数。再来看__block_impl结构体声明:

struct __block_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 (*)(__block_impl *))((__block_impl *)BlockTest)->FuncPtr)((__block_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代码块
  • DescFlags和其他。

调用Block,即调用对象的方法。

截获自动变量


int main() {
int age = 10;
void(^BlockTest)(void) = ^{
printf("执行Block%d",age);
};
BlockTest();
}

这里我们增加一个变量,转换后的代码如下:

//Block结构体 不变
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//Block结构体,多了一个age的成员变量,并且构造参数多了一个age
struct __main_block_impl_0 {
struct __block_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;
}
};
//函数中 多了获取age的值
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
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 (*)(__block_impl *))((__block_impl *)BlockTest)->FuncPtr)((__block_impl *)BlockTest);
}

通过以上转换代码,我们能很清楚的看到,__main_block_impl_0结构体多了一个成员变量age,并在初始化的时候将age的值传递过去。在调用FuncPtr的时候,通过__cselfage取出来。

通过以上分析,可以指定,Block只所以能截获自动变量,是因为Block将自动变量作为自己的成员变量,并在初始化的时候赋值,在使用的时候取出来。

__block 修饰符


有时候,我们需要在Block中修改截获的自动变量,这时候就需要在自动变量前加入__block修饰符,否则编译出错。那么,我们也通过clang来看一下转换后的代码:

//BlockTest.c
int main() {
__block int age = 10;
void(^BlockTest)(void) = ^{
age = 20;
printf("修改后的age%d",age);
};
BlockTest();
printf("再次输出age%d",age);
}
//不变
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
//多出一个结构体
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
//多一个__Block_byref_age_0类型的成员变量,构造函数有变化
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_age_0 *age; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__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_age_0 *age = __cself->age; // bound by ref
(age->__forwarding->age) = 20;
printf("修改后的age%d",(age->__forwarding->age));
}
//多了一个静态函数
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->age, (void*)src->age, 8/*BLOCK_FIELD_IS_BYREF*/);
}
//多了一个静态函数
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->age, 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};
int main() {
//多了一句转换
__attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
void(*BlockTest)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
((void (*)(__block_impl *))((__block_impl *)BlockTest)->FuncPtr)((__block_impl *)BlockTest);
printf("再次输出age%d",(age.__forwarding->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 __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
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类,且都设置在栈上。这是因为我们定义的Blockmain函数体中,假如定义在全局,则生成的Block_NSConcreteGlobalBlock,例如:

int(^GlobalBlockTest)(void) = ^int{
printf("GlobalBlockTest");
return 20;
};
int main() {
}

转换后为:

struct __GlobalBlockTest_block_impl_0 {
struct __block_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

所以,在调用Blockcopy的方法时,如下:



而对于__block变量,如下:



若在1个Block中使用__block变量,则当该Block从栈复制到堆上时,使用到的所有__block变量也必定配置在栈上,这些__block变量也全部被从栈复制到堆中,此时Block持有__block变量。



而堆上的Block_block的引用,完全符合引用计数管理。

循环引用


使用Block需要特别注意的就是循环引用。通过刚才的例子,我们知道,如果在Block中使用附有__strong修饰符的对象类型自动变量,那么当Block从栈复制到堆时,该对象为Block所持有。这样容易引起循环引用。



通常,我们的解决办法是将变量声明成__weak