浅谈block实现原理及内存特性之一: 内部结构和类型

前言, 浅谈block实现原理及内存特性系列文章:
浅谈block实现原理及内存特性之一: 内部结构和类型
浅谈Block实现原理及内存特性之二: 持有变量
浅谈Block实现原理及内存特性之三: copy过程分析


什么是block?

block和函数类似, 只不过是直接定义在另一个函数里的, 和定义它的那个函数共享同一个范围内的东西。block可以实现闭包, 有些人也称它作

block 的内部结构和作用

block是个什么东西呢, 对象? 结构体? 还是其它的什么东西?让我们来看一下block的内部结构:

当然, 这些Block官方源码我已经整理在我的博客资源中了, 可以在文章最后下载Block官方源码, 然后在Block_private.h 查看, 这个结构和上图的结构一致代码中是这么定义block的:

struct Block_layout {
    void *isa;
    int flags;
    int reserved; 
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    // imported variables
};

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

通过上面的结构, 可以看出一个 block 实例的构成实际上有6个部分:
1.isa指针: 所有对象都有该指针,用于实现对象相关的功能。
2.flags: 附加标识位, 在copydispose等情况下可以用到。
3.reserved:保留变量。
4.invoke: 函数指针,指向 block的实现代码, 也可以说是函数调用地址。
5.descriptor: 表示该 block的附加描述信息,主要是 size,以及 copydispose函数的指针。这两个辅助函数在拷贝及丢弃块对象时运行, 其中会执行一些操作, 比方说, 前者要保留捕获的对象,而后者则将之释放。
6.variables: 捕获的变量,block能够访问它外部的局部变量,就是因为将这些变量复制到了结构体中。

block的类型

block其实是有类型的, 且一共有3种类型, 全局块, 栈块, 堆块:
1.__NSGlobalBlock__: 存储在全局/静态的 block,不会捕获任何外部变量。
2.__NSStackBlock__: 存储在栈中的 block,当函数返回时会被销毁。
3.__NSMallocBlock__: 存储在堆中的 block,当引用计数为0时会被销毁。

这些类型是可以通过打印block对象来获取的类型信息。但是还有一些, 是不允许我们使用的原始数据类型, 他们只允许被编译器使用或者内部使用。这些Block官方源码我已经整理在我的博客资源中了, 可以在文章最后下载Block官方源码, 然后在Block_private.h 文件中查看:

// the raw data space for runtime classes for blocks
// class+meta used for stack, malloc, and collectable based blocks
BLOCK_EXPORT void * _NSConcreteGlobalBlock[32];
BLOCK_EXPORT void * _NSConcreteStackBlock[32];
BLOCK_EXPORT void * _NSConcreteMallocBlock[32];
BLOCK_EXPORT void * _NSConcreteAutoBlock[32];
BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32];
BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32];

这里的_NSConcreteGlobalBlock, _NSConcreteStackBlock, _NSConcreteMallocBlock就是上面那三种类型对应的原始类型。你只需要认识这几种原始类型就好, 我下面就按照之前三种类型来进行举例。

NSGlobalBlock

这种块不会捕捉任何变量, 运行时也无须有状态来参与。全局块声明在全局内存里, 在编译期已经完全确定了。所以, 无论是ARC还是MRC下, 如下代码中的 block都是全局静态的。

// NSGlobalBlock
- (void)globalBlock {
    void (^block)(void) = ^{
        NSLog(@"GlobalBlock内部");               // 全局静态区
    };
    block();
    NSLog(@"GlobalBlock:%@", block);           // 全局静态区
}

无论是ARC还是MRC下, 打印结果都一致如下:

// ARC下 和 MRC下
GlobalBlock内部
GlobalBlock:<__NSGlobalBlock__: 0x108e070d0>

可以看出block存储于全局静态区, 是NSGlobalBlock类型。

NSStackBlock 和 NSMallocBlock

为什么把它们两者放在一起来说呢? 栈块堆块的表现可能比较复杂一些。而且, 下面这些代码在ARCMRC的表现效果是不同的。还是先来看代码吧。

// ARC下为NSMallocBlock(堆区), MRC下为NSStackBlock(栈区)
- (void)stackBlockInMRCAndHeapBlockInARC {
    __block int a = 0;
    void (^block)(void) = ^{
        a = 1;
        NSLog(@"Block内部:%p", &a);
    };
    block();
    NSLog(@"Block:%@", block);
}

打印结果:

// MRC下
Block内部:0x7ffee2bdaa58
Block:<__NSStackBlock__: 0x7ffee2bdaa10>
// ARC下
Block内部:0x600000233e98
Block:<__NSMallocBlock__: 0x60000025d8b0>

因此, 可以看出来在MRC下, block是存储于栈区的, 是NSStackBlock类型的。而在ARC下, block存储于堆区, 是NSMallocBlock类型的。

要问MRC下有没有存储于堆区的block, 当然有了。但block默认会分配在栈区, 需要保留的话, 也可以手动改到堆区, 这样它就是堆块了。

// MRC下为NSMallocBlock(堆区), ARC下为NSMallocBlock(堆区)
- (void)heapBlock {
    __block int a = 0;
    void (^block)(void) = [^{
        a = 1;
        NSLog(@"MallocBlock内部:%p", &a);
    } copy];
    block();
    NSLog(@"MallocBlock:%@", block);
}

打印结果:

// MRC 下
MallocBlock内部:0x600000229818
MallocBlock:<__NSMallocBlock__: 0x600000446d20>
// ARC 下
MallocBlock内部:0x6000004250b8
MallocBlock:<__NSMallocBlock__: 0x600000446e40>

由地址可以看出, block在ARCMRC下都是存储于堆区的, 所以其类型是NSMallocBlock的。
为了解决栈块在其变量作用域结束之后被释放的问题,我们需要把block copy到堆中,延长其生命周期。在开启ARC时,编译器会判断其是不是全局块, 若不是全局块则需要将block从栈copy到堆中,并自动生成相应代码。所以, 上面的例子中, 本不用手动添加copy代码的, ARC会帮我们来做这个事情。

block类型总结

总结一下, 在MRC中, 可能有三种block, 就是全局块, 栈块堆块。 但是在ARC中, 只有两种block了, 就是全局块堆块了。由于ARC已经能很好地处理对象的生命周期的管理, 所以都放到堆上管理, 不在使用栈块管理了, 所以就没有栈块的。
而且捕获了变量的block默认会分配在栈区, 在MRC中需要保留的话, 可以手动改到堆区; 在ARC中, block也是在栈区的, 但编译器会并自动将其copy到堆中, 所以会存储在堆区。

相关资源下载

Demo下载: Block实现原理及内存特性
block官方源码: libclosure-38

猜你喜欢

转载自blog.csdn.net/wangyanchang21/article/details/79525394