Block 介绍,截获变量如何实现,__block 修饰符,Block 内存管理,Block 循环引用。
clang -rewrite-objc -fobjc-arc main.m
。
介绍
Block
是 Objective-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);
|
这里面涉及几个结构:
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
|
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; } };
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; };
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_q4_jm3g73557p74v3ncp13gq3l00000gn_T_main_455ba4_mi_0); }
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)};
|
![](https://raw.githubusercontent.com/qiweipeng/images/master/20201001102501.png)
图 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); }; anInteger = 10; simpleBlock(); } 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 = 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) { int anInteger = __cself->anInteger;
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; void (^simpleBlock)(void) = ^{ NSLog(@"Integer is %d", anInteger); }; anInteger = 10; simpleBlock(); } return 0; }
|
编译后的结果为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
struct __Block_byref_anInteger_0 { void *__isa; __Block_byref_anInteger_0 *__forwarding; 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; __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;
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
- Blocks Programming Topics
- Working with Blocks
- How Do I Declare A Block in Objective-C?
- 你真的理解__block修饰符的原理么?
- 对 Strong-Weak Dance的思考
- iOS 中的 block 是如何持有对象的
- OC与Swift闭包对比总结
- iOS中Block的用法,示例,应用场景,与底层原理解析