前言
上一篇文章主要是介绍在iOS开发过程中遇到的Block的具体结构和类型,以及相关操作的影响。
这一篇文章主要是探究 Block 的实现,以及 __block 的原理。
注意:本文中实例都是在ARC环境下运行的。
作者使用的环境是:
- Xcode 9正式版。
- 真机调试 iPhone 6Plus 10.3.3
- MacBook Pro 10.12.6
Block 的实现
关于block的结构,上一篇文章已经说过了,但是我们并没有具体的使用。
这次我将使用 clang 工具查看 Objective-C 代码用c语言的实现,并对block的具体实现进行分析。
需要注意的是clang转化的文件只能作为研究的参考,在实际运行中,还是和源文件有所区别的。
还是熟悉的代码:
typedef void(^TestBlockExample)(void);
TestBlockExample block1 = ^{
printf("Hello, World!\n");
};
补充一下使用clang工具的命令(后面的BlockTest.m是具体的文件名):
clang -rewrite-objc BlockTest.m
image.png
代码太多,这里只取出关键部分进行分析。
从下面的选中部分可以看出来 block 主要分成三个部分:
__BlockTest__test_block_impl_0、
__BlockTest__test_block_func_0、
__BlockTest__test_block_desc_0_DATA
下面我们一个个来分析:
先看最简单的
- __BlockTest__test_block_desc_0_DATA
static struct __BlockTest__test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0)};
从这里就可以看出其实这个结构体就包含两个 size_t 信息。一个是保留数 reserved ,默认就是0,另一个是 Block_size 就是 __BlockTest__test_block_impl_0 结构体的大小。这个地方没啥好说的,我们看下一个:
- __BlockTest__test_block_func_0
static void __BlockTest__test_block_func_0(struct __BlockTest__test_block_impl_0 *__cself) {
printf("Hello, World!\n");
}
这一部分就相当于临时创建了一个函数,供block实际执行的时候调用。
- __BlockTest__test_block_impl_0
struct __BlockTest__test_block_impl_0 {
struct __block_impl impl;
struct __BlockTest__test_block_desc_0* Desc;
__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
这里面包含了一个 impl 是 __block_impl 结构体,定义如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
还有个 Desc 其实就是上面说的 __BlockTest__test_block_desc_0_Data,是block的size信息,下面 __BlockTest__test_block_impl_0 这也是个block结构,就相当于 __BlockTest__test_block_impl_0 的构造函数了。根据传进来的参数对 impl 和 Desc 进行赋值。
下面我们更改一下代码,让block捕获外部变量,根据上一篇的内容,我们已经预见了 block将会是 _ NSMallocBlock 类型,但是这里面的block实际上还是在栈中,所以isa指向是_NSConcreteStackBlock:
int a = 6;
TestBlockExample block1 = ^{
printf("Hello, World! %d\n", a);
};
image.png
我们看到其实多出来的也就是标注的几个地方,中间还贴心的给了注释 “bound by copy” 这里可以简单理解为“值拷贝”,实际上这个地方已经将变量a存到了block的结构体中,外部对变量a以后的修改都不会影响到block结构体中的a。下面讲到 __block 的时候还会探究这个地方。
言归正传,__BlockTest__test_block_impl_0 里面多出了 a 变量,其实这样看来,捕获一个没有被 __block 修饰的变量和普通的block区别不是很大。好,下面我们看一下 __block 的影响。
- (void)test {
int a = 6;
TestBlockExample block1 = ^{
printf("Hello, World! %d\n", a);
};
a = 8;
block1();
}
这段代码的运行结果就是
Hello, World! 6
下面我们改一下代码:
- (void)test {
__block int a = 6;
TestBlockExample block1 = ^{
printf("Hello, World! %d\n", a);
};
a = 8;
block1();
}
运行结果
Hello, World! 8
我们都知道这就是 __block 的功劳。那具体 __block 做了什么呢,我们用clang看一下:
image.png简直了,没想到一个__block做了这么多改变,我做了个文件比对:
image.png使用__block以后大致改变的就是:
- 变量a的类型变了
__block 前 :int a;
__block 后 :__Block_byref_a_0 *a; // by ref
这里面的__Block_byref_a_0就是下面的这个结构体:
- 增加了新的结构体
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
新增的结构体也可以看成是一个对象。
在 __Block_byref_a_0 结构体中我们可以看到成员变量__forwarding,它持有指向该实例自身的指针;
另外还有变量a,变量a在这里只是一个成员了;
这就相当于比之前不加__block修饰的时候多了4个成员变量。
- 构造的时候 入参变了
__block 前 :__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
__block 后 :__BlockTest__test_block_impl_0(void *fp, struct __BlockTest__test_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
主要是 传入的变量值由 _a 变成了 _a->__forwarding,这有什么意义呢,上面说了__forwarding持有指向__Block_byref_a_0实例的指针,这为访问和修改外部变量创造了条件。
- 生成函数体的时候(__BlockTest__test_block_func_0)
__block 前 :
int a = __cself->a; // bound by copy 值拷贝
printf("Hello, World! %d\n", a); // 使用的时候直接使用值
__block 后 :
__Block_byref_a_0 *a = __cself->a; // bound by ref (引用)地址拷贝
printf("Hello, World! %d\n", (a->__forwarding->a));// 使用的时候需要通过__forwarding指针访问变量
这个地方一开始我觉得多此一举,因为直接访问地址不行么,为啥还要加个__forwarding指针“中转”一下,后来也是查阅了一些资料:
其实这样做的目的是防止栈中的block在出栈的时候,其所持有的变量也会被回收,这样在栈中所做的修改也不会保存到堆里,__block也就没了意义。所以在block被copy到堆中的时候,其实栈中的__forwarding的指向也随之改变,和堆中的__forwarding同时指向了堆中的实例(Block_byref_a_0)。
这样无论是栈中的 block 或者堆中的 block,各自使用 __forwarding 来修改变量,就都可以保留下来了。
在栈中的时候大致是这样的情况:
image.pngcopy到堆中的时候的情况:
image.png- 增加了两个函数 copy 和 dispose
static void __BlockTest__test_block_copy_0(struct __BlockTest__test_block_impl_0*dst, struct __BlockTest__test_block_impl_0*src) {
_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
static void __BlockTest__test_block_dispose_0(struct __BlockTest__test_block_impl_0*src) {
_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);
}
这两个函数类似于MRC 中retain和release的效果。
- __BlockTest__test_block_desc_0_DATA 的sizeof 计算block的成员增加了
__block 前 :
__BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0)};
__block 后 :
__BlockTest__test_block_desc_0_DATA = { 0, sizeof(struct __BlockTest__test_block_impl_0), __BlockTest__test_block_copy_0, __BlockTest__test_block_dispose_0};
- 变量声明时的变化
__block 前 :int a = 6;
__block 后 :__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 6};
后面增加的这些就是为了给__Block_byref_a_0填充数据,以备后续修改。
- 初始化block的变化
__block 前 :
TestBlockExample block1 = ((void (*)())&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, a));
__block 后 :
TestBlockExample block1 = ((void (*)())&__BlockTest__test_block_impl_0((void *)__BlockTest__test_block_func_0, &__BlockTest__test_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
真正的变化就是 最后一个参数 ‘a’ 变成了 ‘(__Block_byref_a_0 *)&a, 570425344’
不过这后面的 ‘570425344’ 我一直不明白是什么意思。
- 修改变量的变化
__block 前 :a = 8;
__block 后 :(a.__forwarding->a) = 8;
a.__forwarding指向了自身。
在block构造的时候我们用到了a.__forwarding,
在使用的时候(__BlockTest__test_block_func_0)也是通过(a->__forwarding->a)这样的方式找到最终修改过的数值‘8’。
能做到这样的原因上面已经说过了,这里复述一下:
栈上的__block变量复制到堆上时,会将成员变量__forwarding的值替换为复制到堆上的__block变量用结构体实例的地址。所以“不管__block变量配置在栈上还是堆上,都能够正确的访问该变量”,这也是成员变量__forwarding存在的理由。参照上面的图。。。
好了,到这里__block的作用已经讲的差不多了,剩下的更深层次的东西,暂时不知道怎么入手,其实关于block的知识还有很多,比如在MRC的情况下是怎么样的,还有静态变量和全局变量之类的,本文都没有讨论,下次有机会在研究吧,另外下面参考的几篇文章中也有相关的介绍。
参考的相关资料
Block源码解析和深入理解
关于Block再啰嗦几句
深入理解Block之Block的类型
深入研究Block捕获外部变量和__block实现原理
iOS Block源码分析系列(二)————局部变量的截获以及__block的作用和理解
网友评论