【译】《A look inside blocks Episode 3 (Block_copy)》

422 阅读6分钟

原文作者:Matt Galloway

原文地址:www.galloway.me.uk/2013/05/a-l…


这篇文章姗姗来迟。我写了几个月的初稿,但是我一直忙于写我的书《Effective Objective-C 2.0》(才知道是这本书的作者),没有时间完成这篇文章。但是现在我写完了,一起来看看吧。

第一部分第二部分对block的内部探究后,这篇文章将更深入的探究当block被copy时发生了什么。你很可能听过”block从栈开始“,”如果想保存下来在后面的程序用到block你需要copy“。但是为什么?实际上当copy时发生了什么?我一直想知道block在copy时的机制是什么。例如:block对捕获的值做了什么?在这片文章中,我将探究一下。


目前为止的探究成果

第一部分第二部分中,我们得出内存中block的结构大致如下:

block_layout

第二部分中我们发现这个结构体是在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。这是的使得函数对传递一个NULLblock也是安全的。

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。但是如果你感兴趣的话,仍然有大量有趣的深度资料。


相关系列