美文网首页
Block 的存储域

Block 的存储域

作者: waylen | 来源:发表于2018-04-08 16:30 被阅读46次

    本文主要在 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 的存储域

          本文链接:https://www.haomeiwen.com/subject/zshmhftx.html