Block 底层原理探析
Block 是 iOS4 之后添加的一种语法结构,也成为闭包,或者匿名函数。在 iOS 中被广泛的使用,著名的第三方库也大量用到此特性,如 AFNetworking,SDWebimage 等。
下文将介绍几个方面
Block 的基础用法
Block 底层表示
Block 的变量捕获
__block 变量底层描述
OC 中的 Block 3 种类型
Block 的 copy 相关的理解
__block 的 __forwarding
Block 语法
当做局部变量
当做属性
当做方法参数
调用方法
typedef
Block 底层
我们可以使用 clang 的 rewrite 指令来生成 C/C++ 描述来供我们研究 Block 底层实现原理,首先先写一个最简单的 Block
转换后的代码
整理下,里面有几个结构体,第一个 `__main_block_func_0`
这个比较简单,从代码可以看出是我们 block 里面执行语句,一个执行函数,参数是 __cself 类型是 __main_block_impl_0 也就是我们 block 本身,下文会提到。
第二个是`__main_block_desc_0` 一个 block 的描述结构体,其中有2个字段:
reserved:保留字段。
Block_size block 大小。
第三个是`__block_impl`,这个是 block 对象。其中哟几个字段
isa:类似于类的 isa 的指针,模拟对象。在 ARC 下有3种类型分别是`_NSConcreteStackBlock`,`_NSConcreteMallocBlock`,`_NSConcreteGlobalBlock`
Flags:标志位,有以下几个
Reserved:保留位
FuncPtr:block 执行的函数指针地址。
看完以上3个结构在来看这个`__main_block_impl_0`就比较简单了。此结构体包含了一个`__block_impl`,`__main_block_desc_0`结构体,
还有一个初始化方法,代码比较简单,只是赋值相应的字段。
终于进入到我们的 `main`函数了
首先声明了一个函数指针 block 也就是我们 OC 代码中的
然而右边转换成了
这句话的意思是,让 block 这个变量的指针指向新建生成的`__main_block_impl_0`结构体变量。它调用的构造器并且传入 `__main_block_func_0`(函数地址),`__main_block_desc_0_DATA`(描述字段)
下一句也就是我们执行的 OC 代码
被转换成
这里比较难理解,我们忽略多余的类型转换这里取出了 block 指针的第一个成员(也就是`__main_block_impl_0`中的`impl`,因为指针类型是`__block_impl`,也就是在这个地址连续取`__block_impl`的大小,又因为`impl`是第一个结构体成员,所以取出`impl`),之后调用了`impl.FuncPtr`block 实现函数指针,传入参数 block,也就是自己。
这里我们上文提到
这里有个参数是`__cself`,类型也是`__main_block_impl_0`,也就是传入 block 自己本身的结构体,这不难理解,联想面向对象语言中的方法第一个参数都会是`self`,这也是 block 模拟对象的原理之一,当然还有更大用处,这里后文提到。
有一个点需要注意一下, block 结构体有一定的命名规律(__xxx_block_impl_y:这里的 xxx 是 block 名称,y 是该函数出现的顺序值,**如果 block 是匿名的则会是当前作用域的函数名**)。
变量捕获
我们一直都知道,在 Block 中基础类型的变量会被拷贝值,而指针变量则会捕获指针变量,并且强引用,那么这到底是怎么回事呢?我们一起来研究一下
写一个捕获外部变量的 block
改写之后变成如下
我们看到`__main_block_impl_0`多了2个变量,一个是 i 一个是 指针 n,这和我们捕获的变量是一致的,构造函数中初始化这 2 个值,也就是在底层,对于基础类型,内部会维护一个相应的基础类型变量,对于对象,则内部也会有指向这个对象的指针(默认为强引用)。
对于代码中出现的 copy 和 dispose 下文我们会提到,可以先忽略它,
__block 底层描述
如果想在 block 中改变一个外界变量的值,那么这个变量必须声明为 `__block`,这在底层是怎么实现的呢,我们一起来从代码说话。
写一个`__block`的例子
改写后的代码
我们注意到多了 2 个结构体分别是`__Block_byref_i_0`,`__Block_byref_n_1`。i 为基础类型,n 为对象类型,
在底层`__block`修饰的变量会被转换为`__Block_byref`结构体,
其中分别有
__isa:isa 指针,模拟对象特性
__forwarding:转发对象,后文会提到。
__flags:标志位
__size:大小
如果是对象类型,会多出`__Block_byref_id_object_copy`和`__Block_byref_id_object_dispose` 这 2 都是和 copy 相关的操作,
和变量捕获一样,会维护一个内部变量。
于捕获不同`__main_block_impl_0`这时候维护的变量会变成`__Block_byref`结构体变量(__Block_byref 内部也维护了一个变量)。构造函数在构造的时候会把 __forwarding 会指向外界的 `__block`对象
在来看执行函数的部分代码
可以看出,执行部分也变了,直接将`__forwarding`里面所维护的变量拿出来设置值,从而达到在 block 体内修改变量的作用。
**为什么要有`__forwarding`,这是个很重要的概念,下文我们在 blcok 的拷贝部分会同意处理**
Block 的 3 种类型
我们可以写一段代码来测试一下 block 的类型
输出的 log 为
由此我们可以得出结论
_NSConcreteStackBlock:
只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。
StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了。
_NSConcreteMallocBlock:
有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制
_NSConcreteGlobalBlock:
没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束。
ARC 下的 Block 类型
在 ARC 下,任何的 block 对**强引用变量赋值操作**将会触发 block 的 copy 操作,如上文中 第二个 block 的输出,照规则来说应该是 `_NSConcreteStackBlock`
,但是此 block 赋值给了 block 变量,所以这时候会变成了`_NSConcreteMallocBlock`,如果 block 变量由`__weak`修饰,那么不会发生 copy 也就是还是原来的`_NSConcreteStackBlock`。
Block 的 copy 相关
在变量捕获那一部分,我们看到了 copy 和 dispose 相关的内容,这些内容是拷贝和释放相关的部分内容,我们一起来探讨一下。
copy
拷贝方法我们可以在系统库中看到`_Block_copy`方法,方法也是开源[点击查看](http://opensource.apple.com/source/clang/clang-137/src/projects/compiler-rt/BlocksRuntime/runtime.c),
内部调用了 `_Block_copy_internal`方法
简化版`_Block_copy_internal`
可以看出内部处理手段,通过 Block 的 descriptor 获取 Block 大小,之后直接 malloc 在堆上创建一个新个 block ,之后使用`memmove`拷贝内存空间,将 isa 设置为 `_NSConcreteMallocBlock` ,最后如果是 Block 是 copy 的话,调用 `block->descriptor->copy`。[简化版代码出处](http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/)。
`block->descriptor->copy` 是什么玩意呢,
在 block 的 descriptor 中,新增了 2 个函数指针 copy 和 dispose,**指向相应 block 的捕获变量的 assign(基础类型) 和 retain (对象)这也是为什么 block 会循环引用所在**
copy 这个指针最后会调用`_Block_object_assign`,根据不同类型,会传入不同 flags调用不同方法
如果 block 里面引用了对象类型会传入`BLOCK_FIELD_IS_OBJECT`。最终会调用`_Block_retain_object`来 retain 对象,使用`_Block_assign`来赋值。
如果 block 里面引用了 Block 会传入`BLOCK_FIELD_IS_BLOCK`最终也使用`_Block_copy_internal`来进行 copy
如果 block 里面引用了`__block`变量,会传入`BLOCK_FIELD_IS_BYREF`,使用`_Block_byref_assign_copy`来设置 forwarding 指向,这个我们后面说。
dispose
释放过程的简化版的代码如下
代码比较简单,最终也可以看到会调用`Block->descriptor->dispose`,这和 copy 是相对应的,可以自己去挖掘一下,这边就不写出来了。
__Block 中的 __forwarding
前文`__block`部分我们会发现出现`__forwarding`这个东西,既然存在必然有存在的意义,`__forwarding`在初始化的时候我们可以看到是指向自己的。
这里有几个情况,
当 block 是 _NSConcreteStackBlock 的时候,这个时候`__forwarding`指向自己,在执行的函数中,使用 __forwarding->xxx 访问变量。或者设置 __forwarding->xxx 来修改 __block 指向的对象,则也是为什么 __block 可以修改外界变量的原因。
当一个 block 被 copy 情况下呢,如果这个时候栈上的 block 已经出了作用域,如果 __forwarding 还是自己,那么必然指向已经释放的地址,这不是我们要的结果,上一节中,谈到过 Block 被 copy 会 malloc 出一片新空间,那么 __block 变量也会被 copy,这时候将 __forwarding 指向堆上的那个 block 的 __forwarding,这时候 __forwarding->xxx 就是访问堆上的新的变量,即使出了作用域,我们的结果也是正确的。
下面有一张图可以加深理解
那么 __forwarding 是如何被修改的呢?
还记得上一下小结提到的`_Block_byref_assign_copy`吗。我们可以看下实现。
可以看出 __block 在 copy 的时候会设置 __forwarding
小结
Block 的在提供便利性的同时也会引入循环引用等问题,我们需要知道如何合理使用 Block。当然 Block 还有大量内容需要去研究。
版权声明: 本文为 InfoQ 作者【Damien】的原创文章。
原文链接:【http://xie.infoq.cn/article/a1b0a4c67f1c37650b5123ab8】。文章转载请联系作者。
评论