0%

理解 Objective-C Block

Block 介绍,截获变量如何实现,__block 修饰符,Block 内存管理,Block 循环引用。
clang -rewrite-objc -fobjc-arc main.m

介绍

BlockObjective-C 对闭包的实现,Block 是将函数及其上下文封装起来的对象。

Block 的底层实现

main.m 文件中写入如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {

void (^simpleBlock)(void) = ^{
NSLog(@"This is a block.");
};

simpleBlock();
}
return 0;
}

命令行输入 clang -rewrite-objc main.m 将其转为 C++ 文件 main.cpp。两段关于 Block 的代码编译后如下:

1
2
3
void (*simpleBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));

((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock); // 可以看到 Block 的调用最终其实就是去找它的实现 `__block_impl` 中的函数指针 `FuncPtr`,最终就是一个函数调用

这里面涉及几个结构:

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

// 代表 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; // 代表这是一个栈区 Block
impl.Flags = flags;
impl.FuncPtr = fp; // 将 __main_block_func_0 作为 Block 具体的实现函数
Desc = desc;
} // 构造函数
};

// Block 的实现
struct __block_impl {
void *isa; // 表明 Block 是一个对象
int Flags;
int Reserved;
void *FuncPtr; // 函数指针,指向具体实现
};

// Block 的实现函数
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {

NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_455ba4_mi_0);
}

// 包含 Block 的内存管理
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)};

图 1 - Block 的底层结构

结论:

  • Block 是一个对象,因为有 isa 指针;
  • 上述 Block 是一个栈区 Block
  • Block 的执行本质就是在执行一个函数;

Block 值类型临时变量的捕获

上述代码修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {

int anInteger = 5; // 增加一个临时变量

void (^simpleBlock)(void) = ^{
NSLog(@"Integer is %d", anInteger); // Block 中使用这个临时变量。
};

anInteger = 10; // 修改临时变量

simpleBlock(); // 执行后打印结果为 5
}
return 0;
}

同样编译为 C++ 后,代码如下:

1
2
3
4
5
6
7
int anInteger = 5;

void (*simpleBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, anInteger)); // 增加了 anInteger 作为参数

anInteger = 10;

((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);

主要的变化是,__main_block_impl_0 的构造函数增加了 anInteger 作为参数传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int anInteger; // 增加了捕获的变量作为成员变量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _anInteger, int flags=0) : anInteger(_anInteger) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 增加了一句从 __main_block_impl_0 中获取捕获的变量的步骤,最终函数执行使用的也是捕获后的变量,而不是之前的临时变量
int anInteger = __cself->anInteger; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_62f6c8_mi_0, anInteger);
}

结论:

  • Block 会捕获临时变量,即复制一份放入 Block 对象的结构体中,最终 Block 执行是使用的是复制过去的变量,因此被捕获的临时变量即使发生改变,不影响 Block 中捕获的值;
  • 对于值类型的临时变量,Block 是直接捕获的,即直接复制一份进去;

Block 关于更多类型变量的截获

上一部分是关于基本类型的临时变量的截获,就是直接复制一份,那么对于更多类型的变量呢?

  • 对于局部变量
    • 基本数据类型:直接截获该值,就是复制一份;
    • 对象类型:连同对象的所有权修饰符一起截获;
  • 静态局部变量:指针引用;
  • 静态全局变量:不捕获;
  • 全局变量:不捕获;

全局变量在整个工程中均有效;静态全局变量即 ObjC 中常常在 @implementation 上面定义的静态变量,它只在定义它的文件内有效;静态局部变量是在某个函数中定义的静态变量,程序只会为它分配一次内存,在函数返回后它不会消失,它的储存和静态全局变量类似,都不会放在栈区,而是在静态区;局部变量存储在栈区,随着函数执行结束内存就会释放。

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
#import <Foundation/Foundation.h>

int num1 = 10;
static int num2 = 20;

int main(int argc, const char * argv[]) {
@autoreleasepool {

static int num3 = 30;
int num4 = 40;

__unsafe_unretained id obj1 = nil;
__strong id obj2 = nil;

void (^simpleBlock)(void) = ^{
NSLog(@"全局变量%d", num1);
NSLog(@"静态全局变量%d", num2);
NSLog(@"静态局部变量 %d", num3);
NSLog(@"局部变量 %d", num4);
NSLog(@"对象1 %@", obj1);
NSLog(@"对象2 %@", obj2);
};

simpleBlock();
}
return 0;
}

使用命令 clang -rewrite-objc -fobjc-arc main.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
// 全局变量和静态全局变量都是在函数外定义,Block 不进行捕获而是直接可以拿到
int num1 = 10;
static int num2 = 20;


struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *num3; // 局部静态变量通过指针方式捕获
int num4; // 局部变量直接捕获其值
__unsafe_unretained id obj1; // 对于对象,会包含其所有权修饰符一起捕获
__strong id obj2;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_num3, int _num4, __unsafe_unretained id _obj1, __strong id _obj2, int flags=0) : num3(_num3), num4(_num4), obj1(_obj1), obj2(_obj2) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *num3 = __cself->num3; // bound by copy
int num4 = __cself->num4; // bound by copy
__unsafe_unretained id obj1 = __cself->obj1; // bound by copy
__strong id obj2 = __cself->obj2; // bound by copy

NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_3341c1_mi_0, num1);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_3341c1_mi_1, num2);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_3341c1_mi_2, (*num3));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_3341c1_mi_3, num4);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_3341c1_mi_4, obj1);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_3341c1_mi_5, obj2);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->obj1, (void*)src->obj1, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_assign((void*)&dst->obj2, (void*)src->obj2, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->obj1, 3/*BLOCK_FIELD_IS_OBJECT*/);_Block_object_dispose((void*)src->obj2, 3/*BLOCK_FIELD_IS_OBJECT*/);}

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(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;

static int num3 = 30;
int num4 = 40;

__attribute__((objc_ownership(none))) id obj1 = __null;
__attribute__((objc_ownership(strong))) id obj2 = __null;

void (*simpleBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &num3, num4, obj1, obj2, 570425344));

((void (*)(__block_impl *))((__block_impl *)simpleBlock)->FuncPtr)((__block_impl *)simpleBlock);
}
return 0;
}

__block 修饰符

默认情况下,被捕获的局部变量在 Block 内部是无法被赋值的,如果赋值编译器会报错。默认情况下,被捕获的局部变量在 Block 的实现结构体中是一个成员变量。

__block 修饰符的作用是,将这个临时变量变成一个对象存储起来。

还是上述对基本类型的临时变量的捕获的例子,我们加上 __block 修饰符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
@autoreleasepool {

__block int anInteger = 5; // 加上 __block 修饰符

void (^simpleBlock)(void) = ^{
NSLog(@"Integer is %d", anInteger);
};

anInteger = 10;

simpleBlock(); // 加上 __block 修饰符后,打印结果变成了 10
}
return 0;
}

编译后的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

// 针对 anInteger 变量专门创建了一个结构体
struct __Block_byref_anInteger_0 {
void *__isa; // 拥有 isa
__Block_byref_anInteger_0 *__forwarding; // 转发指针,栈区 Block 在 copy 之前都是指向自己
int __flags;
int __size;
int anInteger; // 这里存储变量的值
};

struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_anInteger_0 *anInteger; // by ref // 之前是直接捕获,现在变成了一个结构体指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_anInteger_0 *_anInteger, int flags=0) : anInteger(_anInteger->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};

结论:

  • 临时变量被捕获后在 Block 内部无法被修改,因为是值类型
  • 静态变量和全局变量都不涉及 __block 修饰符
  • 加上 __block 修饰符后,变量变成对象存储起来,并且在 Block 内外进行修改都是在操作这个对象。

__forwarding

继续上面的例子,我们查看 __main_block_func_0 函数,也就是 Block 的具体函数实现。

1
2
3
4
5
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_anInteger_0 *anInteger = __cself->anInteger; // bound by ref

NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_5fc99a_mi_0, (anInteger->__forwarding->anInteger));
}

可以看到,使用 NSLog() 打印这个变量时,并不是直接从 __Block_byref_anInteger_0 结构体中取出 anInteger,而是通过它的 __forwarding 绕了一下,即 anInteger->__forwarding->anInteger
这个例子中,Block 就是一个栈区的 Block,这个 __forwarding 就是指向本身栈区创建的这个结构体,因此不管直接取值还是通过指针取值最终都是一样的。

Block 的内存管理

Block 分三种类型,分别是栈区 Block、堆区 Block 和全局 Block

Block类别 Copy结果
_NSConcreteStackBlock
_NSConcreteMallocBlock 增加引用计数
_NSConcreteGlobalBlock 数据区 什么也不做

关于 copy 操作,当栈区 Block 拷贝一份到堆区后,会产生一份一样的 Block 在堆区,唯一不同的是,__block 修饰的变量对象在拷贝一份后,堆区的那个其 __forwarding 依旧指向自己,但是栈区的这个其 __forwarding 指针指向的是对应的堆区的那个对象。

因此,__forwarding 保证了不管是否有 copy 操作,最终操作的 __block 变量都是同一个。

References

  1. Blocks Programming Topics
  2. Working with Blocks
  3. How Do I Declare A Block in Objective-C?
  4. 你真的理解__block修饰符的原理么?
  5. 对 Strong-Weak Dance的思考
  6. iOS 中的 block 是如何持有对象的
  7. OC与Swift闭包对比总结
  8. iOS中Block的用法,示例,应用场景,与底层原理解析