《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
// 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逆向-从汇编代码理解函数调用栈》 - 傻傻木,分析函数调用栈比较详细的一篇文章
坚持原创技术分享,您的支持将鼓励我继续创作!