《iOS 三问》 -- Block (下) 深入 Block 的实现

在前面的几篇中,我们已经做了充足的准备了,本篇中我们将尝试分析下 Block 的源码,了解下 Block 是怎样实现的,以及前几篇中我们提到的 Block 的几个特性是怎样设计并实现的。

1 Block 的实现

1.1 Block 源码分析

分析 Block 源码,有两种方式,一种是直接分析 libclosure(苹果官方 block 源码,可在网上搜索最新版);另一种是使用 clang 将 OC 代码转为 C++ 源码分析。我个人较喜欢 《iOS-iOS 与 OS X 多线程和内存管理》 一书中的分析方式,基于自己代码改写的 C++ 源码较之直接看 libclosure 更容易理解与入手,所以我们的学习也是主要以此方法入手,适时对比下 block 中的源码,对照着理解。

实际上这里也引出一种我推荐的学习方式,就是 “兼听则明”。大家自己在学习的过程中也可以这样,就是不要光看一个人的著作或偏执于一种方法。比如学习 iOS 某项技术,到网上或书中找多些相关的材料,有条件的将它们打印下来,对比着看对比着学习,最重要是自己手动操作一下,这样理解会更深。

好了,废话不多说,我们下面来进入 Block 源码分析。

我们先写一个简单的示例,然后将之用 clang 命令重写成 C++ 源码,再慢慢进行分析。

简单的 block 示例 - testblock.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{
// 声明一个 block 类型变量 myBlock,指向一个新定义的 Block
void (^myBlock)(void) = ^{
NSLog(@"Hello world!");
};

// 调用此 block
myBlock();

return 0;
}

然后使用下面的 clang 命令将其改写成 C++ 源码

将 OC 源码其改写成 C++ 源码
1
2
3
4
clang -rewrite-objc -fobjc-arc -w testblock.m  # 生成 testblock.cpp

# 如果代码中有使用 ARC 语法 (比如有用到__weak 引用),要用下面的命令
clang -rewrite-objc -fobjc-arc -w -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations xx.m

重写出来的文件我们可以自己细看下感受一下,可以看到,生成出来的源码编译器帮我们自动生成了很多基于 Runtime 的辅助代码。我们在 《面向对象》 一文中说过了 OC Runtime 中面向对象的实现,有此文的基础你再看这些源码会显得亲切一些。

我们看看生成的 main 方法:

生成源码中的 main 方法
1
2
3
4
5
6
7
8
9
10
11
//main 方法
int main(int argc, const char * argv[])
{
// 定义 Block
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

// 调用 Block
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

return 0;
}

这段源码还是挺好看的嘛~与我们 OC 代码中做的一样,就是简单的翻译过来,先定义 Block,再调用 Block。我们不妨先看 block 的定义。

1.2 Block 的定义

Block 的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定义 Block
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

// 我们所定义的 Block 方法体
// @param 参数 __cself 相当于 oc 中消息机制里 msg_send (obj, selector, ..) 中的 obj
// 这里我们发现 C++ 的面向对象实现和 OC 的相似,当调用实例方法时,实质上也是转换成调用元类方法,再将实例作为第一个参数传进去
// 而既然 self 的类型是 __main_block_impl_0,说明 block 的类型实质上应该是 __main_block_impl_0 。
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
NSLog((NSString *)&__NSConstantStringImpl__var_folders__9_98znkr191gzfg1hg7lq_0n8c0000gn_T_test_1b945a_mi_0);
}

// 方法体中要找印的字符串 "hello world"
static __NSConstantStringImpl __NSConstantStringImpl__var_folders__9_98znkr191gzfg1hg7lq_0n8c0000gn_T_test_1b945a_mi_0 __attribute__ ((section ("__DATA, __cfstring"))) = {__CFConstantStringClassReference,0x000007c8,"Hello world!",12};

block 定义部分的代码可以这样看,myBlock 是一个标准的指向结构体的指针,我们知道,OC 面向对象的实现中,对象和类都是基于结构体实现的,所以 myBlock 就可以理解为是我们定义的这个 block 对象的引用!这样一来,定义一个 Block 看起来就像是定义了一个 Block 类,并生成了一个此 Block 的对象一般。我们从下面的简单过程可以看得更清楚:

简化定义代码
1
2
3
4
5
6
7
8
9
10
void (*myBlock)(void)
= ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
│ │ │
▼ ▼ ▼
------------------------------------------------------------------------------------------
__main_block_impl_0 ( __main_block_func_0 , __main_block_desc_0_DATA );
------------------------------------------------------------------------------------------
│ │ │
▼ ▼ ▼
生成 Block 结构体构造函数 block 方法体 Block 的描述信息 (block 大小等信息)

再看后面的具体定义的代码:

定义 block 结构体实例
1
((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

此处有个 __main_block_impl_0,我们在源码中搜索,发现其是一个结构体,上面这行语句就是调用此结构体的构造函数生成了一个 __main_block_impl_0 结构体实例。我们看看其命名 __main_block_impl_0main 是生成此 block 的外层方法,impl 这个我们熟悉,不就是 implement 吗,后面的 0 我们可以猜测,如果在 main 中定义多个 block,就会以此规则用数字序号递增下去 (后面感兴趣的童鞋可以自行生成多个 block 验证下~)。

  • __main_block_impl_0
block 结构体定义
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
//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;
}
};
// 其中,__main_block_impl_0 的 2 个主要成员结构如下:

struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};

//block 结构体描述信息
static struct __main_block_desc_0
{
size_t reserved; // 保留字段
size_t Block_size; //block 大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

此构造函数第一个参数 void *fp,一个方法指针,正是我们定义 Block 中的方法体。第二个参数 __main_block_desc_0_DATA 我们在其后看到定义,如其名是 block 结构体的描述信息 (主要是保存 block 占内存大小)。

我们再看看 __main_block_impl_0 的成员 __block_impl,其中有一个 isa – 这不禁让我们想到我们之前说过的 类对象元类!这也可以让我们猜想,Block 的很多公共行为将会由此 impl 定义,我们在 block 调用中也可以看到,当调用 block 时,实际上就是调用 impl.FuncPtr

回顾 Block 调用语法
1
2
3
4
5
6
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
───────────┬──────────── │ │
▼ │ ▼
myBlock 之所以可向上转型成__block_impl │ 调用实例方法时传入实例 self!
因为其第一个成员就是 impl。 ▼
调用 Block 方法体

我们从 __main_block_impl_0 的结构函数中可以看出端倪:

__main_block_impl_0 结构函数
1
2
3
4
5
6
__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;
}

在构造 __main_block_impl_0 时,同时也初始化了其主要成员 __block_impl,将方法指针赋给其 FuncPtr 成员,同时指定其 isa_NSConcreteStackBlock(这个_NSConcreteStackBlock 后面还会细说).

基于 OC 中类与对象的实现 (《面向对象》), 我们可以猜测,__main_block_impl_0 结构体相当于一个 OC 对象结构体 (objc_object),而其 isa 指向相当于 objc_class。即当我们定义一个 Block 时,底层自动为每个 Block 生成了一个包装类如 __main_block_impl_0, 其继承自 _NSConcreteStackBlock。所以我们也可以说Block 的实质也是一个 Objective-C 对象。(这真有点像 Java 中的匿名内部类~:) )

啰嗦一点我再通俗解释下,也方便自己日后复看些文章快速回忆。相当于我们每次定义一个 Block,编译器其实自动帮我们生成了一个特定的 Block 类,此 Block 类继承自 _NSConcreteStackBlock 或其它预定义好的 Block 基类,然后将 Block 方法体和传入的变量 (后面将要介绍) 赋给经 Block 类。当我们调用 block 时,会自动转成 (__block_impl *) blockObj)->FuncPtr (blockObj) 调用。

我们现在搞清楚了 Block 定义相关的内容,下面我们接着看看 Block 特性之一的访问外层变量是怎样实现的

2 Block 访问外层变量的实现

我们首先看看截取自动变量的实现。

2.1 截取自动变量的实现

与研究 Block 定义的实验方法一样,我们也是先按我们设想写个 Demo,然后用 C++ 将之重写来研究:

截取自动变量示例程序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{
// 定义局部变量 (为了测试的更完整,我增加了一个对象)
int a = 10;
id obj = [[NSObject alloc] init];

// 声明一个 block 类型变量 myBlock,指向一个新定义的 Block
void (^myBlock)(void) = ^{
NSLog(@"Hello world! %d, %@", a, obj);
};

// 调用此 block
myBlock();

return 0;
}

我看先看看 rewrite 后源码的 main 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main(int argc, const char * argv[])
{
// 定义局部变量与生成对象
int a = 10;
id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));

// 定义 Block
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, obj, 570425344));

// 调用 Block
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

return 0;
}
  1. 第一部分的定义局部变量我们没有问题,生成 obj 对象也没有问题。
  2. 看了定义 Block 这里我们终于真像大白了!就如我在上篇中说的一样,虽然在 Block 中我们可以自由地使用外层变量,但实际上,编译器帮我们做了传值!!如下示例:
  3. 调用 Block 这里我们上节已经学习过了。
自动变量传值示意:
1
2
3
4
5
void (*myBlock)(void)
= ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, obj, 570425344));
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
__main_block_impl_0 ( __main_block_func_0 , &__main_block_desc_0_DATA, a, obj , flags)

既然在我们定义 Block 之时编译器帮我们做了传值,那 Block 内部是怎样对待它们的呢?知道了 Block 定义之后,我们可以直接看 Block 包装类 __main_block_impl_0:

Block 包装类__main_block_impl_0
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
43
44
// Block 包装类
struct __main_block_impl_0
{
struct __block_impl impl;
struct __main_block_desc_0* Desc;

// 自动变量!!
int a;
id obj;

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, id _obj, int flags=0) : a(_a), obj(_obj) {
impl.isa = &_NSConcreteStackBlock; // 表示分配在栈上
impl.Flags = flags;
impl.FuncPtr = fp; // Block 函数体指针
Desc = desc;
}
};

//block 方法体
static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
int a = __cself->a; // bound by copy
id obj = __cself->obj; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders__9_98znkr191gzfg1hg7lq_0n8c0000gn_T_testWithBasicVariable_c33981_mi_0, a, obj);
}

//block 描述信息
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};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 3 /*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src)
{
_Block_object_dispose((void*)src->obj, 3 /*BLOCK_FIELD_IS_OBJECT*/);
}

从上面源码中看出,Block 包装类 __main_block_impl_0 自动构造了 2 个成员变量,分别对应着我们传入 block 的自动变量!然后在其构造函数中在定义处传了进去。这里我们就彻底明白自动截取变量的实现玄机了!所以我们说截取的是瞬时值,因为其实现就是在 block 定义之时复制到 block 内部的~ 那么,在 Block 中不可修改截取的自动变量,当然就是编译器做的限制咯。

然后我们可以留意下 Block 方法体中,先是对传入的 cself (也就是 Block 对象本身) 取出其中生成的截取变量值,然后再继承函数体操作的。

而此时 __main_block_desc_0 这个结构体,多出了两个方法

下面小测试我留了道思考题,大家看看对传进来的 OC 对象会有什么遭遇,为什么?

截取自动变量测试题
1
2
3
4
5
6
7
NSMutableArray *array = [@[@"1", @"2", @"3"] mutableCopy];
// 声明一个 block 类型变量 myBlock,指向一个新定义的 Block
void (^myBlock)(void) = ^{
NSLog (@"array: %@", array); //block 执行时,这句的输出是什么
};
[array removeLastObject]; // 在调用 block 之前,于 block 外部操作 array
myBlock (); // 调用此 block

这里截取的变量会变吗 —————————————— 答案是会的,因为对于对象而言,截取的只是其引用。

2.2 __block 变量的实现

截取的自动变量实现虽然简单,但是其缺点为不能对之进行修改。我们说过,__block 变量可以解决这种问题,老规矩,我们写 Demo 实验看看:

__block 变量 Demo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[])
{
// 定义 block 变量
__block id obj = [[NSObject alloc] init];
__block int a = 10;

// 声明一个 block 类型变量 myBlock,指向一个新定义的 Block
void (^myBlock)(void) = ^{
obj = [[NSObject alloc] init];
a = 20;
};

// 调用此 block
myBlock();

return 0;
}

我们看看 rewrite 之后的 main 方法,我们惊奇地发现,增加了 __block 修饰符后变量的定义都变了:

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
int main(int argc, const char * argv[])
{
// 对应于:__block id obj = [[NSObject alloc] init];
__attribute__((__blocks__(byref))) __Block_byref_obj_0 obj =
{
(void*)0,
(__Block_byref_obj_0 *)&obj,
33554432,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))
};

// 对应于:__block int a = 10;
__attribute__((__blocks__(byref))) __Block_byref_a_1 a =
{
(void*)0,
(__Block_byref_a_1 *)&a,
0,
sizeof(__Block_byref_a_1),
10
};

// Block 定义
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_obj_0 *)&obj, (__Block_byref_a_1 *)&a, 570425344));

// 调用 block 方法
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);

return 0;
}

我们发现,生成的代码对 __block 修饰的变量进行了封装,比如使用 __Block_byref_obj_0 这个类型来封装 __block id obj, 使用 __Block_byref_a_1 来封装 __block int a. 我把这个封装结构体代码列出来,从其 isa 看出它也是一个类型。然后我们对照 main 里的该结构体的生成代码就可以发现其中的关系:

__block 变量封装类
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
43
struct __Block_byref_obj_0 {
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
id obj; // 封装类中的
};

struct __Block_byref_a_1 {
void *__isa;
__Block_byref_a_1 *__forwarding;
int __flags;
int __size;
int a;
};

// 我们举例看看 main 函数中 obj 对象的定义,我在注释中给出其对应的结构体成员名
__attribute__((__blocks__(byref))) __Block_byref_obj_0 obj =
{
(void*)0, // __isa
(__Block_byref_obj_0 *)&obj, //__Block_byref_a_1 *__forwarding 指向自己!
33554432, // __flags
sizeof(__Block_byref_obj_0), // __size
__Block_byref_id_object_copy_131, //copy & dispose 方法
__Block_byref_id_object_dispose_131,
((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))
};

┌──────────────┐
│ │
│ │
┌────────────────────────▼──────┐ │
│__isa │ │
│__forwarding ──────────────────┼───────┘
│__flags │
│__size │
│__Block_byref_id_object_copy │
│__Block_byref_id_object_dispose│
│id obj │
└───────────────────────────────┘
( __Block_byref_obj_0 图解 )

乍一看这个自动生成的变量封装类有点奇怪,里面有个指向结构体本身的 __Block_byref_a_1 *__forwarding 指针,还有两个函数 copy、dispose, 它们各自有什么用呢?我们下面就来说变量封装类的作用,其实正因为有了这种机制,才使用__block 变量的特性得以实现:

  1. 使得外部变量可以在 Block 内修改;
  2. 变量超出作用域后

2.2.1 修改外部变量的实现

我们看 Block 方法体源码:

Block 方法体源码
1
2
3
4
5
6
7
8
9
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_obj_0 *obj = __cself->obj; // 先从 block 对象中取出对应的__Block_byref 结构体
__Block_byref_a_1 *a = __cself->a; // 同上

// 修改__block 变量时,从__Block_byref 结构体中取出变量进行修改。
// 因为在 Block 外访问__block 变量时,也是访问此同一结构体,因此,就达到了修改外部变量的作用
(obj->__forwarding->obj) = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
(a->__forwarding->a) = 20;
}

可见,正因为将变量封装成__Block_byref_xx 结构,使其不必像截取自动变量一样在 block 定义时复制到 block 内部。而是类似 (__Block_byref_obj_0 *)&obj 这样传递__Block_byref_xx 结构的引用。

这样,在方法体中对外部__block 变量的修改就变成了对传入的__Block_byref_xx 结构体中相应的变量成员的修改。在 Block 外访问__block 变量时,也是访问此同一结构体,因此,就达到了修改外部变量的作用。

至此,我们已经了解了 Block、自动变量截取、__block 变量的底层是怎样实现的了。不过现在还有一些疑问,比如

  • __forwarding 指针有什么用?
  • block 结构体中的 copy、dispose 方法又有何用?
  • 我们平时说的 block 循环引用是为什么会产生?

下面我们分析 Block 的存储域问题后,这些问题将一一解开。

3 Block 存储域与堆拷贝

我们前面分析过,Block 其实也是一个 Objetive-C 对象,我们每定义一个 Block 其实对应生成了一个 Block 结构体,而其 isa 指向一个叫 NSConcreteStackBlock 的结构。这个结构说明了 block 类的类型!从字面上可以看出,这是一个分配在栈 (stack) 上的 block。

3.1 Block 存储域

其实,Block 共有 3 种类型,分别代表其存储在不同的存储区中:

类 | 对应的存储域 |
———————-|—————|–
NSConcreteStackBlock | 栈区 |
NSConcreteGlobalBlock | 数据区 (.data) |
NSConcreteMallocBlock | 堆区 |

存储在不同区域的 Block 有什么区别呢?我们不妨先构造场景看一看

block 存储域示例
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
// 定义一个全局 Block
void (^globalBlock)(void) = ^{ printf("globalBlock\n", ); };

int main(int argc, const char * argv[]) {
int a = 1;
static int b = 2;

void(^localBlock)(void) = ^(void) {
NSLog (@"访问局部变量 localBlock % d", a);
};
void(^staticBlock)(void) = ^(void) {
NSLog (@"访问静态变量 staticBlock % d", b);
};
void(^paramBlock)(NSString *) = ^(NSString *inputStr) {
NSLog (@"只访问入参 paramBlock %@", inputStr);
};

globalBlock();
localBlock();
staticBlock();
paramBlock(@"c");

NSLog(@"globalBlock:%@", globalBlock);
NSLog(@"localBlock:%@", localBlock);
NSLog(@"staticBlock:%@", staticBlock);
NSLog(@"paramBlock:%@", paramBlock);
NSLog(@"stackBlock:%@", ^{
NSLog (@"访问局部变量 stackBlock % d", a);
});
}

==> output:
globalBlock
访问局部变量 localBlock 1
访问静态变量 staticBlock 2
只访问入参 paramBlock hello
globalBlock:<__NSGlobalBlock__: 0x10b7f9138>
localBlock:<__NSMallocBlock__: 0x60000270e100>
staticBlock:<__NSGlobalBlock__: 0x10b7f9198>
paramBlock:<__NSGlobalBlock__: 0x10b7f91d8>
stackBlock:<__NSStackBlock__: 0x7ffee4406520>
  • NSGlobalBlock

我们看到,全局定义的 Block访问静态变量的 Block没有访问外部变量 的 Block 都是 __NSGlobalBlock__ 类型!也就是说,当 Block 不需要捕获外部变量 (除了静态变量与局部变量) 时,Block 会被定义为全局 Block。这是编译器的一个优化,因为全局 Block 不存在对自动变量的截取、封装等操作,肯定是机制最常用也最简单的 Block,这种 Block 在整个程序中只需要一个实例,因此直接将其置于数据区最合适 (因为全局变量也在这个区域)。

  • NSStackBlock

虽然这些 Block 除了全局定义的 Block 外都是在函数中定义的,但是我们看,只有最后一个在 NSLog 方法内定义并直接访问的 Block 为我们最熟悉的 __NSStackBlock__。此类 Block 存储在栈区中,上一篇我们对栈区有了深入的了解,我们知道,当函数调用结束,它就会随着栈帧的销毁而销毁。

  • NSMallocBlock

示例中的 localBlock 让我们大跌眼镜,我们并没有对 block 做任何 copy 的动作,也没有指定在堆上为之分配内存,为什么这个 Block 会被分配到堆上呢?原来在 ARC 中,赋值操作会自动对指向的 Block 调一次 copy 方法。我们看下图:

对不同的 block 调用 copy 方法
1
2
3
__NSStackBlock__    --> 执行 copy 后 --> 从栈复制到堆并变成__NSMallocBlock__
__NSGlobalBlock__ --> 执行 copy 后 --> 什么也不做
__NSMallocBlock__ --> 执行 copy 后 --> 引用计数增加

也就是说,本来 localBlock 在我们源码分析时应该是 stackBlock,但因为自动执行了 copy,将之从栈复制到堆并变成 __NSMallocBlock__

这个自动调用 copy 的示例也解释了为什么我们平时将 block 作为 property 时用 strong 和 copy 其实是一样的。

知道了 Block 的存储域后我们进一步发问,为什么要如此设计呢?将 Block 拷贝到堆上有什么好处?下面我们就来深入 Block 的堆拷贝。

3.2 Block 的堆拷贝

Block 堆拷贝的第一个作用就是使得 Block 超出变量作用域后仍然可以使用

3.2.1 Block 超出变量作用域后仍然可以使用

我们来看一下这个例子:

Block 的堆拷贝示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
AddBlock generateAddBlock(int inc)
{
return ^(int a, int b) {
printf("%d", a + b + inc);
};
}

int main(int argc, const char * argv[])
{
AddBlock myAdd = generateAddBlock(2);
myAdd(1, 2); // output ==> 5

return 0;
}

在上面的例子中,block 被作为函数的返回值,我们在学习内存管理时了解过,这是一个经典的返回内部对象问题 (因为 Block 本质上也是 OC 对象嘛)。因为这个成生的 Block 是个 __NSStackBlock__,联系我们上一篇学的函数栈管理我们知道如果 Block 分配在栈上的函数一返回其就被回收了,那我们为什么在 main 中仍然可以调用之呢?

答案就是 Block 的堆拷贝!将 Block 从栈上拷贝到堆上变成 __NSMallocBlock__ 后,就能解决问题,也就是说,函数返回的是被拷贝在堆上的 Block。对应于 ARC 的转换代码应该如下:

参照《iOS-iOS 与 OSX 多线程和内存管理》一书中对应于 ARC 的转换代码
1
2
3
4
5
6
7
8
9
AddBlock generateAddBlock(int inc)
{
// 这里 __generateAddBlock_block_impl_0 为自动生成的 Block 类结构体
AddBlock tmp = ((void (*)(int, int))&__generateAddBlock_block_impl_0((void *)__generateAddBlock_block_func_0, &__generateAddBlock_block_desc_0_DATA, inc));

tmp = _Block_copy(tmp);

return objc_autoreleaseReturnValue(tmp);
}

其中 tmp = _Block_copy (tmp); 将 Block 复制到堆上,复制到堆上后,block 终于更像一个 OC 对象了,可以将其注册到 autoReleasePool 中并返回。实际上,上面我们说过,在 ARC 中,只要对 Block 有赋值,就会发生堆拷贝。我们不妨深入 _Block_copy 源码去一窥究竟:

libclosure-73 中_Block_copy 源码,为便于阅读稍有改动
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
43
44
45
46
47
48
49
50
51
52
//block 信息枚举
enum {
BLOCK_DEALLOCATING = (0x0001), //
BLOCK_REFCOUNT_MASK = (0xfffe), // 用来标识栈 Block
BLOCK_NEEDS_FREE = (1 << 24), // 用来标识堆 Block
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // 表示 blockDESC 中是否含有 copy_dispose 方法 (后面会介绍)
BLOCK_IS_GLOBAL = (1 << 28), // 是否为全局 Block
};

void *_Block_copy(const void *arg) {
// 参数校验
struct Block_layout *aBlock;
if (!arg) return NULL;

// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) { // BLOCK_NEEDS_FREE 堆 Block
// 如果是堆 Block,增加引用计数
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) { // BLOCK_IS_GLOBAL 全局 Block 直接返回
return aBlock;
}
else { // 如果是栈 Block,进行堆拷贝!
// 1. 在堆中为此 Block 分配内存
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;

// 2. 根据 block 中的 descriptor->size 的大小,将 block 从栈拷贝到堆空间中
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first

// 3. 置标志位
// 3.1 把栈标识去掉
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
// 3.2 这里后面有列出,实际上为调用 block desc 中的 copy 方法
_Block_call_copy_helper(result, aBlock);
// 3.3 将 Block 置为堆 Block
result->isa = _NSConcreteMallocBlock;
return result;
}
}

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;

(*desc->copy)(result, aBlock); // do fixup
}

其实上面的 _Block_copy 就正对着我们之前列出的这张表:

1
2
3
__NSStackBlock__    --> 执行 copy 后 --> 从栈复制到堆并变成__NSMallocBlock__
__NSGlobalBlock__ --> 执行 copy 后 --> 什么也不做
__NSMallocBlock__ --> 执行 copy 后 --> 引用计数增加

3.2.2 __block 变量随着 block 一块堆拷贝

上面 _Block_copy 源码中我们注意到还有一点没有说,就是里面的 _Block_call_copy_helper (result, aBlock);。它调用了 Block 类的 Desc 中的 copy 方法,也是我们之前遗留的一个问题,这个 copy 是用来干嘛的呢?我们把他找出来看看(在 rewrite 的 C++ 源码中):

我们以之前介绍__block 变量定义的示例来说,其中定义了两个__block 变量 int aid obj:

深入 DESC 的 copy 方法
1
2
3
4
5
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src)
{
_Block_object_assign((void*)&dst->obj, (void*)src->obj, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}

其中调用了 _Block_object_assign 方法,此方法在 rewrite 源码中被定义为外部符号,主要作用就是当 block 发生拷贝时对其内部成员的或拷贝或持有的操作,所幸我们可以在 libclosure 中找到源码:

libclosure-73 中_Block_object_assign 方法追踪
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 此源码苹果本身的注释就相当的详细,老实说,真可以作为我们平时写代码时注释的典范;
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void**dest = (const void**)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT: // 要复制的如果是截取的对象
// 这里示例给的很清楚的,不知道其公开的源码是不是就是内部原封不动的源码,
// 像这类 Switch 我们平时很多时就靠维护代码的人自己跟踪枚举去看意义了。
// 而苹果开发这种简洁又明了的说明的注释这是值得我们学习的。
/*******
id object = ...;
[^{ object; } copy];
********/

// 要复制的如果是截取的对象,对其做 retain 操作,即增加其引用计数
_Block_retain_object(object);
*dest = object;
break;

case BLOCK_FIELD_IS_BLOCK: // 要复制的内部成员如果也是一个 block,那就再对其调用_Block_copy (我们上面介绍过了)
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/

*dest = _Block_copy(object);
break;

case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
case BLOCK_FIELD_IS_BYREF: // 这是我们要找的 __block 变量
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/

//__block 变量变量如果在栈上,也要对其一并进行堆拷贝!
*dest = _Block_byref_copy(object);
break;

case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/

*dest = object;
break;

case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/

*dest = object;
break;

default:
break;
}
}

从上面源码我们知道,编译器在对 Block 进行堆拷贝时,会将其包含的__block 变量一并也进行堆拷贝!

libclosure-73 中_Block_byref_copy 源码
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
43
44
45
46
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;

if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
// 1. 为__block 变量分配堆究竟
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
// 2. 为堆上的__block 变量置标志位
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;

// 如果__block 变量有 copy/dispose 方法,也要一并调用之
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;

if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}

(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
// 对于已做过堆拷贝的__block 变量 (多个 Block 共用),只需简单地增加其引用计数
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}

return src->forwarding;
}

那么这个 forwarding 有什么用呢?我们需要画图解释!

栈拷贝图示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

┌────────────────────────┐ ┌─────────────────────────┐
│ │ forwarding 拷贝后指堆上 ┌────────┐ │
│ ┌─┼────────────┼─────────────▶│ │ │
│ ┌───────────────┐ │ │ │ ┌────────▼──────┐ │ │
│ │ __block var │ │ │ │ │ __block var │ │ │
┌──┼───▶│ │ │■■■■■ 堆拷贝 ■■■■▶ │ │◀┼─┼───┐
│ │ │ forwarding ──┼─┘ │ │ │ forwarding ──┼─┘ │ │
│ │ └───────────────┘ │ │ └───────────────┘ │ │
│ │ │ │ │ │
│ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │
│ │ │ │ │ │ │ │ │ │
├──┼────│ Block1 │ ■■■■■ 堆拷贝 ■■■■▶ │ Block1 ├────┼───┤ 持有 (block 销毁后将释放)
│ │ │ │ │ │ │ │ │ │
│ │ └──────────────┘ │ │ └──────────────┘ │ │
│ │ │ │ │ │
│ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │
│ │ │ Block2 │ │ │ │ Block2 │ │ │
└──┼────│ │ ■■■■■ 堆拷贝 ■■■■▶ │ │────┼───┘
│ │ │ │ │ │ │ │
│ └──────────────┘ │ │ └──────────────┘ │
│ │ │ │
└────────────────────────┘ └─────────────────────────┘

当 Block 堆拷贝后,其__block 变量随着一起拷贝 (Block_byref 结构),这样做当然也是为了避免栈上的变量随着栈帧销毁释放的设计。现在我们就知道 forwarding 的设计多巧妙了,它使得__block 拷贝到堆上后原来栈上的 Block 与堆上的 Block 仍可使用,且指向的__block 变量是同一个,这样的小设计可以使代码变得更简单。

3.3 堆拷贝相关问题

堆拷贝可以优雅地解决 Block 与__block 变量的作用域问题,也会带来一些新的要注意的问题,了解这些点有助于我们对 Block 的运用。

3.3.1 Block 持有对象

我们前面就提到过,当截取的自动变量是对象时,会在 block_impl 中生成一个指向此对象的引用。在 “libclosure-73 中_Block_object_assign 方法追踪” 中我们看到,发生堆拷贝时,又对其持有的对象做了 retain 操作。

我们看看下面这个示例:

Block 持有对象示例
1
2
3
4
5
6
7
8
9
10
11
12
13
typedef void (^blk_t)(id);

blk_t blk;
{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:10];
blk = ^(id obj){
[array addObject:obj];
NSLog(@"After add object, array count is %ld", array.count);
};
} // 局部代码块结果
blk([NSObject new]); // After add object, array count is 1
blk([NSObject new]); // After add object, array count is 2
blk([NSObject new]); // After add object, array count is 3

这里我们用了一个局部代码块,在代码块中定义了一个局部的 array 对象,正常来说,当代码块执行完毕,array 出了其作用域后,ARC 会将其引用计数减至 0,释放此数组对象。但是从 block 的运行结果来看,此 array 并没有销毁。

我们 rewrite 上面代码观察,发现在 block_impl 中增加了引用此对象的成员:

block 结构体定义
1
2
3
4
5
6
7
8
9
10
struct __main_block_impl_0
{
struct __block_impl impl;
struct __main_block_desc_0* Desc;

// 自动变量!!
NSMutableArray *__strong array;

//__main_block_impl_0 构造函数,省略 ...
};

我这里就不把全部代码罗列出来了,和我们之前已经看过的差不多,就是在 block 定义处,将 array 引用传给了 block 的 NSMutableArray *__strong array; 成员。

因此,我们发现,当往 Block 中 “传” 对象的时候 (这里说的传,其实就是访问外部对象!),实际上 Block 将其转化成了持有该对象!这就是我们要注意的地方了,也就是在 Block 内访问外部对象时要格外注意,因为这将导致 Block 持有该对象。

现在我们终于知道为什么 Block 会持有外部对象了,那这里要注意什么问题呢?主要就是我们常说的 Block 循环引用问题。

3.3.2 Block 循环引用问题

其实有了上一小节的示例,常见的循环引用问题就很好解了,比如下面这个常见的例子

Block 循环引用示例
1
2
3
4
// 模拟一个点击按钮事件,在事件中加载数据
[self.button onButtonPress:^{
[self loadData];
}];

这会导致一个怎样的循环引用出现呢?

我们先分析下,首先,self 持有 button,这个没问题。 然后给 Button 设置了这个 Block 后,发生 Block 的堆拷贝,然后 Button 持有此 Block。最后我们看下 Block 内访问到了 self 对象,顾 Block 也会持有 self。我们作出持有关系图:

循环引用示意图
1
2
3
4
5
6
┌────┐         ┌─────────┐          ┌───────┐
│self│─retain─▶│ button ├─retain──▶│ block │
└────┘ └─────────┘ └───────┘
▲ │
│ │
└───────────retain──────────────────┘

如何打破此循环引用呢?众所周知,就是使用__weak 引用:

使用__weak 解决循环引用
1
2
3
4
5
6
7
8
9
10
11
__weak typeof(self) weakSelf = self;
[self.button onButtonPress:^{
[weakSelf loadData]; // 在 Block 中使用 weak 引用
}];

// 解决循环引用示意图:
// ┌────┐ ┌─────────┐ ┌───────┐
// │self│─retain─▶│ button ├─retain──▶| block │
// └────┘ └─────────┘ └───────┘
// ▲ │
// └ ─ ─ ─ weak reference─ ─ ─ ─ ─ ─ ─ ┘

3.3.3 Block 中的__weak 引用

我们最后看看 Block 中的__weak 引用是啥样:

Block 中使用引部__weak 引用的 OC 源码
1
2
3
4
5
6
7
8
9
NSArray* array = [[NSArray alloc] init];
__weak NSArray* w_array = array;
__block __weak NSArray* bw_array = array;

// 声明一个 block 类型变量 myBlock,指向一个新定义的 Block
void (^myBlock)(void) = ^{
NSLog(@"%@", w_array);
NSLog(@"%@", bw_array);
};

我们将之 rewrite, 注意因为这里用到的 weak,所以要用命令:

1
clang -rewrite-objc -fobjc-arc -w -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations xx.m

我们单看生成了 Block 类结构体就知道了:

Block 中使用引部__weak 引用的 C++ 源码
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
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;

NSArray *__weak w_array; // Block 内生成的也是 Weak 引用
__Block_byref_bw_array_0 *bw_array; // by ref

__main_block_impl_0(void *fp,
struct __main_block_desc_0 *desc,
NSArray *__weak _w_array,
__Block_byref_bw_array_0 *_bw_array,
int flags=0) : w_array(_w_array), bw_array(_bw_array->__forwarding)
{
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

struct __Block_byref_bw_array_0 {
void *__isa;
__Block_byref_bw_array_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);

NSArray *__weak bw_array; //__block 变量结构体中生成的也是 weak 引用
};

从上面的源码我们也可以看出,__block __weak__weak 的效果其实是一样的,而加上 __block 后还会增加额外的结构体与堆拷贝,所以为了解决循环引用问题,使用 __weak 就可以了。

4 小结

至此,我们对 Objective-C 的 Block 的探索就暂告一段落了。

在这一系列中,我们了解了 Block 的由来,它是 OC 中对于闭包的一种实现。而闭包又是一种优秀的技术,主要来说其语法简单高效,可以轻松地实现方法的传递,而且闭包还可以方便地访问外部变量,这种灵活高效的技术也提高了我们平时工作的效率。

然后我们还了学习了关于函数栈的知识,巩固了我们知识面的同时,也为了解 Block 时涉及的各种栈区啊、堆区啊等作好了知识储备。

最后,也就是本篇中,我们深入探索了 Block 的实现。知道了 Block 在底层中是怎样定义的,它其实是一个 Objective-C 对象!接着我们又了解到 Block 是怎样访问外部变量的,截取自动变量是怎样实现的,__block 变量是怎么被封装成__block_byref 结构。最后,我们共同学习了 Block 的堆拷贝技术,终于明白了为什么 Block 可以自由传递而不会随着栈区销毁、Block 为什么可以持有其内部访问的对象、以及搞明白了 Block 循环引用是怎样造成的又是如果解决的。

Block 作为多线程技术中重要的结构,可说是重中之重,全面地认识 Block 为后面我们继续深入学习 iOS 多线程编程做了良好的铺垫。

5 引用

  1. 《iOS-iOS 与 OS X 多线程和内存管理》 我学习本系列的主要参考书,写得非常好,真正的深入浅出。
  2. 《谈 Objective-C block 的实现 - 唐巧》 唐巧一篇早期的文章,写得不错的。
  3. 源码:libclosure-73,objc4-709
  4. 《iOS逆向-从汇编代码理解函数调用栈》 - 傻傻木,分析函数调用栈比较详细的一篇文章