Block 原理
文本内容只涉及ARC.
0x0. 分门别类
block 类型 | 条件 |
---|---|
栈 block | 引用外部变量 & 没有被强引用 |
堆 block | 引用外部变量 & 被强引用 |
全局 block | 没有引用外部变量,或者引用静态局部变量、全局变量 |
外部变量:是指局部变量、实例变量和属性,不含全局变量和静态局部变量。
0x00. 栈 Block
NSInteger value = 2020;
__unsafe_unretained void(^myBlock)() = ^(){
NSLog(@"%ld", value);
};
NSLog(@"myBlock is %@", myBlock); // 输出:myBlock is <__NSStackBlock__: 0x16d79ae80>
2
3
4
5
0x01. 堆 Block
NSInteger value = 2020;
void(^myBlock)() = ^(){
NSLog(@"%ld", value);
};
NSLog(@"myBlock is %@", myBlock); // 输出:myBlock is <__NSMallocBlock__: 0x109b9a2d0>
2
3
4
5
0x02. 全局 Block
static NSInteger value = 2020;
void(^myBlock)() = ^(){
NSLog(@"%ld", value);
};
NSLog(@"myBlock is %@", myBlock); // 输出:myBlock is <__NSGlobalBlock__: 0x1029101a0>
2
3
4
5
当然 block 类型不止这三种,还有_NSConcreteAutoBlock
、_NSConcreteFinalizingBlock
等,本文暂时不探讨。
🌴
0x1. 实现原理
得益于 block 代码开源,我们可以一饱眼福。接下来,就揭开 block 的“神秘”面纱,看看它的庐山真面目。
首先,我们创建一个最基本的 block,无参数无返回值,如下:
void(^myBlock)(void) = ^(){
};
2
根据官方文档、源码 (libclosure-74.tar.gz)、再结合clang -rewrite-objc
,分析出编译器会将这一小段代码,转换成类似如下代码:
// block 结构体
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)((void *, ...);
struct Block_descriptor *descriptor;
// imported variables
};
// 调用block时,最终会执行到这里
void __block_invoke(struct Block_layout *_block) {
}
// block的一些描述信息,比如存储block函数签名、block大小等
static struct Block_descriptor {
size_t reserved; // Block_descriptor_1
size_t Block_size; // Block_descriptor_1
const char *signature; // Block_descriptor_3
const char *layout; // Block_descriptor_3
} __block_descriptor = {0, sizeof(struct Block_layout), "v8@?0", NULL};
// 我们创建的block
struct Block_layout myBlock = {
&_NSConcreteGlobalBlock, // isa
0x50000000, // flags
0, // reserved
__block_invoke, // invoke
&__block_descriptor // descriptor
};
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 -rewrite-objc
Rewrite Objective-C source to C++. 分析 block 时,可以用来作个辅助参考。
接下来,我们看下 block 结构体中这些值的具体含义:
isa
为 _NSConcreteGlobalBlock ,顾名思义这是一个全局 block;flags
为0x50000000,是 1 << 28 和 1 << 30 这两个值的组合,由下述枚举值可知,其含义分别为:- 标识 block 类型为 global;
- 标识 block 具有函数签名,所以 descriptor 中包含
signature
成员变量;
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
2
3
4
5
6
7
8
9
10
11
12
13
reserved
为0,保留字段;__block_invoke
block 调用时,执行的函数指针;__block_descriptor
为 block 的描述信息,从源码 (libclosure-74.tar.gz)中,可以看到描述信息共有三个:
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved; // 保留字段
uintptr_t size; // block大小
};
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
BlockCopyFunction copy; // block拷贝时的辅助函数指针
BlockDisposeFunction dispose; // block释放时的辅助函数指针
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
const char *signature; // 函数签名
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
不同的 block,其成员变量 descriptor 结构也不同,默认包含Block_descriptor_1
。上述例子中,因为 block 具有函数签名,所以包含Block_descriptor_3
。那么什么情况下会包含Block_descriptor_2
呢?请接着往下看。
🌴
0x2. 变量捕获
对 block 实现原理有了基本了解之后,再来看下 block 是如何捕获一个外部变量的。
NSInteger value = 2020; // 先捕获个最基本的变量
void(^myBlock)(void) = ^(){
printf("%ld", (long)value);
};
2
3
4
实现如下:
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)((void *, ...);
struct Block_descriptor_1 *descriptor;
NSInteger value; // 捕获的外部变量
};
void __block_invoke(struct Block_layout *_block) {
NSInteger value = _block->value; // bound by copy
printf("%ld", (long)value);
}
static struct Block_descriptor {
size_t reserved; // Block_descriptor_1
size_t Block_size; // Block_descriptor_1
const char *signature; // Block_descriptor_3
const char *layout; // Block_descriptor_3
} __block_descriptor = {0, sizeof(struct Block_layout), "v8@?0", NULL};
NSInteger value = 2020;
struct Block_layout myBlock = {
&_NSConcreteMallocBlock,
0xc1000002,
0,
__block_invoke,
&__block_descriptor,
value // 传入外部变量值
};
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
可以看到这是一个堆 block。flags
为0xc1000002,是1 << 24
| 1 << 30
| 1 << 31
| 2
这四者的组合。对照上述枚举值,可知:
- 1 << 24 为
BLOCK_NEEDS_FREE
,表明 block 销毁时需要递减引用计数,当引用计数为0时会调用free
释放内存。因为堆 block 在创建的时候会调用malloc
函数,同时引用计数自增2,之后每调用一次copy
,引用计数便会增加2; - 1 << 30 为
BLOCK_HAS_SIGNATURE
,这是 block 具有函数签名的标志,函数签名存储在Block_descriptor_3
中; - 1 << 31 为
BLOCK_HAS_EXTENDED_LAYOUT
,该 flag 含义比较隐晦,稍后重点分析下。首先可以明确的是:只有 block 捕获了外部变量,才会设置该 flag 。 - 2 最后这个2,就是上面所说的,block 拷贝后增加的引用计数。
值的注意的是:BLOCK_NEEDS_FREE
是在运行时确定的,也就是说只有调用了_Block_copy
函数,将 block 拷贝到堆上之后,才会包含该 flag。这一点可以从源码中确定。
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
latching_incr_int(&aBlock->flags); // 多次copy会执行这里,直接增加引用计数
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size);
#if __has_feature(ptrauth_calls)
result->invoke = aBlock->invoke;
#endif
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);
result->flags |= BLOCK_NEEDS_FREE | 2; // 在这里设置flag,并且引用计数+2
_Block_call_copy_helper(result, aBlock); // 调用辅助函数,即Block_descriptor_2中的copy函数指针
result->isa = _NSConcreteMallocBlock;
return result;
}
}
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
🍍
接下来,重点说下这个比较隐晦的BLOCK_HAS_EXTENDED_LAYOUT
。在源码中,看到这样一段描述:
// Extended layout encoding.
// Values for Block_descriptor_3->layout with BLOCK_HAS_EXTENDED_LAYOUT
// and for Block_byref_3->layout with BLOCK_BYREF_LAYOUT_EXTENDED
// If the layout field is less than 0x1000, then it is a compact encoding
// of the form 0xXYZ: X strong pointers, then Y byref pointers,
// then Z weak pointers.
// If the layout field is 0x1000 or greater, it points to a
// string of layout bytes. Each byte is of the form 0xPN.
// Operator P is from the list below. Value N is a parameter for the operator.
// Byte 0x00 terminates the layout; remaining block data is non-pointer bytes.
enum {
BLOCK_LAYOUT_ESCAPE = 0, // N=0 halt, rest is non-pointer. N!=0 reserved.
BLOCK_LAYOUT_NON_OBJECT_BYTES = 1, // N bytes non-objects
BLOCK_LAYOUT_NON_OBJECT_WORDS = 2, // N words non-objects
BLOCK_LAYOUT_STRONG = 3, // N words strong pointers
BLOCK_LAYOUT_BYREF = 4, // N words byref pointers
BLOCK_LAYOUT_WEAK = 5, // N words weak pointers
BLOCK_LAYOUT_UNRETAINED = 6, // N words unretained pointers
BLOCK_LAYOUT_UNKNOWN_WORDS_7 = 7, // N words, reserved
BLOCK_LAYOUT_UNKNOWN_WORDS_8 = 8, // N words, reserved
BLOCK_LAYOUT_UNKNOWN_WORDS_9 = 9, // N words, reserved
BLOCK_LAYOUT_UNKNOWN_WORDS_A = 0xA, // N words, reserved
BLOCK_LAYOUT_UNUSED_B = 0xB, // unspecified, reserved
BLOCK_LAYOUT_UNUSED_C = 0xC, // unspecified, reserved
BLOCK_LAYOUT_UNUSED_D = 0xD, // unspecified, reserved
BLOCK_LAYOUT_UNUSED_E = 0xE, // unspecified, reserved
BLOCK_LAYOUT_UNUSED_F = 0xF, // unspecified, reserved
};
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
什么意思呢?简单来说layout
字段就是一个以 0x00 作为结束标志的字节编码值。分为两种情况:
layout
值如果小于0x1000的话,那么编码值的格式为 0xXYZ。X 表示捕获__strong
变量的个数、Y 表示捕获__block
变量的个数、Z 表示捕获__weak
变量的个数。layout
值如果大于等于0x1000的话,那么就指向一个字符串,该字符串以字节为单位,每个字节格式为 0xPN。其中:- P 对应上述枚举值,表示捕获变量的类型。例如捕获一个
__strong
变量,那么 P 就是3,即BLOCK_LAYOUT_STRONG
; - N 是 P 的参数。这个所谓的“参数”没有具体说明是什么意思,不过据我测试发现,指的是捕获 P 类型变量的个数减1。比如:捕获16个
__strong
变量,那么 N 就是15,该字节就是 0x3f。
- P 对应上述枚举值,表示捕获变量的类型。例如捕获一个
🌰 举个例子:
1.layout
值小于0x1000的情况
2.layout
值大于等于0x1000的情况
如果捕获的外部变量,是基本数据类型,那么layout
中的值也就没什么意义了。
通过实践,发现一个比较有趣的地方😏。就是 block 捕获外部变量后,结构体内部永远是按照__strong
、__block
、__weak
、__unsafe_unretained
、基本数据类型
,这个先后次序进行排列。
🌴
0x3. 变量捕获 (__block)
在源码注释中看到这么一段话:
A Block can reference four different kinds of things that require help when the Block is copied to the heap.
1) C++ stack based objects.
2) References to Objective-C objects.
3) Other Blocks.
4) __block variables.
也就是说,如果 block 捕获了以上这四种类型变量,那么在 block 被拷贝到堆上时,需要一个辅助函数,这个辅助函数的作用,就是将其所捕获的变量也进行拷贝或者直接增加引用计数。
因此,其成员变量 descriptor 结构体中将会包含Block_descriptor_2
,即copy
和dispose
这两个辅助函数指针。此时,flags
也会包含BLOCK_HAS_COPY_DISPOSE
。所以,源码中在获取 block 函数签名时,是这样处理的:
static struct Block_descriptor_3 * _Block_descriptor_3(struct Block_layout *aBlock)
{
if (! (aBlock->flags & BLOCK_HAS_SIGNATURE)) return NULL;
uint8_t *desc = (uint8_t *)aBlock->descriptor;
desc += sizeof(struct Block_descriptor_1);
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE) { // 判断是否包含Block_descriptor_2
desc += sizeof(struct Block_descriptor_2);
}
return (struct Block_descriptor_3 *)desc;
}
2
3
4
5
6
7
8
9
10
记得以前工作中,在这里就踩过坑,因为函数签名获取不正确,造成了 crash。
断点调试看下 block 被拷贝时,辅助函数调用流程:
🍒
最后来看下,捕获__block
变量时,block 内部是如何处理的。
__block NSInteger value = 30;
void(^myBlock)(void) = ^(){
NSLog(@"%ld", value); // block内部访问value变量
};
NSLog(@"%ld", value); // block外部访问value变量
2
3
4
5
这段代码的实现,大概这样:
struct Block_byref { // 针对捕获的__block变量单独生成一个结构体
void *isa;
struct Block_byref *forwarding;
volatile int32_t flags; // contains ref count
uint32_t size;
NSInteger value; // 捕获的外部变量
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)((void *, ...);
struct Block_descriptor_1 *descriptor;
struct Block_byref *value; // by ref
};
void __block_invoke(struct Block_layout *_block) {
struct Block_byref *value = _block->value; // bound by ref
NSLog(@"%ld", value->__forwarding->value) // block内部访问value变量
}
static void copy_helper(struct Block_layout *dst, struct Block_layout *src) {
// TO DO
}
static void dispose_helper(struct Block_layout *src) {
// TO DO
}
static struct Block_descriptor {
size_t reserved; // Block_descriptor_1
size_t Block_size; // Block_descriptor_1
void(*copy)(struct Block_layout *, struct Block_layout *), // Block_descriptor_2
void(*dispose)(struct Block_layout *) // Block_descriptor_2
const char *signature; // Block_descriptor_3
const char *layout; // Block_descriptor_3
} __block_descriptor = {0, sizeof(struct Block_layout), copy_helper, dispose_helper, "v8@?0", NULL};
// 创建Block_byref结构来表示变量2020
struct Block_byref value = {(void*)0, (Block_byref *)&value, 0, sizeof(Block_byref), 2020};
struct Block_layout myBlock = {
&_NSConcreteMallocBlock,
0xc3000002, // BLOCK_NEEDS_FREE | BLOCK_HAS_COPY_DISPOSE | BLOCK_HAS_SIGNATURE | BLOCK_HAS_EXTENDED_LAYOUT | 2
0,
__block_invoke,
&__block_descriptor,
(struct Block_byref *)&value
};
NSLog(@"%ld", value.forwarding->value); // block外部访问value变量
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 内部还是外部,对 value 变量的访问与设置,其实都是用的 struct Block_byref value 这个结构体变量。之所以使用forwarding
指针间接访问,就是为了 block 拷贝到堆上后,栈 block 和堆 block 都能访问到这同一个变量。
🎉 💯
参考资料:
https://clang.llvm.org/docs/Block-ABI-Apple.html https://opensource.apple.com/tarballs/libclosure/libclosure-74.tar.gz