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 类型 |
存储域 |
_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 内部|传递方式|访问|修改
:-: | :-: | :-: | :-: | :-: | :-:
局部变量|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_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};
|
源代码:
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 修饰效果是一样的。