OC底层-Block本质(三、类型)

231 阅读4分钟

前言

block对象是什么类型的,通过源码可以知道block中的isa指针指向的是_NSConcreteStackBlock类对象地址。那么block是否就是_NSConcreteStackBlock类型的呢?我们继续往下看

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        void (^block)(void) = ^{
            NSLog(@"Hello");
        };
        
        NSLog(@"%@", [block class]);
        NSLog(@"%@", [[block class] superclass]);
        NSLog(@"%@", [[[block class] superclass] superclass]);
        NSLog(@"%@", [[[[block class] superclass] superclass] superclass]);
    }
    return 0;
}

///打印结果:
 __NSGlobalBlock__
 __NSGlobalBlock
 NSBlock
 NSObject

///继承链
__NSGlobalBlock__ : __NSGlobalBlock : NSBlock : NSObject

从上述打印内容可以看出block最终都是继承自NSBlock类型,而NSBlock继承于NSObjcet。那么block其中的isa指针其实是来自NSObject中的。这也更加印证了block的本质其实就是OC对象。

Block三种类型

分别为:

__NSGlobalBlock__ ( _NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock__ ( _NSConcreteMallocBlock )

通过代码查看一下block在什么情况下其类型会各不相同

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 1. 内部没有调用外部变量的block
        void (^block1)(void) = ^{
            NSLog(@"1");
        };
        // 2. 内部调用外部变量的block
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"2 - %d",a);
        };
       // 3. 直接调用的block的class
        NSLog(@"1.%@ 2.%@ 3.%@", [block1 class], [block2 class], [^{
            NSLog(@"AAA %d",a);
        } class]);
    }
    return 0;
}

///打印
1.__NSGlobalBlock__ 2.__NSMallocBlock__ 3.__NSStackBlock__

Block在内存中的存储

通过下面一张图看一下不同block的存放区域

上图中可以发现,根据block的类型不同,block存放在不同的区域中。 数据段中的__NSGlobalBlock__直到程序结束才会被回收,不过我们很少使用到__NSGlobalBlock__类型的block,因为这样使用block并没有什么意义。

__NSStackBlock__类型的block存放在栈中,我们知道栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放,而在相同的作用域中定义block并且调用block似乎也多此一举。

__NSMallocBlock__是在平时编码过程中最常使用到的。存放在堆中需要我们自己进行内存管理。

Block类型的定义

block是如何定义其类型,依据什么来为block定义不同的类型并分配在不同的空间呢?首先看下面一张图

接着我们使用代码验证上述问题,首先关闭ARC回到MRC环境下,因为ARC会帮助我们做很多事情,可能会影响我们的观察。

// MRC环境!!!
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // Global:没有访问auto变量:__NSGlobalBlock__
        void (^block1)(void) = ^{
            NSLog(@"block1---------");
        };
        // Stack:访问了auto变量: __NSStackBlock__
        int a = 10;
        void (^block2)(void) = ^{
            NSLog(@"block2---------%d", a);
        };
        NSLog(@"1.%@, 2.%@,3.%@", [block1 class], [block2 class], [[block2 copy] class]);
    }
    return 0;
}

///打印如下:
 1.__NSGlobalBlock__, 2.__NSMallocBlock__,3.__NSMallocBlock__

通过打印的内容可以发现正如上图中所示。 没有访问auto变量的block是__NSGlobalBlock__类型的,存放在数据段中。 访问了auto变量的block是__NSStackBlock__类型的,存放在栈中。 __NSStackBlock__类型的block调用copy成为__NSMallocBlock__类型并被复制存放在堆中。

上面提到过__NSGlobalBlock__类型的我们很少使用到,因为如果不需要访问外界的变量,直接通过函数实现就可以了,不需要使用block。

但是__NSStackBlock__访问了auto变量,并且是存放在栈中的,上面提到过,栈中的代码在作用域结束之后内存就会被销毁,那么我们很有可能block内存销毁之后才去调用他,那样就会发生问题,通过下面代码可以证实这个问题。

void (^block)(void);
void test()
{

    int a = 20;
    block = ^{
        NSLog(@"block---------%d", a);
    };
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}

///打印、
block----------272632776

可以发现a的值变为了不可控的一个数字。为什么会发生这种情况呢?因为上述代码中创建的block是__NSStackBlock__类型的,因此block是存储在栈中的,那么当test函数执行完毕之后,栈内存中block所占用的内存已经被系统回收,因此就有可能出现乱得数据。查看其c++代码可以更清楚的理解

为了避免这种情况发生,可以通过copy将NSStackBlock类型的block转化为NSMallocBlock类型的block,将block存储在堆中,以下是修改后的代码。

void (^block)(void);
void test()
{

    int a = 20;
    block = [^{
        NSLog(@"block---------%d", a);
    } copy];
    [block release];;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}
///打印:
 block---------20

那么其他类型的block调用copy会改变block类型吗?

所以在平时开发过程中MRC环境下经常需要使用copy来保存block,将栈上的block拷贝到堆中,即使栈上的block被销毁,堆上的block也不会被销毁,需要我们自己调用release操作来销毁。而在ARC环境下回系统会自动copy,是block不会被销毁。