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>
1
2
3
4
5

0x01. 堆 Block

NSInteger value = 2020;
void(^myBlock)() = ^(){
    NSLog(@"%ld", value);
};
NSLog(@"myBlock is %@", myBlock); // 输出:myBlock is <__NSMallocBlock__: 0x109b9a2d0>
1
2
3
4
5

0x02. 全局 Block

static NSInteger value = 2020;
void(^myBlock)() = ^(){
    NSLog(@"%ld", value);
};
NSLog(@"myBlock is %@", myBlock); // 输出:myBlock is <__NSGlobalBlock__: 0x1029101a0>
1
2
3
4
5

当然 block 类型不止这三种,还有_NSConcreteAutoBlock_NSConcreteFinalizingBlock等,本文暂时不探讨。

🌴


0x1. 实现原理

得益于 block 代码开源,我们可以一饱眼福。接下来,就揭开 block 的“神秘”面纱,看看它的庐山真面目。
首先,我们创建一个最基本的 block,无参数无返回值,如下:

void(^myBlock)(void) = ^(){
};
1
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
};
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

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
};
1
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
};
1
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);
};
1
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 // 传入外部变量值
};
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

可以看到这是一个堆 block。flags为0xc1000002,是1 << 24 | 1 << 30 | 1 << 31 | 2这四者的组合。对照上述枚举值,可知:

  • 1 << 24BLOCK_NEEDS_FREE,表明 block 销毁时需要递减引用计数,当引用计数为0时会调用free释放内存。因为堆 block 在创建的时候会调用malloc函数,同时引用计数自增2,之后每调用一次copy,引用计数便会增加2;
  • 1 << 30BLOCK_HAS_SIGNATURE,这是 block 具有函数签名的标志,函数签名存储在Block_descriptor_3中;
  • 1 << 31BLOCK_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;
    }
}
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

🍍

接下来,重点说下这个比较隐晦的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
};
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

什么意思呢?简单来说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。

🌰 举个例子:

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,即copydispose这两个辅助函数指针。此时,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;
}
1
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变量
1
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变量
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 内部还是外部,对 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

最后更新: 2/1/2021, 5:39:58 PM