原文作者:Matt Galloway
这篇文章姗姗来迟。我写了几个月的初稿,但是我一直忙于写我的书《Effective Objective-C 2.0》(才知道是这本书的作者),没有时间完成这篇文章。但是现在我写完了,一起来看看吧。
继第一部分和第二部分对block的内部探究后,这篇文章将更深入的探究当block被copy时发生了什么。你很可能听过”block从栈开始“,”如果想保存下来在后面的程序用到block你需要copy“。但是为什么?实际上当copy时发生了什么?我一直想知道block在copy时的机制是什么。例如:block对捕获的值做了什么?在这片文章中,我将探究一下。
目前为止的探究成果
从第一部分和第二部分中,我们得出内存中block的结构大致如下:
在第二部分中我们发现这个结构体是在block被初始引用时在栈上创建的。一旦存在栈上,在block的封闭作用域之后,这块内存可以被重置。所以在你之后想使用block时怎么办?对,你可以copy它。因为block是一个Objective-C对象,可以通过调用Block_copy
方法或者直接像block发送Objective-C的消息copy
。这里仅调用Block_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()
仅是一个#define
定义,将传入的参数强制转换为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;
}
下面列出了这个方法做了什么:
1、如果参数(arg)传入了NULL
那么直接返回NULL
。这是的使得函数对传递一个NULL
block也是安全的。
2、如果参数传递给struct Block_layout
类型的指针。你也许会想起在第一部分中这些是什么。这是组成block的内部数据结构,包括block的实现函数的指针和各种元数据位。
3、如果block的flags
中包含了BLOCK_NEEDS_FREE
,那么这就是一个堆block(一会就可以看到)。在这种情况下,所需要做的就是引用计数+1并且返回相同的块。
4、如果是一个全局block(global block)(回忆一下第一部分中的内容),除了将相同的block返回,没有需要做的。这是因为全局block实际上是一个单例。
5、如果我们到达了注释5处,那么block一定是一个在占空间分配的块。在这种情况下,block需要copy到堆上。这部分很有趣。首先,使用malloc()
去创建一块所需大小的内存。如果创建失败,返回NULL
,否则继续向下进行。
6、在这里,memmove()
函数用于逐位地将当前分配在栈上的block复制到上一步我们为堆block分配的内存空间上。这保证所有的原数据例如block的descriptor都可以被拷贝。
7、下一步,block的flags标识被更新。第一行确保引用计数被设置为0。注释表明这一步不是必要的,大概因为,此时引用计数应该已经是0了。我猜想这一行被留在这儿是为了防止有某些情况下应用计数不为0的bug。下一行设置BLOCK_NEEDS_FREE
标识。这表明,这是一个堆上的block,并且一旦应用计数减为0,存放block的内存需要释放。这一行的| 1
(与1)操作,将block的引用计数设为1。
8、这里,block的isa
指针被设为_NSConcreteMallocBlock
,这意味着这是一个堆block。
9、最后,如果这个block有一个copy helper函数,这个函数将被创建。如果有需要(比如当需要block捕获对象时),编译器会创建copy helper函数。在block捕获对象的场景下,copy helper函数需要retain捕获到的对象。
嗯!这太巧妙了!现在你知道了一个block是如何被复制的。但这只完成了一半的工作,对吧?一个block是如何被释放的呢?
Block_release()
Block_copy()
的另一半是Block_release()
。同样,其实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);
}
}
1、首先,参数被转换为一个struct Block_layout
类型的指针,因为他就是这个类型的。如果参数传入NULL
,我们会提早返回以确保传入NULL
时也是安全的。
2、这里block的用于指明引用计数的标识部分(回想下Block_copy()
中标识被设置为指明引用计数为1的部分)会减少。
3、如果新的引用计数大于0,这表明还有一些对象引用这个block,所以这个block还不能被释放。
4、相反,如果标识flags包含BLOCK_NEEDS_FREE,并且满足堆block和引用计数为0的条件,这个block就会被释放。首先,block的dispose helper函数会被调用。这是block的copy helper的反义词。这个函数的作用相反,例如释放任何捕获的对象。最终,block通过使用
_Block_deallocator函数被销毁。如果你在[
runtime.c]([]())中寻找相关代码,你会发现这里是一个
free函数指针,这个函数释放了
malloc`分配的内存后。
5、如果我们成功了,并且是一个global block,那么什么都不做。
6、如果我们走到了注释6的位置,那么会发生一些异常的情况,因为一个stack block视图被释放,一条日志会打印出来提醒开发者。实际上,你不应该看到这条信息。
就是这样。没有更多东西了。
下一步
我窥探block的旅程到此为止。有一些资料在我的《Effective Objective-C 2.0》书中。那些资料是关于如何有效地使用block。但是如果你感兴趣的话,仍然有大量有趣的深度资料。