说说Block那些事儿(一)

###什么是 Block?
Block 是 C 语言的扩充功能,《Objective-C 高级编程 iOS 与 OSX 多线程和内存管理》一书中是这么描述的:

Block 是一种带有自动变量值的匿名函数

匿名就是没有名字,自动变量值其实就是局部变量。而 block 最大的特点就是可以截获函数以外的自动变量的值。而传统的函数只能通过传参或者使用全局变量或者静态变量才可以访问。

那这么设计有什么好处呢?最大的好处是让代码更紧凑,同时由于 Block 可以截获外部变量的值,方便了对象间的通信。

###Block的实现原理

我们可以通过 clang -rewrite-objc 命令,来对 block 进行深入分析,这里分四种情况:

block 表达式中不引用任何外部变量

main.m

1
2
3
4
5
6
7
8
/************* Objective-C 源码 *************/
#include <stdio.h>

int main(int argc, const char * argv[])
{
^{ printf("Hello, World!\n"); } ();
return 0;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/************* 使用 clang 翻译后如下 *************/
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("Hello, World!\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[])
{
(void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
return 0;
}

下面进行一下说明:

1
2
3
4
5
6
7
8
// __block_impl 是 block 实现的结构体

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
  • isa 指向实例对象,表明 block 也是一个 Objective-C 对象。block 有三种类型:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,isa 对应有三种值,后边再说。
  • Flags,按位表示一些block的附加信息
  • Reserved,保留变量
  • FuncPtr,函数指针,指向具体的block实现的函数调用地址
1
2
3
4
5
6
7
8
9
10
11
12
// __main_block_impl_0 是 block 实现的入口

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;
}
};

该结构体在 __block_impl 结构体基础上,追加了 struct __main_block_desc_0* Desc 变量,以及构造函数,其中__main_block_desc_0 是对 block 的附加描述,包括 block 结构体的大小和保留信息。

1
2
3
4
5
6
// __main_block_desc_0 是 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) };

此处已定义了一个该结构体类型的变量 __main_block_desc_0_DATA,并进行了初始化。

####block 表达式截获外部的普通变量

main.m

1
2
3
4
5
6
7
8
9
10
11
12
/************* Objective-C 源码 *************/
#include <stdio.h>

int main(int argc, const char * argv[]) {
int a = 1;
void (^block)(void) = ^{
printf("%d\n", a);
};
block();

return 0;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/************* 使用 clang 翻译后如下 *************/
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("%d\n", a);
}

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[])
{
int a = 1;
void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);

return 0;
}

这个代码跟上一个很相似,唯一不同的是 struct __main_block_impl_0 结构体在最后追加了一个变量 int a; 通过构造函数传进去,注意:这个是传值哦,这也就说明了为什么不能在 block 表达式中对截获的外部变量进行写操作,这跟 git 里的分支概念差不多,外部自动变量处于主分支,block 表达式在截获外部变量的时候就类似在主分支创建了一个子分支,子分支当然不可以修改主分支的内容,而且主分支里的自动变量即使重新赋值,也不会对 block 表达式中的子分支造成影响。

####block 表达式截获外部的 static 变量

main.m

1
2
3
4
5
6
7
8
9
10
11
12
/************* Objective-C 源码 *************/
#include <stdio.h>

int main(int argc, const char * argv[]) {

static int a = 1;
void (^block)(void) = ^{
printf("%d\n", a);
};
block();
return 0;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/************* 使用 clang 翻译后如下 *************/
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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *a = __cself->a; // bound by copy

printf("%d\n", (*a));
}

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[]) {

static int a = 1;
void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &a);
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}

这个跟上一个最大的不同是:__main_block_impl_0 里的 int a 变成了 int *a, 由整型变成了整型指针类型,注意:这个不再是传值,而是传地址,这说明对应 static 修饰的自动变量值在被 block 截获之后仍可以与外部自动变量保持同步,因为它们的地址是同一个。

####block 表达式截获外部的 __block 变量

main.m

1
2
3
4
5
6
7
8
9
10
11
12
13
/************* Objective-C 源码 *************/
#include <stdio.h>

int main()
{
__block int i = 1;
void (^block)(void) = ^{
printf("%d\n", i);
};
i = 2;
block();
return 0;
}

main.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/************* 使用 clang 翻译后如下 *************/
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__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_i_0 *i = __cself->i; // bound by ref

printf("%d\n", (i->__forwarding->i));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 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(int argc, const char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1};
void (*block)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344);
(i.__forwarding->i) = 2;
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}

这个跟上一个最大的不同是:__block 修饰的自动变量被转换成了一个结构体 __Block_byref_i_0,同时 __main_block_impl_0 里的 int a 变成了 __Block_byref_i_0 *i; 由整型变成了结构体指针类型,注意:这个也是传址,这说明被 __block 修饰的自动变量值被 block 截获之后也可以与外部变量保持同步,但是还是有个疑问,就是 __Block_byref_i_0 结构体到底为什么这么设计?

1
2
3
4
5
6
7
8
9
// 存储 __block 外部变量的结构体
struct __Block_byref_intValue_0
{
void *__isa; // 对象指针
__Block_byref_intValue_0 *__forwarding; // 指向自己的指针,大有玄机
int __flags; // 标志位变量
int __size; // 结构体大小
int intValue; // 外部变量
};

特别是里边的 __Block_byref_intValue_0 *__forwarding; 这个到底是干嘛用的?在解释这个之前得先说说 block 的内存管理,这里也是分两种情况来说:

Block的内存管理

Block的三种类型在内存中的分布如图:

image

_NSConcreteGlobalBlock

1 当 block 字面量写在全局作用域时,即为 global block;

2 当 block 字面量不获取任何外部变量时,即为 global block;

除了上述描述的两种情况,其他形式创建的 block 均为 stack block。

_NSConcreteStackBlock

处于内存的栈区,如果其变量作用域结束,这个 block 就被废弃,block 上的 __block 变量也同样会被废弃。

image

为了解决这个问题,block 提供了 copy 的功能,将 block 和 __block 变量从栈拷贝到堆,就是下面要说的 _NSConcreteMallocBlock。

_NSConcreteMallocBlock

当 block 从栈拷贝到堆后,当栈上变量作用域结束时,仍然可以继续使用 block

image

此时,堆上的 block 类型为 _NSConcreteMallocBlock,所以会将 _NSConcreteMallocBlock 写入 isa

1
impl.isa = &_NSConcreteMallocBlock;

__block 变量的内存管理

当 block 从栈内存被拷贝到堆内存时,__block 变量的变化如图:

image

当 block 被废弃时,__block 变量被释放,如图:

image

下面重点说一下 __forwarding

前文已经说过,当 block 从栈被拷贝到堆时,__forwarding 指针变量也会指向堆区的结构体。但是为什么要这么做呢?为什么要让原本指向栈区的结构体的指针,去指向堆区的结构体呢?看起来匪夷所思,实则原因很简单,要从 __forwarding 产生的缘由说起。想想起初为什么要给 block 添加 copy 的功能,就是因为 block 获取了局部变量,当要在其他地方(超出局部变量作用范围)使用这个 block 的时候,由于访问局部变量异常,导致程序崩溃。为了解决这个问题,就给 block 添加了 copy 功能。在将 block 拷贝到堆上的同时,将 __forwarding 指针指向堆上结构体。后面如果要想使用 __block 变量,只要通过 __forwarding 访问堆上变量,就不会出现程序崩溃了,如图:

image

ARC 下的 block

在开启 ARC 时,大部分情况下编译器通常会将创建在栈上的 block 自动拷贝到堆上(潜台词是 MRC 没有这个褔利),只有当
block 作为方法或函数的参数传递时,编译器不会自动调用 copy 方法;剩下的这几种情况调用者就不需再手动拷贝

  • 当 block 作为函数返回值返回时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 被赋值给 __strong id 类型的对象或 block 的成员变量时,编译器自动将 block 作为 _Block_copy 函数,效果等同于 block 直接调用 copy 方法;
  • 当 block 作为参数被传入方法名带有 usingBlock 的 Cocoa Framework 方法或 GCD 的 API 时。这些方法会在内部对传递进来的 block 调用 copy 或 _Block_copy 拷贝;

下面分别举个例子,自动拷贝的:

1
2
3
4
5
6
/************ ARC下编译器自动拷贝block ************/
typedef int (^blk_t)(int);
blk_t func(int rate)
{
return ^(int count){return rate * count;};
}

上面的 block 获取了外部变量,所以是创建在栈上,当 func 函数返回给调用者时,脱离了局部变量 rate 的作用范围,如果调用者使用这个 block 就会出问题。那 ARC 开启的情况呢?运行这个 block 一切正常。和我们的预期结果不一样,ARC 到底给 block 施了什么魔法?我们将上面的代码翻译下

1
2
3
4
5
6
blk_t func(int rate)
{

blk_t tmp = &__func_block_impl_0(__func_block_func_0, &__func_block_desc_0_DATA, rate);
tmp = objc_retainBlock(tmp);
return objc_autoreleaseReturnValue(tmp);
}

由于 block 字面量是创建在栈内存,通过 objc_retainBlock() 函数拷贝到堆内存,让 tmp 重新指向堆上的 block,然后将 tmp 所指的堆上的 block 作为一个 Objective-C 对象放入 autoreleasepool 里面,从而保证了返回后的 block 仍然可以正确执行。

下面举一个需要手动拷贝的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/************ ARC下编译器手动拷贝block ************/
id getBlockArray()
{
int val = 10;
return [[NSArray alloc] initWithObjects:
^{NSLog(@"blk0:%d", val);},
^{NSLog(@"blk1:%d", val);}, nil];
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray *array = getBlockArray();
void (^b)() = array[0];
b();
}
return 0;
}

运行这段代码就直接挂了,因为 block 没有被 copy 到堆内存,超出变量作用域就释放了。

好了,先说到这里吧,下篇说一说实际开发过程中 block 中的一些坑以及填坑方法。