Block的本质(二)

413 阅读5分钟

前面 block的本质(一)已经介绍了Block的底层实现,变量捕获,下面继续

Block的类型

block有3中类型,可以通过调用class方法(为什么可以调用class呢?如果还有此疑问,看本质一)或者isa指针查看具体类型,最终都是继承自NSBlock类型

__NSGlobalBlock___NSConcreteGlobalBlock )
__NSStackBlock__ ( _NSConcreteStackBlock )
__NSMallocBlock___NSConcreteMallocBlock )

咱们写个示例来看一下

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

通过打印内容确实可以发现block的三种类型

这儿有人会有疑问了,block2和最后打印的block都访问了局部变量a,怎么会有不同结果呢?这个问题等大家先保留 但是我们上面提到过,上述代码转化为c++代码查看源码时却发现block的类型与打印出来的类型不一样,c++源码中三个block的isa指针全部都指向_NSConcreteStackBlock类型地址。 我们可以猜测runtime运行时过程中也许对类型进行了转变。最终类型当然以runtime运行时类型也就是我们打印出的类型为准。

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

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

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

block是如何定义其类型

咱们来看一张图就明白了

但大家会发现下面的代码打印后是MallocBlock

   void (^block2)(void) = ^{
            NSLog(@"Hello - %d",a);
        };

上面你不是说是在Stackblock吗?那是因为ARC帮我们做了一些事情 关闭ARC回到MRC环境下

// 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(@"%@ %@", [block1 class], [block2 class]);
        // __NSStackBlock__调用copy : __NSMallocBlock__
        NSLog(@"%@", [[block2 copy] class]);
    }
    return 0;
}

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

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

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

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

为了避免这种情况发生,可以通过copy将__NSStackBlock__类型的block转化为__NSMallocBlock__类型的block,将block存储在堆中

void (^block)(void);
void test()
{
    // __NSStackBlock__ 调用copy 转化为__NSMallocBlock__
    int age = 10;
    block = [^{
        NSLog(@"block---------%d", age);
    } copy];
    [block release];
}

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

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

ARC下系统对Block做了什么

在ARC环境下,编译器会根据情况自动将栈上的block进行一次copy操作,将block复制到堆上。

那么什么情况下ARC会自动将block进行一次copy操作?

1. block作为函数返回值

typedef void (^Block)(void);
Block myblock()
{
    int a = 10;
    // 上文提到过,block中访问了auto变量,此时block类型应为__NSStackBlock__
    Block block = ^{
        NSLog(@"---------%d", a);
    };
    return block;
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Block block = myblock();
        block();
       // 打印block类型为 __NSMallocBlock__
        NSLog(@"%@",[block class]);
    }
    return 0;
}

上文提到过,如果在block中访问了auto变量时,block的类型为__NSStackBlock__,上面打印内容发现blcok为__NSMallocBlock__类型的,并且可以正常打印出a的值,说明block内存并没有被销毁。 上面提到过,block进行copy操作会转化为__NSMallocBlock__类型,block复制到堆中,那么说明ARC在 block作为函数返回值时会自动帮助我们对block进行copy操作,以保存block,并在适当的地方进行release操作

2. 将block赋值给__strong指针时

block被强指针引用时,ARC也会自动对block进行一次copy操作。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // block内没有访问auto变量
        Block block = ^{
            NSLog(@"block---------");
        };
        NSLog(@"%@",[block class]);
        int a = 10;
        // block内访问了auto变量,但没有赋值给__strong指针
        NSLog(@"%@",[^{
            NSLog(@"block1---------%d", a);
        } class]);
        // block赋值给__strong指针
        Block block2 = ^{
          NSLog(@"block2---------%d", a);
        };
        NSLog(@"%@",[block1 class]);
    }
    return 0;
}

查看打印内容可以看出,当block被赋值给__strong指针时,ARC会自动进行一次copy操作。

3. block作为Cocoa API中方法名含有usingBlock的方法参数时

NSArray *array = @[];
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            
}];

4. block作为GCD API的方法参数时

例如:GDC的一次性函数或延迟执行的函数,执行完block操作之后系统才会对block进行release操作。

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
            
});        
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            
});