Block 的存储域

本文主要在 MRC 和 ARC 环境下,通过实例来分析block在内存中的存储位置,阅读本文的读者需要提前了解block的相关知识和使用技巧。

我们先定义一个Block_t类型:

typedef void (^Block_t)(void);

然后再定义一个不捕获任何外部变量的block,尝试去打印这个block isa 指针指向的类型:

Block_t t = ^{
    NSLog(@"I'm a block.");
};

Class cls = object_getClass(t);
NSLog(@"%@", cls);  // __NSGlobalBlock__

无论在 MRC 还是在 ARC 环境下,能得到的结果都是__NSGlobalBlock__。这个__NSGlobalBlock__是什么东西呢?使用以下方式打印cls的各级父类:

Class superCls      = class_getSuperclass(cls);
Class superSuperCls = class_getSuperclass(superCls);
Class rootCls       = class_getSuperclass(superSuperCls);

NSLog(@"%@", superCls);         // __NSGlobalBlock
NSLog(@"%@", superSuperCls);    // NSBlock
NSLog(@"%@", rootCls);          // NSObject

可以发现__NSGlobalBlock__NSBlock的一个子类,而NSBlockblock基于Cocoa的一层封装。在blcok的实现层来看,LLVM 为其给出了如下的结构体形式:

struct Block_literal_1 {
    void *isa;  //  初始化为 &_NSConcreteStackBlock 或 &_NSConcreteGlobalBlock
    int flags;                      // 标志位
    int reserved;                   // 占位用
    void (*invoke)(void *, ...);    // block 的实现函数指针
    struct Block_descriptor_1 {     // block 的附加描述信息
        ...
    } *descriptor;
    // imported variables
};

这与我们使用clang -rewrite-objc分析出来的源码有些不一致,但其内存布局基本相同(相关源码中结构体的名称叫Block_layout)。由于block也会被当做对象看待,该结构体中的isa指针需要指向其所属类型,那么_NSConcreteStackBlock就表明了block的具体类型。在 libclosure 源码中还能找到其他类型的blockblockisa指针始终指向下面这些指针数组的首地址,该指针也决定了block的类名称。

BLOCK_EXPORT void * _NSConcreteMallocBlock[32];
BLOCK_EXPORT void * _NSConcreteAutoBlock[32];
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32];
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32];
// declared in Block.h
// BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
// BLOCK_EXPORT void * _NSConcreteStackBlock[32];

其中_NSConcreteFinalizingBlock_NSConcreteWeakBlockVariable_NSConcreteAutoBlock只在 GC 环境下使用,我对这个也不太了解,暂且不讨论。

因此,根据block命名规则来看block的存储域大致有 3 个地方:全局区(数据区域 .data 区)栈区堆区

接下来,我们要根据各种实例分析block的存储域,这里使用的打印block的方式而非用clang -rewrite-objc命令分析,原因是后者只是对源码的一种改写,并不能真正反映blcok存储域的变化,blockisa指针在这种情况下永远只会被初始化成_NSConcreteStackBlock或者_NSConcreteGlobalBlock

在上面的实例中,一个不捕获任何外部变量的block被存放在全局区。关于在全局区的block,我觉得还可以补充一点,block作为全局变量并初始化时,无论是否捕获外部变量,在 MRC 和 ARC 环境下都会被存放在全局区,并且对处于全局区的block进行 copy 操作是无效的(后面会解释到)。以下代码可以进行验证。

int a = 1;

Block_t t = ^{
    NSLog(@"I'm a block.");
};

Block_t t1 = ^{
    a = 2;
    NSLog(@"I'm a block too.");
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"%@", t);            // <__NSGlobalBlock__: 0x100001050>
        NSLog(@"%@", t1);           // <__NSGlobalBlock__: 0x100001090>
        NSLog(@"%@", [t1 copy]);    // <__NSGlobalBlock__: 0x100001090>
    }
    return 0;
}

当然,还有 3 种情况下的block会被放置在全局区中,这一部分文章后面会具体分析。

那么捕获了外部变量的block会被存放在哪里?这个问题需要分几种情况具体分析。

我们先来看block捕获了一个auto局部变量的情况,代码如下:

{
    int a = 1;
    Block_t t = ^{
        NSLog(@"I'm a block. %d", a);
    };
    
    NSLog(@"%@", t);
}

在 MRC 和 ARC 中分别打印如下:

MRC: <__NSStackBlock__: 0x7ffeefbff558>
ARC: <__NSMallocBlock__: 0x100443280>

在 MRC 环境中t被存放在栈区,这个不难理解。除了之前提到过的全局区block在初始化时会被放置在全局区(impl.isa = _NSConcreteGlobalBlock),在其他情况下定义并初始化block都会被放置在栈区(impl.isa = _NSConcreteStackBlock)。然而在 ARC 环境中t并不在栈中,它被放置于堆区,这是我们第一个遇到的存放于堆区里的block(impl.isa = _NSConcreteMallocBlock)。block结构关于isa的注释里明确表示isa指针只会被初始化为_NSConcreteGlobalBlock_NSConcreteStackBlock,那么_NSConcreteMallocBlock一定是在运行时才存在的一种状态。libclosure 源码的runtime.c文件中的_Block_copy函数实现了更改block isa指针的操作:

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
        struct Block_layout *result = malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

以上代码表明了对一个block执行 copy 操作需要进行哪些操作。第一个 if 很简单,如果入参为空则返回空即可。前面提到的blcok结构体重有一个flags标志位,这个成员变量记录着block的状态和其引用计数,一个变量记录多种信息在 Apple 的源代码中很常见。在这个函数中,如果flags标志位包含BLOCK_NEEDS_FREE,表明该block存在于堆中,因此所需要做的就是增加其引用计数,返回原地址即可。如果block的标志位包含BLOCK_IS_GLOBAL,说明其存在于全局区,直接返回原来的block即可。最后一种情况就是block在栈中,需要重新开辟一块内存空间将原来的block的成员变量和函数地址全部复制到新内存空间,并重新设置其flags,接着更改isa指针类型为_NSConcreteMallocBlock,最后返回新block的首地址。

分析到这里可以发现,如果一个block是一个堆 block(这样称呼可能会比block被存放在堆区更简洁好听一些),那么它可能是从栈上 copy 过来的。这真是句没用的废话,不过这能解释之前的疑问,为什么 ARC 环境下t是一个堆 block?原因是在 ARC 中,大多数情形下编译器会自动将block copy 到堆中,也就是编译器自己帮我们 copy 一个block的副本,我们使用的是它的副本,并不是原来的block

上面的例子还能引申出另外一种情况,如果block同时捕获了auto局部变量和全局变量,它又会在哪里,还和上面那个例子一样么?

int g_a = 1;

int main(int argc, const char * argv[])
{
    int b = 2;
    Block_t t = ^{
        NSLog(@"a = %d, b = %d", g_a, b);
    };
    
    NSLog(@"%@", t);
    return 0;
}

答案很简单,确实是一样的。MRC 中t栈 block,ARC 中t堆 block,它一定不会是全局 block,因为在使用全局变量的地方不能使用auto变量。

现在我想抛出两个问题。

第一,如果t里只捕获了那个全局变量g_at会是什么类型的block呢?
答:当然是全局 block了。

第二,如果变量b是静态局部变量(static int a = 2;)t会是什么类型的block呢?
答:依旧是全局 block咯。静态变量和全局变量是都是放在全局区的嘛。

到目前为止,3 中类型的block都出现过了,现在我们来总结一下:

Block 的类型 条件
全局 block block被初始化为全局变量时;
block未捕获任何外部变量时;
block只捕获了全局/静态变量时。
栈 block MRC 中block捕获了auto局部变量;
ARC 中不存在栈 block.
堆 block ARC 中block捕获了auto局部变量;
对除全局 block外的block执行 copy 操作。

前面我们谈论的都是block在定义和初始化时的存储域,接下来我们继续分析block在函数中作为形参和返回值的存储域,这一部分非常简单,如果你明白引用类型的参数传递和返回值的一些特点,这部分可以忽略不看了。

先来看看block作为形参的存储域,其实这个没什么好说的。block被当做 Objective-C 对象看待时,其是一个引用类型,其形参和实参是同一个首地址。

再来看block作为返回值时的存储域。定义如下函数:

Block_t func(Block_t aBlock)
{
#if __has_feature(objc_arc)
    return aBlock;
#else
    return [aBlock autorelease];
#endif
}

我们调用func函数时,将一个未捕获任何外部变量的block作为该函数的参数:

Block_t t = ^{
    NSLog(@"I am a block.");
};
NSLog(@"%@", t);    // <__NSGlobalBlock__: 0x100001058>
    
Block_t t2 = func(t);
NSLog(@"%@", t2);   // <__NSGlobalBlock__: 0x100001058>

在 ARC 和 MRC 环境下发现tt2同为全局 block,并且内存地址一致,也就是说全局 block作为返回值时,它的存储域并不会变化。这一点很好理解,全局 block不依赖任何外部条件,它可以看做为字面量,其内存地址是唯一确定和共享的。

接下来,将一个捕获auto局部变量的block作为该函数的参数:

int a = 1;
Block_t t = ^{
    NSLog(@"a = %d", a);
};
NSLog(@"%@", t);
// ARC: <__NSMallocBlock__: 0x10060ef10>
// MRC: <__NSStackBlock__: 0x7ffeefbff558>
    
Block_t t2 = func(t);
NSLog(@"%@", t2);
// ARC: <__NSMallocBlock__: 0x10060ef10>
// MRC: <__NSStackBlock__: 0x7ffeefbff558>

结果还是一样,返回的block和原来的block始终是同一个。还有,综合上面的这些例子我们可以发现 ARC 中已经不存在了栈 block,在编译期间栈 block已被转移至堆区。

那么最后现在我们来装模作样得总结一下block在函数中的存储域变化:

原 Block 的类型 作为形参和返回值
全局 block 与原block一致,为其本身。
栈 block 与原block一致,为其本身。
堆 block 与原block一致,为其本身。

你可能感兴趣的:(Block 的存储域)