Objective-C Block

前言

本文内容主要来自 Pro Multithreading and Memory Management for iOS and OS X with ARC, Grand Central Dispatch, and Blocks 这本书,参考这篇文章

Block 是语言级别的语法,是 C 语言的扩展。Block 可以解释为“包含了局部变量的匿名函数(anonymous functions together with automatic (local) variables)”。本文不多说 Block 的使用方法,着重讨论 Block 的实现机制。

一、Block 的基本实现

可以使用指令 clang -rewrite-objc file_name_of_the_source_code,将 OC 源代码转换成对应的 C++ 实现,从而探究 Block 的实现原理。

原始代码:

1
2
3
4
5
6
int main()
{
void (^blk)(void) = ^{printf("Block\n");};
blk();
return 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
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\n");
}

static struct __main_block_desc_0
{
unsigned long reserved;
unsigned long Block_size;
} __main_block_desc_0_DATA = {
0,
sizeof(struct __main_block_impl_0)
};

int main() {
void (*blk)(void) =
(void (*)(void))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA);
((void (*)(struct __block_impl *))((struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
return 0;
}

对比原始代码,转换后的代码增加了三个结构体和一个函数的定义。结构体:__block_impl、__main_block_impl_0、__main_block_desc_0;函数:__main_block_func_0。__main_block_func_0 对应原始代码中的 Block 实现,函数命名的规则是取原始方法名(main)和该 Block 在原始方法中的次序(第0个),结构体的命名规则也是如此。

二、isa 和 _NSConcreteStackBlock

上小节 __main_block_impl_0 的构造函数中有赋值语句 impl.isa = &_NSConcreteStackBlock,本小节简述 isa 和 _NSConcreteStackBlock 的含义。

OC 中的对象实例和类编译后都有其对应的结构体定义,如下所示。

对象实例对应的结构体:

1
2
3
4
/* declared in /usr/include/objc/objc.h */
struct objc_object {
Class isa;
}

类对应的结构体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* declared in /usr/include/objc/runtime.h */
struct objc_class {
Class isa;
};

typedef struct objc_class *Class;

/* declared at runtime/objc-runtime-new.h in the objc4 runtime library */
struct class_t {
struct class_t *isa;
struct class_t *superclass;
Cache cache;
IMP *vtable;
uintptr_t data_NEVER_USE;
};

OC 中的类使用 class_t 构造(class_t 本身基于 objc_class),也就是说 OC 中的每个类都使用 class_t 创建了实例。比如,NSObject 有其对应的 class_t 实例,NSMutableArray 有其对应的 class_t 实例。class_t 实例保存了类的信息,如方法名、方法实现、指向父类的指针等,提供给 OC 运行时库使用。

基于上面描述,下图描述 isa 值的含义。

图 OC 对象和类中 isa 指针的指向

__main_block_impl_0 结构体基于 objc_object,表明 Block 本身即为 OC 对象。创建 Block 时执行语句 impl.isa = &_NSConcreteStackBlock。根据上文描述,_NSConcreteStackBlock 是 class_t 实例,保存了该 Block 对应的类的信息。

三、Block 捕获自动变量

Block 能够捕获自动变量。下面使用 clang -rewrite-objc file_name_of_the_source_code 指令转换代码,描述了在这种情况下 Block 实现方式的变化。(__block_impl、__main_block_desc_0、__main_block_desc_0_DATA的声明和定义与上文相同,不再描述)

原始代码:

1
2
3
4
5
6
7
int main() {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, val);};
return 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
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
const char *fmt;
int val;

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, const char *_fmt, int _val, int flags=0)
: fmt(_fmt), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself)
{
const char *fmt = __cself->fmt;
int val = __cself->val;
printf(fmt, val);
}

int main() {
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (*blk)(void) = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, fmt, val);
return 0;
}

__main_block_impl_0 为 Block 对象的定义,在这种情况下,其中增加了两个成员变量(fmt 和 val)用来存储捕获的自动变量。Block 不会捕获未使用的自动变量(dmy)。

四、Block 中修改静态变量、静态全局变量和全局变量

Block 中能够修改静态变量、静态全局变量和全局变量的值,但是底层实现机制存在差异。下面给出转换前后的代码,并给出说明。

原始代码:

1
2
3
4
5
6
7
8
9
10
11
12
int global_val = 1;
static int static_global_val = 2;
int main()
{
static int static_val = 3;
void (^blk)(void) = ^{
global_val *= 1;
static_global_val *= 2;
static_val *= 3;
};
return 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
int global_val = 1;
static int static_global_val = 2;

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0)
: static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val;

global_val *= 1;
static_global_val *= 2;
(*static_val) *= 3;
}

int main()
{
static int static_val = 3;

blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &static_val);
return 0;
}

静态全局变量和全局变量的处理方式是一致的,Block 能够直接对其进行读写。

静态变量的可见性只是当前函数内,转换后的代码中 __main_block_func_0 无法访问。在 __main_block_impl_0 中增加指向静态变量指针的成员变量,通过该成员变量实现读写静态变量。

前文说到 Block 能够捕获自动变量,但是不能修改其值。自动变量的生命周期跟随其所在的作用域,离开作用域即销毁。Block 的生命周期可能会长于自动变量的生命周期,所以无法采用读写静态变量的实现方案。

五、Block 中修改 __block 变量

原始代码:

1
2
3
4
5
int main() {
__block int val = 10;
void (^blk)(void) = ^{val = 1;};
return 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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
__Block_byref_val_0 *val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0)
: val(_val->__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_val_0 *val = __cself->val;
(val->__forwarding->val) = 1;
}

static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_impl_0 *src)
{
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long 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()
{
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, &val, 0x22000000);
return 0;
}

转换后的代码中,原始 __block 变量转换成了 __Block_byref_val_0 结构体类型。__Block_byref_val_0 的成员变量 val 存储原始值。

__Block_byref_val_0 实例和 __main_block_impl_0 实例是多对多的关系,即一个 __Block_byref_val_0 实例可以在多个 __main_block_impl_0 实例中使用,一个 __main_block_impl_0 实例也可以使用多个 __Block_byref_val_0 实例。

六、Block 的存储类型

上文可知 Block 本身也是 OC 对象,其在内存中的存储方式有三种:_NSConcreteStackBlock、_NSConcreteGlobalBlock、_NSConcreteMallocBlock,分别对应:栈、全局/静态存储区、堆。内存区域划分方式大致可用下图表示,下图同时描述了不同存储方式的 Block 对应的内存区域。

图 Block 不同存储方式对应的内存区域

Block 字面定义在全局作用域生成 _NSConcreteGlobalBlock 类型 Block 对象。

存储类型为 _NSConcreteStackBlock 的 Block 以及 __block 修饰的变量的生命周期与普通自动变量相同,离开作用域后即销毁。

七、Block 的存储类型 -- 堆上的 Block

堆上的 Block 即存储类型为 _NSConcreteMallocBlock 的 Block。_NSConcreteStackBlock 存储类型的 Block 可以从栈拷贝到堆上。下图为 Block 从栈拷贝到堆的示意图,同样的,__block 变量也可以从栈拷贝到堆。

图 Block 和 __block 变量从栈拷贝到堆

在下列情况下,栈中的 Block 会拷贝到堆:

a. 调用 Block 对象的 copy 方法;

b. 函数返回值为 Block 对象;

c. Block 对象赋给 __strong 所有权描述符修饰的变量;

d. Block 对象被 Cocoa 框架中的 “usingBlock” 方法使用,或者被 GCD 中的函数使用。

不同存储类型的 Block 对象调用 copy 方法的效果不同:

a. _NSConcreteStackBlock 类型的 Block 对象,从栈拷贝到堆;

b. _NSConcreteGlobalBlock 类型的 Block 对象,不发生作用;

c. _NSConcreteMallocBlock 类型的 Block 对象,引用计数加一。(在 ARC 开启的情况下,多次调用 copy 方法也没有问题。)

八、__block 变量

当 Block 使用了 __block 变量并且 Block 从栈拷贝到堆时:如果 __block 变量存储在栈上,__block 变量会被拷贝到堆上,并且 Block 对象拥有 __block 变量的所有权;如果 __block 变量本来即存储在堆上,Block 也会拥有 __block 变量的所有权。

__block 变量编译后也是普通的结构体实例,其中有个特别的成员变量 __forwarding。通过 __forwarding 成员变量保证访问 __block 变量的一致性。如下代码片段,__block 变量随着 Block 从栈拷贝到堆上。

1
2
3
4
__block int val = 0;
void (^blk)(void) = [^{++val;} copy];
++val;
blk();

Block 内部会修改堆上的 __block 变量,Block 外部会修改栈上的 __block 变量。转换后,这两种行为是一致的,即++(val.__forwarding->val)。栈和堆上 __block 变量中的 __forwarding 指针都指向堆上的 __block 变量,下图描述了这种机制。

图 __block 变量拷贝到堆

九、Block 捕获对象实例

前文讲述的 Block 捕获自动变量,自动变量的类型为整型变量。当 Block 捕获的自动变量为 OC 对象时:

原始代码:

1
2
3
4
5
6
7
8
9
10
11
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
} copy];
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);

转换后代码:

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
/* a struct for the Block and some functions */
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
id __strong array;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id __strong _array, int flags=0)
: array(_array) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj)
{
id __strong array = __cself->array;
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
}

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

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

static struct __main_block_desc_0 {
unsigned long reserved;
unsigned long 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
};

/* Block literal and executing the Block */
blk_t blk;
{
id __strong array = [[NSMutableArray alloc] init];
blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, array, 0x22000000);
blk = [blk copy];
}
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);
(*blk->impl.FuncPtr)(blk, [[NSObject alloc] init]);

十、内存管理

前文讲述到 Block 内存管理时提到对象之间的所有权关系,但是转换后的 C 代码是无法利用 ARC 机制的。本小节说明相关的内存管理是如何实现的。

10.1 Block 内的内存管理实现

在前文中,当 Block 对象需要引用对象时,比如捕获 __block 变量、捕获 OC 对象实例,__main_block_desc_0 结构体中多了两个成员变量 copy 和 dispose,他们都为函数指针,函数实现如下代码所示。copy 和 dispose 分别对应对象的初始化和销毁,OC 运行时检测到 Block 从栈拷贝到堆或者 Block 对象被销毁时,能够适时调用 copy 和 dispose 实现 Block 内的内存管理。

Block 捕获 __block 变量内存管理相关代码:

1
2
3
4
5
6
7
8
9
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}

static void __main_block_dispose_0(struct __main_block_impl_0 *src)
{
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}

Block 捕获对象实例内存管理相关代码:

1
2
3
4
5
6
7
8
9
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src)
{
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}

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

10.2 __block 变量内的内存管理实现

当 __block 修饰的变量为 OC 对象实例时,__block 内部需要负责该对象实例的内存管理。如下代码所示。__block 变量内的内存管理实现和 Block 内的内存管理类似。该情况下,__Block_byref_obj_0 结构体中多了两个成员变量 __Block_byref_id_object_copy 和 __Block_byref_id_object_dispose,都为函数指针,作用与上小节的 copy 和 dispose 相同,OC 运行时检测到 __block 变量从栈拷贝到堆或者 __block 变量被销毁时,适时调用这对方法,实现 __block 变量内的内存管理。

原始代码:

1
__block id obj = [[NSObject alloc] init];

转换后代码:

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
/* struct for __block variable */
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*);
__strong id obj;
};

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

/* __block variable declaration */
__Block_byref_obj_0 obj = {
0,
&obj,
0x2000000,
sizeof(__Block_byref_obj_0),
__Block_byref_id_object_copy_131,
__Block_byref_id_object_dispose_131,
[[NSObject alloc] init]
};

十一、ARC 下 Block 得存储类型变化

_NSConcreteGlobalBlock:这种类型的 block 定义在全局存储区,它没有捕获任何上下文,在编译时就能完全定义。

_NSConcreteStackBlock:这种类型的 block 定义在栈上。block 在被拷贝到堆上之前,都是存储在栈中。

_NSConcreteMallocBlock:这种类型的 block 存储在堆上。和普通 OC 对象遵循同样的内存管理规则(引用计数)。

MRC 和 ARC 的 block 存储有差异,“在 ARC 开启的情况下,将只会有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 类型的 block。”

十二、案例分析

案例

下面的代码显然存在内存泄漏,有 self -> blk -> self 这样的循环引用。那内存泄漏是哪个环节导致的,是位置 A,还是位置 B?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@interface TestObject : NSObject
@property (nonatomic, copy) dispatch_block_t blk;
@end
@implementation TestObject
- (instancetype)init {
self = [super init];
if (self) {
self.blk = ^{ // A
[self emptyMethod];
};
self.blk(); // B
}
return self;
}
- (void)emptyMethod {
NSLog(@"hello");
}
@end

使用 Instruments 能够分析出位置 A 便引入了循环引用。参考 九、Block 捕获对象实例,Block 实例化的时候,外部对象实例在其构造函数中就被强引用。所以 Block 定义的时候就已经产生了循环引用,而不用等到执行的时候。

参考文献:

Sakamoto, Kazuki, and Tomohiko Furumoto. Pro Multithreading and Memory Management for IOS and OS X. Apress, 2012.

A look inside blocks: Episode 2

唐巧 . 谈Objective-C block的实现 – ARC 对 block 类型的影响