在第一篇文章和第二篇文章我们已经研究了一些blocks的内部原理了。本文将进一步研究block拷贝的过程。你可能听到过一些术语比如"blocks 起始于栈"以及"如果想保存它们以后用你必须拷贝"。但是为什么呢?拷贝到底做了什么事?我长久以来一直在好奇拷贝block的机制到底是什么。比如block捕获的值会怎么样。本文我将对此做些阐述。
从第一篇文章和第二篇文章中我们知道一个block的内存布局长这样:
在第二篇文章中我们看到block最开始被引用的时候是在栈上创建的。既然是在栈上,那么在block的封闭域结束后内存就会被回收重用。那你之后再用这个block会发生什么呢?好吧,你必须拷贝它。这是通过调用
方法或者直接向他发送OC的
- Block_copy()
消息完成。这就是所谓的
- 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()
的实现在runtime.c:
- _Block_copy()
- void * _Block_copy(const void * arg) {
- return _Block_copy_internal(arg, WANTS_ONE);
- }
所以也就是调用
方法,传入block自己和
- _Block_copy_internal
。为了明白这什么意思,我们需要看一下实现代码。也在runtime.c。下面是方法的实现,已经删掉不想干的部分(主要是垃圾收集的部分):
- WANTS_ONE
- 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
的Block。
- NULL
类型的指针。你也许还记得第一篇文章中提到它。它就是block内部一个包含了实现函数和一些元数据的数据结构。
- struct Block_layout
字段包含
- flags
,那么这是一个堆block(稍后你就明白)。这里只需要增加引用计数然后返回原blcok。
- BLOCK_NEEDS_FREE
创建一块特定的内存。如果创建失败,返回
- malloc()
;否则,继续。
- NULL
方法将当前栈上分配的block按位拷贝到我们刚刚创建的堆内存上。这样可以保证所有的元数据都拷贝过来,比如descriptor。
- memmove()
标志位,表明这是一个堆block,一旦引用计数减为0,它所占用的内存将被释放。
- BLOCK_NEEDS_FREE
操作设置block的引用计数为1。
- |1
指针被设置为
- isa
,说明这是个堆block。
- _NSConcreteMallocBlock
哈哈,已经十分清晰了。现在你知道拷贝一个block到底发生了什么事!但那只是图片展示的一半内容,对吧?释放一个block又会怎么样呢?
图的另一半是
- 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);
- }
}
```
这段代码做了这些事:
的指针。如果传入NULL,直接返回。
- struct Block_layout
中标志位操作代表的是引用计数置为1)。
- Block_copy()
,那么这是一个堆block而且引用计数为0,应该被释放。首先block的处理辅助函数(dispose helper)被调用,它是拷贝辅助函数(copy helper)的反义词,执行相反的操作,比如释放被捕获的对象。最后调用
- BLOCK_NEEDS_FREE
方法释放block。如果你查找runtime.c你就会发现这个方法最后就是一个
- _Block_deallocator
的函数指针,释放
- free
分配的内存。
- malloc
这些就是Block! 东西也并不多嘛(呵呵)。
来源: https://juejin.im/post/5a02ec8cf265da432717a89b