底层初窥——Block

Block 的实质

  • block本质上也是一个OC对象,它内部也有个isa指针。
  • block是封装了函数调用以及函数调用环境的OC对象。
  • block是封装函数及其上下文的OC对象。

下面我们通过探究 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
- (void) testBlock {
void (^globalBLock)(void) = ^ {
NSLog(@"globalBLock");
};

int a = 1;
void (^mallocBLock)(void) = ^ {
NSLog(@"mallocBLock---%d", a);
};

// 输出 Block 类型
NSLog(@"%@---%@---%@", [globalBLock class], [mallocBLock class], [^{
NSLog(@"stackBlock");
} class]);

// 输出 Block 父类及基类
NSLog(@"[block] = %@", globalBLock);
NSLog(@"[block class] = %@", [globalBLock class]);
NSLog(@"[block superclass] = %@", [globalBLock superclass]);
NSLog(@"[block superclass superclass] = %@", [[globalBLock superclass] superclass]);
NSLog(@"[block superclass superclass superclass] = %@", [[[globalBLock superclass] superclass] superclass]);
}


----输出 Block 类型----

__NSGlobalBlock__---__NSMallocBlock__---__NSGlobalBlock__

---- 输出 Block 父类及基类 ----
Block Print
[block] = <__NSGlobalBlock__: 0x100001030>
[block class] = __NSGlobalBlock__
[block superclass] = __NSGlobalBlock
[block superclass superclass] = NSBlock
[block superclass superclass superclass] = NSObject

我们从打印的结果可以看到两点:

  • Block 有三种类型,分别为:
Block类型 如何确定 Block 类型 存储域
_NSConcreteGlobalBlock 没有用到外界变量或只用到全局变量、静态变量 存在于全局内存中, 生命周期从创建到应用程序结束,相当于单例
_NSConcreteStackBlock 只用到外部局部变量、成员属性变量,且没有强指针引用 存在于栈内存中, 超出其作用域则马上被销毁
_NSConcreteMallocBlock 有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为堆Block 存在于堆内存中, 是一个带引用计数的对象, 需要自行管理其内存
  • Block 的基类是 NSObject,这也说明了 Block 本质上就是一个对象。我们在后面对 c++ 代码的剖析中能更直观的证实 Block 在运行时本质就是一个对象。

探索——源码分析

我们先写一个简单的 Block:

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, const char * argv[]) {
@autoreleasepool {

void (^Jiangtao)(NSString *) = ^(NSString *str) {
NSLog(@"%@", str);
};

Jiangtao(@"Block Print");
}
return 0;
}

上面是一个简单的 Block 实现,这里我们使用下面的命令,将 Objective-C 的源码改写为 c 语言的,借此我们可以研究一下 Block 的具体实现方式。

1
clang -rewrite-objc filename.m

这里我们就直接使用了上方的 main.m 中的代码。下面我们一步步来看看重写为 c 之后的代码。(这里只选取了一些关键代码)。

首先先看一下 main 函数的实现

1
2
3
4
5
6
7
8
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

void (*Jiangtao)(NSString *) = ((void (*)(NSString *))&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// 省略了原代码中的 NSLog 语句...
}
return 0;
}


我们可以看到 Block 的 C++ 实现就是 __main_block_impl_0(这后面的数字 0 代表是 main 中的第一个 block),接下来我们来看看 __main_block_impl_0 的实现:

__main_block_impl_0: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;
__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 在初始化的时候需要传入 fp(即__main_block_func_0)及 __main_block_desc_0 来为结构体中的 __block_impl 进行赋值。

__main_block_func_0:Block 内部执行的具体逻辑

1
2
3
4
static void __main_block_func_0(struct __main_block_impl_0 *__cself, NSString *str) {

NSLog((NSString *)&__NSConstantStringImpl__var_folders_r9_gf2287mj75zb2tkf74w102ym0000gn_T_main_871427_mi_0, str);
}

可以看到初始化时传入了 __main_block_impl_0, 说明方法是封装了 block 执行逻辑的函数。

__main_block_desc_0:Block 的描述信息

1
2
3
4
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)};
* reserved:保留字段
* Block_size:block大小(sizeof(struct __main_block_impl_0))

在定义__main_block_desc_0结构体时,同时创建了__main_block_desc_0_DATA,并给它赋值,以供在main函数中对__main_block_impl_0进行初始化。

__block_impl:Block 的结构

1
2
3
4
5
6
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
* isa,只想了所属类的指针,也就是 Block 的类型。
* Flags,标志变量
* Reserved,保留变量
* FuncPtr,Block 执行时调用的函数指针(指针指向 __main_block_func_0)

从上面 __main_block_impl_0 的实现中,我们可以看到它的初始化方法其实就是讲 Block 的内部逻辑执行函数 __main_block_func_0 及 Block 的一些描述信息 __main_block_desc_0 传给 __block_impl,所以说 __block_impl 也就是 Block 的具体实现。

在其中,我们看到它包含了 isa 指针,所以我们知道 Block 的本质也就是一个对象。(在 runtime 中,对象和类都是用结构体表示的)。


__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
42
43
44
45
46
47
48
49
50
51
52
53
int global_val = 10; // 全局变量
static int static_global_val = 20; // 全局静态变量

int main() {
typedef void (^MyBlock)(void);

static int static_val = 30; // 静态变量

int val = 40; // 局部变量
__block int val_blocked = 45; // __block 修饰的局部变量

int val_unuse = 50; // 未使用的局部变量

// oc 对象
NSMutableArray *array = [[NSMutableArray alloc] init];

// __block 修饰的 oc 对象
__block NSMutableArray *array_blocked = [[NSMutableArray alloc] init];

MyBlock block = ^{
// 捕获局部变量
NSLog(@"val------------------%d", val);
NSLog(@"__blocked val--------%d", val_blocked);

// 修改局部变量 -> 代码编译不通过
//val = 4000;
val_blocked = 450;

// 全局变量
global_val *= 10;
// 全局静态变量
static_global_val *= 10;
// 静态变量
static_val *= 10;

// 修改OC对象
[array addObject:@1];
[array_blocked addObject:@1];

NSLog(@"__block array add---%@", array_blocked);

// 重新赋值
//array = [[NSMutableArray alloc] init]; // 编译不通过
array_blocked = [[NSMutableArray alloc] init];
};
val *= 10; // Block 定义后,更改局部变量值
block();
NSLog(@"global_val-----------%d", global_val);
NSLog(@"static_global_val----%d", static_global_val);
NSLog(@"static_val-----------%d", static_val);
NSLog(@"array----------------%@", array);
NSLog(@"__block array reinit-%@", array_blocked);
}

下面是输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
val------------------40     // 局部变量截获
__blocked val--------45 // __block 修饰的局部变量
__block array add---(
1
)
global_val-----------100
static_global_val----200
static_val-----------300
array----------------(
1
)
__block array reinit-(
)

接下来,我们利用 ==clang -rewrite-objc fileMame.m== 命令,将上面的代码转为 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
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
75
76
77
78
79
80
struct __Block_byref_val_blocked_0 {
void *__isa;
__Block_byref_val_blocked_0 *__forwarding;
int __flags;
int __size;
int val_blocked;
};
struct __Block_byref_array_blocked_1 {
void *__isa;
__Block_byref_array_blocked_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
NSMutableArray *array_blocked;
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val;
int *static_val;
NSMutableArray *array;
__Block_byref_val_blocked_0 *val_blocked; // by ref
__Block_byref_array_blocked_1 *array_blocked; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int *_static_val, NSMutableArray *_array, __Block_byref_val_blocked_0 *_val_blocked, __Block_byref_array_blocked_1 *_array_blocked, int flags=0) : val(_val), static_val(_static_val), array(_array), val_blocked(_val_blocked->__forwarding), array_blocked(_array_blocked->__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_blocked_0 *val_blocked = __cself->val_blocked; // bound by ref
__Block_byref_array_blocked_1 *array_blocked = __cself->array_blocked; // bound by ref
int val = __cself->val; // bound by copy
int *static_val = __cself->static_val; // bound by copy
NSMutableArray *array = __cself->array; // bound by copy

...省略了 Block 内部对数据的操作...


}

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->val_blocked, (void*)src->val_blocked, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->array, (void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_assign((void*)&dst->array_blocked, (void*)src->array_blocked, 8/*BLOCK_FIELD_IS_BYREF*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->val_blocked, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->array, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_dispose((void*)src->array_blocked, 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() {
...

// 这里只取了 Block 的初始化方法
MyBlock block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,
&__main_block_desc_0_DATA,
val,
&static_val,
array,
(__Block_byref_val_blocked_0 *)&val_blocked,
(__Block_byref_array_blocked_1 *)&array_blocked,
570425344));

...
}

从转化的代码中,我们可以得到以下几点结论:

  • 对于外部的变量引用,默认是将其复制到其数据结构中来实现访问的。

从__main_block_impl_0中可以看到 Block 也就是说 Block 的自动变量截获只针对 Block 内部使用的自动变量,不使用则不截获,因为截获的自动变量会存储于 Block 的结构体内部, 会导致 Block 体积变大( __main_block_desc_0 )。

  • Block 对各类型变量的可允许操作不同。

变量类型|是否捕获到 Block 内部|传递方式|访问|修改
:-: | :-: | :-: | :-: | :-: | :-:
局部变量|YES|值传递|YES|NO
全局变量&全局静态变量|NO|指针传递|YES|YES
静态变量|YES|指针传递|YES|YES
OC对象|YES|指针传递|YES|YES

Block 截获变量说明

  • 局部变量

捕获局部变量时,传入了其值保存在__main_block_impl_0。在 Block 内部逻辑实现__main_block_func_0中,通过 __cself->val 直接访问是可以的,而且,Block 对值得捕获是在 Block 定义处,在其后面更改了局部变量值,对 Block 内部的变量值不造成影响。当我们但是修改这个值对外部是没有任何效果的,当我们这样操作时,会得到一个编译错误提示需要为局部变量添加 __block 说明符。

  • 全局变量&全局静态变量

全局变量和全局静态变量没有被截获到 Block 里面,它们的访问是不经过 Block 的(见__main_block_func_0)。因为全局变量存储域为全局(静态)存储区。

  • 静态变量

访问静态变量(static_val)时,将静态变量的指针传递给__main_block_impl_0结构体的构造函数并保存。修改及访问静态变量时,通过指针操作。

Block 截获对象说明

捕获——OC对象

Block 捕获到的是指向对象的指针,在 __main_block_func_0 中,使用对象的时候,对指针进行了复制,于是我们可以对指针所指的对象进行修改,这样外部也可以获得此修改,但是不能对指针重新赋值,因为赋值操作只是修改了 __main_block_func_0 中复制出来的指针,对原指针不造成影响。当我们这样操作时,也会得到一个编译错提示需要为对象添加 __block 说明符。

__block 说明符

Block 不允许修改外部变量的值,这所说的其实是栈中指针的内存地址。__block 其实就是观察到变量被 Block 持有之后,就将外部变量从栈中拷贝到堆中。

再没有添加 __block 说明符的时候,我们只能对局部变量访问但不能修改,对对象能访问及修改但不能重新赋值,但是当我们添加了 __block 说明符之后,对截获的变量和对象的操作都被允许了,我们通过代码来看看 __block 做了什么

  • 在 Block 初始化时,被 __block 修饰的变量(对象)是通过指针传入到 Block 内部,并且根据这个指针生成了一个 __Block_byref_val_blocked_0,并在 __main_block_impl_0 结构体中持有。

  • 在 Block 内部的实现中,在对 __block 修饰的变量的使用是通过引用的方式对原来的变量(对象)进行操作。

__Block_byref_array_blocked 结构体

1
2
3
4
5
6
7
8
9
struct __Block_byref_array_blocked_1 {
void *__isa;
__Block_byref_array_blocked_1 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
int val_blocked;
};

在代码中,带有 __block 的变量会被转化成 __Block_byref_val_blocked_0 结构体:

1
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val_blocked, 0, sizeof(__Block_byref_val_0), 45};

源代码:

1
val_blocked = 450;

Block 内部实现为:

1
(val_blocked->__forwarding->val_blocked) = 450;
  • __forwarding:持有指向该实例自身的指针。通过 __forwarding,可以实现无论 __block 变量配置在栈上还是堆上都能正确地访问 __block 变量,也就是说 __forwarding 是指向自身的。
  • val:保存了最初的 val_blocked 变量,也就是说原来单纯的 int 类型的 val_blocked 变量被 __block 修饰后生成了一个结构体。这个结构体其中一个成员变量持有原来的 val 变量。

Block 存储域

探索 Block 的实质一节中,可以知道 Block 有三种类型。

类型 存储域 特点
全局 Block 程序的数据区域 生命周期从创建到应用程序结束,相当于单例
栈 Block 超出其作用域则马上被销毁
堆 Block 带引用计数的对象, 需要自行管理其内存
  • 全局Block(_NSConcreteGlobalBlock)

    • 定义全局变量的地方有block语法时
    • Block不截获的自动变量时
  • 栈Block(_NSConcreteStackBlock)

    • 在生成 Block 以后,如果这 个Block 不是全局 Block,那么它就是为 _NSConcreteStackBlock 对象。
  • 堆Block(_NSConcreteMallocBlock)

    • 为了解决栈块在其变量作用域结束之后被废弃(释放)的问题,我们需要把 Block 复制到堆中,延长其生命周期。开启ARC时,大多数情况下编译器会恰当地进行判断是否有需要将 Block 从栈复制到堆。

那么什么时候栈上的Block会被复制到堆上呢?

  • 调用Block的copy实例方法时
  • Block作为函数返回值返回时
  • 将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
  • 将方法名中含有usingBlock的Cocoa框架方法或GCD的API中传递Block时

Matt Galloway的博客中讲解了 Block_copy 的具体实现,也可以直接查看runtime.c中源码学习一下栈 Block 是怎样复制到堆中的。

常见问题

一个int变量被 __block 修饰与否的区别?

  • __block 修饰前:Block使用int变量是值传递。
  • __block 修饰后:int变量会被转化为一个 __Block_byref_val_0 结构体的一个实例。因为是指针传递到 Block 内部,有可能在 int 变量作用域结束之后,在 Block 内部继续使用,为了使其在作用域结束之后使用,Block 及 __block 变量会从栈区被复制到堆区。

__block 说明符用于指定将变量值设置到哪个存储区域中,也就是说,当自动变量加上__block 说明符之后,会改变这个自动变量的存储区域。


block在修改NSMutableArray,需不需要添加__block?

这里的情景是修改,如果针对修改的话,是不需要加上__block的。

但是如果是对数组重新赋值的话,是会编译报错了,如果需要重新赋值,必须要加上__block修饰符。

block可以用strong修饰吗?

对于这个问题,得区分 MRC 环境 和 ARC 环境;首先,通过上面小节可知,Block 引用了普通外部变量,都是创建在栈区的;对于分配在栈区的对象,我们很容易会在释放之后继续调用,导致程序奔溃,所以我们使用的时候需要将栈区的对象移到堆区,来延长该对象的生命周期。

对于 MRC 环境,使用 Copy 修饰 Block,会将栈区的 Block 拷贝到堆区。

对于 ARC 环境,使用 Strong、Copy 修饰 Block,都会将栈区的 Block 拷贝到堆区。

所以,Block 不是一定要用 Copy 来修饰的,在 ARC 环境下面 Strong 和 Copy 修饰效果是一样的。