在第一篇文章和第二篇文章我们已经研究了一些blocks的内部原理了。本文将进一步研究block拷贝的过程。你可能听到过一些术语比如”blocks 起始于栈”以及”如果想保存它们以后用你必须拷贝”。但是为什么呢?拷贝到底做了什么事?我长久以来一直在好奇拷贝block的机制到底是什么。比如block捕获的值会怎么样。本文我将对此做些阐述。
我们已经知道的事
从第一篇文章和第二篇文章中我们知道一个block的内存布局长这样:
在第二篇文章中我们看到block最开始被引用的时候是在栈上创建的。既然是在栈上,那么在block的封闭域结束后内存就会被回收重用。那你之后再用这个block会发生什么呢?好吧,你必须拷贝它。这是通过调用Block_copy()
方法或者直接向他发送OC的copy
消息完成。这就是所谓的Block_copy()
。
Block_copy()
首先我们来看Block.h。其中有下面的定义:
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__)))
void *_Block_copy(const void *arg);
所以Block_copy
是一个宏,它将传入的参数转换为一个const void *
然后传递给_Block_copy()
方法。_Block_copy()
的实现在runtime.c:
void *_Block_copy(const void *arg) {
return _Block_copy_internal(arg, WANTS_ONE);
}
所以也就是调用_Block_copy_internal
方法,传入block自己和WANTS_ONE
。为了明白这什么意思,我们需要看一下实现代码。也在runtime.c。下面是方法的实现,已经删掉不想干的部分(主要是垃圾收集的部分):
static void *_Block_copy_internal(const void *arg, const int flags) {
struct Block_layout *aBlock;
const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE;
// 1
if (!arg) return NULL;
// 2
aBlock = (struct Block_layout *)arg;
// 3
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
// 4
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
// 5
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return (void *)0;
// 6
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 7
result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 1;
// 8
result->isa = _NSConcreteMallocBlock;
// 9
if (result->flags & BLOCK_HAS_COPY_DISPOSE) {
(*aBlock->descriptor->copy)(result, aBlock); // do fixup
}
return result;
}
主要做了以下工作:
-
如果传入参数是
NULL
就直接返回NULL
。防止传入一个NULL
的Block。 -
将参数转换为一个
struct Block_layout
类型的指针。你也许还记得第一篇文章中提到它。它就是block内部一个包含了实现函数和一些元数据的数据结构。 -
如果block的
flags
字段包含BLOCK_NEEDS_FREE
,那么这是一个堆block(稍后你就明白)。这里只需要增加引用计数然后返回原blcok。 -
如果这是一个全局block(回看第一篇文章),那么不需要做任何事,直接返回原block。因为全局block是一个单例。
-
如果走到这里,那么这一定是一个栈上分配的block。那样的话,block需要拷贝到堆上。这才是有趣的部分。第一步,调用
malloc()
创建一块特定的内存。如果创建失败,返回NULL
;否则,继续。 -
调用
memmove()
方法将当前栈上分配的block按位拷贝到我们刚刚创建的堆内存上。这样可以保证所有的元数据都拷贝过来,比如descriptor。 -
接下来,更新标志位。第一行确保引用计数为0。注释表明这行其实不需要——大概这个时候引用计数已经是0了。我猜保留这行是因为以前有个bug导致这里的引用计数不是0(所以说runtime的代码也会偷懒)。下一行设置了
BLOCK_NEEDS_FREE
标志位,表明这是一个堆block,一旦引用计数减为0,它所占用的内存将被释放。|1
操作设置block的引用计数为1。 -
block的
isa
指针被设置为_NSConcreteMallocBlock
,说明这是个堆block。 -
最后,如果block有一个拷贝辅助函数,那么它将被调用。必要的时候编译器会生成拷贝辅助函数。比如一个捕获了对象的block就需要。那么拷贝辅助函数将持有被捕获的对象。
哈哈,已经十分清晰了。现在你知道拷贝一个block到底发生了什么事!但那只是图片展示的一半内容,对吧?释放一个block又会怎么样呢?
Block_release()
Block_copy()
图的另一半是Block_release()
。实际上这又是一个宏:
#define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
跟Block_copy()
一样,Block_release()
也是转换传入的参数然后调用一个方法。这一定程度上解放了程序员的双手,他们不用自己做转换。
我们来看看_Block_release()
的源码(简明起见,重新整理了代码顺序,并删除了垃圾回收相关的代码):
void _Block_release(void *arg) {
// 1
struct Block_layout *aBlock = (struct Block_layout *)arg;
if (!aBlock) return;
// 2
int32_t newCount;
newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK;
// 3
if (newCount > 0) return;
// 4
if (aBlock->flags & BLOCK_NEEDS_FREE) {
if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock);
_Block_deallocator(aBlock);
}
// 5
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
;
}
// 6
else {
printf("Block_release called upon a stack Block: %p, ignored\n", (void *)aBlock);
}
}
这段代码做了这些事:
-
首先,参数被转换为一个指向
struct Block_layout
的指针。如果传入NULL,直接返回。 -
标志位部分表示引用计数减1(之前
Block_copy()
中标志位操作代表的是引用计数置为1)。 -
如果新的引用计数值大于0,说明有其他东西在引用block,所以block不应该被释放。
-
否则,如果标志位包含
BLOCK_NEEDS_FREE
,那么这是一个堆block而且引用计数为0,应该被释放。首先block的处理辅助函数(dispose helper)被调用,它是拷贝辅助函数(copy helper)的反义词,执行相反的操作,比如释放被捕获的对象。最后调用_Block_deallocator
方法释放block。如果你查找runtime.c你就会发现这个方法最后就是一个free
的函数指针,释放malloc
分配的内存。 -
如果到这一步且lock是全局的,什么也不做。
-
如果到这一步,一定是发生了未知状况,因为一个栈block试图在这里释放,输出一行警告。实际上,你应该永远不会走到这一步。
这些就是Block! 东西也并不多嘛(呵呵)。