Block窥探:第三篇(Block_copy)
原文地址:http://www.galloway.me.uk/2013/05/a-look-inside-blocks-episode-3-block-copy/
如原作者发现有侵权行为可责令我在24小时之内删除,前提是你能看到。
因为我一直在忙于我自己的书(就是那本大名鼎鼎的《Effective Objective-C 2.0 提高效率的52个tips》)的事,所以一直没顾上这篇文章,距第二篇已经过了好个月,但是我还是将它完成了!
通过我的第一篇文章和第二篇 文章我们已经对block进行了深入了解,这篇文章主要来深入研究block被copy的时候到底做了什么。你可能听过这样一句术语“block一开始都是存储在栈内存上,如果你需要一直使用它那么你必须对block进行copy操作”。但为什么要这样做呢?在copy的时候到底发生了什么?对于block捕获的外部变量又发生了什么?这个问题也困扰了我很久,这篇文章咱们来整一整。
目前为止我们知道的
通过第一篇和第二篇我们知道了block在内存中的结构是下面这样的:
block内存布局通过上一篇文章我们了解到,当block捕获到一个外部非对象类型的变量时,一开始是创建在栈内存上的,因为栈内存是会被系统回收重复使用的,所以如果我们想一直使用这个block的话就需要调用Block_copy()函数,类似于Objective-C中对block发送一条copy消息,因为block可以被看成是一个OC对象,发送copy消息也会去调用Block_copy()函数。
那最好来看看Block_copy()函数做了什么。
Block_copy()
首先我找到了Block.h文件中关于Block_copy的定义:
Block_copyBlock_copy()原来就是一个宏定义,将传入的参数强转为const void *类型然后传给_Block_copy()函数。在runtime.c
文件中有_Block_copy()函数实现的原型:
_Block_copy()
函数内部调用了_Block_copy_internal()函数,_Block_copy_internal()有两个参数,第一个是block自己,第二个是WANTS_ONE,在runtime.c
文件中看看WANTS_ONE的实现过程。我将函数中一些和垃圾回收相关的东西省略了,方便我们阅读:
_Block_copy_internal()
下面来解释一下这个函数做了些啥:
1.如果传入的block参数为空,则函数返回NULL,这是一段防御代码。
2.将参数arg强转为Block_layout *类型,第一篇中我们讲到过Block_layout,他由一个实现函数和一系列元数据共同构成了block的内部结构。
3.如果block中断flags标记位包含BLOCK_NEEDS_FREE那么该block就是一个堆block。这里需要做的是对block的引用计数加一并将block返回。
4.如果block是global block那么直接将block返回。因为global block是个单例。
5.如果能走到第五步,那这个block就是分配在栈上的block,需要将栈block拷贝到堆中,首先调用malloc()分配size大小的内存空间,如果分配失败则返回NULL。
6.memmove()函数将分配在栈中的block按位拷贝至刚刚在堆上分配的内存中。按位拷贝可以确保block中的所有元数据都能准确的进行拷贝,例如block的descriptor。
7.这一步是更新block的标志位,第一行确保block的引用计数变为0,后面的注释说这句操作不是必须的,可能因为在这个地方引用计数已经是0了,加上这句代码是为了防御某种情况下引用计数不为0的bug。第二行是设置BLOCK_NEEDS_FREE标记位,这个标记位表明block是一个堆block,当引用计数变为0时,内存会被释放掉,后面紧跟的| 1将block的引用计数置为1。
8.将isa指针置为_NSConcreteMallocBlock,表明这是一个堆block。
9.如果block还有copy helper函数(上一篇末尾提到的__block_descriptor_tmp结构中的__copy_helper_block和__destroy_helper_block这两个函数指针)那么也会调用它相关的copy helper函数,他们会对block捕获的对象进行retain和release操作。
这看起来是不是非常的酷,现在你已经知道block是怎么被copy的,但这只是一部分,那它是怎么被release的呢?
Block_release()
Block_release()实际上也是一个宏定义:
Block_release()和Block_copy()函数一样,Block_release()也将传入的参数类型进行强转,然后调用_Block_release。这样做其实是为了方便开发者使用,不用自己再去转换参数类型。
让我们来看看_Block_release()函数的实现(和之前一样,我也对代码进行了优化,删除了垃圾回收相关的部分,方便阅读):
下面是每行代码的分析:
1.将传入的参数强转为struct Block_layout类型,这也是传入的block的真实类型。然后对传入的参数为NULL的情况作了防御。
2.将block的引用标记位减1,还记得在Block_copy()中对引用计数加了1吗。
3.如果block的引用计数还大于0,那说明还有人对block引用,block现在还不能被释放。
4.如果flags标记位中包含BLOCK_NEEDS_FREE,那么表明这是一个堆block,并且block的引用计数是0,所以这个block应该被释放。这个时候block的dispose helper函数(__copy_helper_block)会被调用。这个函数的作用和copy helper函数(__destroy_helper_block)的作用刚好相反,是用来释放block锁捕获的对象的。当block所捕获的外部对象被释放以后,通过调用_Block_deallocator函数来将block释放,如果你在runtime.c
文件中查找,你会发现该函数的尾部是一个指向free的函数指针,也就是释放掉malloc分配的内存。
5.走到这里说明block是一个global block,所以什么也不用做。
6.如果代码执行到这里了,会发生一些奇怪的事情:因为正在尝试将栈上的block释放掉,所以这行代码是为了提醒开发者的。在程序实际运行过程中,你最好不要看到这个提示。
这就是我要讲的全部了,但block的内容不仅仅只有这些。
下一篇讲啥?
到目前为止我的block系列就告一段落了,其中有的内容参考了我的书(Effective Objective-C 2.0)。这本书中还有更多的如何高效地使用block的内容,如果你对block感兴趣的话这部分内容是你进行深入研究的很好的资料。