程序员社区

OC-Block的本质(二)-Block的三种类型

我们在讲block的本质的时候已经知道了,block的本质就是一个 OC 对象(它的底层结构中也有isa指针),那么既然它是一个 OC 对象,它就会有类型,本文就将讲解block的三种类型.

OC-Block的本质(二)-Block的三种类型插图
image-20210422152917519
OC-Block的本质(二)-Block的三种类型插图1
image-20210422152946078

block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承于NSBlock类型

NSGlobalBlock ( _NSConcreteGlobalBlock )
NSMallocBlock ( _NSConcreteMallocBlock )
NSStackBlock ( _NSConcreteStackBlock )

验证block是OC对象,运行代码:

#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        //Block的定义
        void (^block)(void) = ^(){
            NSLog(@"Hello World");
        };
        
        NSLog(@"%@", [block class]);
        NSLog(@"%@", [block superclass]);
        NSLog(@"%@", [[block superclass] superclass]);
        NSLog(@"%@", [[[block superclass] superclass] superclass]);
    }
    return 0;
}

RUN>

*********************** 运行结果 **************************
2019-06-05 14:44:53.179548+0800 Interview03-block[16670:1570945] __NSGlobalBlock__
2019-06-05 14:44:53.179745+0800 Interview03-block[16670:1570945] __NSGlobalBlock
2019-06-05 14:44:53.179757+0800 Interview03-block[16670:1570945] NSBlock
2019-06-05 14:44:53.179767+0800 Interview03-block[16670:1570945] NSObject

上面的代码中,我们通过 [xxx class][xxx supperclass] 方法,打印出block的类型以及父类的类型,可以看继承关系是这样的
__NSGlobalBlock__->__NSGlobalBlock->NSBlock->NSObject
这也可以很好地证明block是一个对象,因为它的基类就是NSObject。而且我们也就知道了,block中的isa成员变量肯定是从NSObject继承而来的,而且isa指向block的类对象。

三种block在内存中的分布

在讲block在内存中的分布之前,先了解一下程序的内存分配情况,因为不同类型的block分配的内存也不同.

  • .text段 : 也称代码段,我们写的代码都存放在这里
  • .data区 : 也称数据区,一般存放全局变量, __NSGlobalBlock存放在这里
  • 堆区 : 存放我们自己alloc出来的对象,动态分配内存,需要程序员自己申请内存,自己管理. __NSMallocBlock存放在堆区
  • 栈区 : 一般存放局部变量,不需要程序员管理,系统自动分配,自动销毁,__NSStackBlock存放在栈区
OC-Block的本质(二)-Block的三种类型插图2
程序内存布局

下面借助一个经典的图例,来看一看不同类型的block到底存储在哪里!

OC-Block的本质(二)-Block的三种类型插图3
block的存放区域

一: __NSGlobalBlock

  • 结论:没有访问 auto变量 的block 就是 __NSGlobalBlock
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        static int age = 10;
        void(^block)(void) = ^{
            NSLog(@"Hello, World! %d",age);
        };
        NSLog(@"%@",[block class]);
    }
    return 0;
}

RUN>

*********************** 运行结果 **************************
2019-06-05 14:44:53.179548+0800 Interview03-block[16670:1570945] __NSGlobalBlock__

二: __NSStackBlock

  • 结论:访问了auto变量 的block 就是 __NSStackBlock
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 10;
        void(^block)(void) = ^{
            NSLog(@"Hello, World! %d",age);
        };
        NSLog(@"%@",[block class]);
    }
    return 0;
}

RUN>

*********************** 运行结果 **************************
2021-04-23 14:18:41.452310+0800 Interview01-Block的本质[3973:122225] __NSMallocBlock__

怎么打印的是__NSMallocBlock__,刚才不是说访问了auto变量就是__NSStackBlock吗?
因为这里我们使用的是ARC,在ARC环境下,Xcode会默认帮我们做很多事情,我们在Build Settings中把ARC设置成MRC,再来打印一下:

OC-Block的本质(二)-Block的三种类型插图4
image-20210423142137469
*********************** 运行结果 **************************
2021-04-23 14:21:27.667116+0800 Interview01-Block的本质[3992:123867] __NSStackBlock__

没有ARC的帮助下,这里的block类型确实是__NSStackBlock__
其实我们在很多场景下,都会用到这种类型的block,因为很多情况下,我们都会在block 中用到环境变量,而大部分的环境变量都可能是auto变量,思考一下,如果我们不做任何处理,会碰到什么麻烦吗?

我们再将生面的代码调整如下

#import <Foundation/Foundation.h>

void (^block)(void);//全局变量block

void test(){
    int a = 10;
    
    block =     ^(){
                    NSLog(@"a的值为---%d",a);
                };
    
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        test();
        block();
    }
    return 0;
}

RUN>

*********************** 运行结果 **************************
2021-04-23 14:24:57.918540+0800 Interview01-Block的本质[4014:125856] a的值为----272632904

a的值10能被正确打印出来吗?a现在的值为272632904,很显然,这样的值用在我们的程序里面,肯定不是预期的。运行变得不可预料,这可不是一个正确的程序。

代码中,block是一个定义在函数外的全局变量

在函数test()内,代码^(){ NSLog(@"a的值为---%d",a); };首先会为我们生成一个__NSStaticBlock__类型的Block,它存储与当前函数test()的栈空间内,然后它的指针被赋值给了全局变量block

main函数中,首先调用函数test(),全局变量block就指向了test()函数栈上的这个__NSStaticBlock__类型的Block,然后test()调用结束,栈空间回收

然后block被调用,问题就出在这里,此时,test()的栈空间都被系统回收去做其他事情了,也就是说上面的那个__NSStaticBlock__类型的Block的内存也被回收了。虽然通过对象block(或者说block指针),最终还可访问原来变量a的所指向的那块内存,但是这里面寸的值就无法保证是我们所需要的10了,所以可以看到打印结果是一个无法预期的数字

为了避免出现这种情况,我们需要把block存储在堆上,__NSMallocBlock就闪亮登场了.

三: __NSMallocBlock

结论: 当一个__NSStackBlock调用了copy操作,返回的就是一个__NSMallocBlock

#import <Foundation/Foundation.h>

void (^block)(void);//全局变量block

void test(){
    int a = 10;
    
    block =     [^(){ NSLog(@"a的值为---%d",a); } copy];
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        test();
        block();
        NSLog(@"block的类型为%@",[block class]);
    }
    return 0;
}

RUN>

*********************** 运行结果 **************************
2021-04-23 14:33:02.199804+0800 Interview01-Block的本质[4069:130439] a的值为---10
2021-04-23 14:33:02.200377+0800 Interview01-Block的本质[4069:130439] block的类型为__NSMallocBlock__

可以看到, 变量a的打印值还是10,并且block所指向的也确实是一个__NSMallocBlock__。正是由于copy之后,[^(){ NSLog(@"a的值为---%d",a); } copy];所返回的Block是存放在堆上的,所以里面a的值仍是被捕获时后的值10,因此打印结果不受影响。

如果对__NSGlobalBlock__调用copy方法呢?这里就直接告诉你,结果仍然是一个__NSGlobalBlock__

OC-Block的本质(二)-Block的三种类型插图5
image-20210423143641956

总结

block类型 环境
NSGlobalBlock 没有访问auto变量
NSMallocBlock NSStackBlock调用了copy
NSStackBlock 访问了auto变量

GlobalBlock没有访问auto变量,这种类型的block都可用方法代替,不常用。

可以把block从栈放到堆里面,每一种类型的block调用copy后的结果如下所示:

block类型 副本源的配置存储域 复制效果
NSGlobalBlock 程序的数据区段 什么也不做
NSMallocBlock 引用计数器增加
NSStackBlock 从栈复制到堆
ARC环境下Block的copy问题

上面的讨论实验,我们都是基于MRC环境下,对Block在内存中的存储情况进行讨论。由于我们在平时代码中生成的block都是在函数内创建的,也就是都是__NSStaticBlock__类型的,而通常我们需要将其保存下来,在将来的某个时候调用,但是那个时间点上往往该block所在的函数栈已经不存在了,因此在MRC环境下,我们需要通过对其调用copy方法,将__NSStaticBlock__的内容复制到堆区内存上,使之成为一个__NSMallocBlock__,这样才不影响后续的使用,同时,作为使用者,需要确保在使用完block之后而不在需要它的时候,对block调用release方法将其释放掉,这样才能避免产生内存泄漏问题。

ARC的出现,为我们开发者做了很多繁琐而细致的工作,是我们不用再内存管理方面耗费太多精力,其中,就包括了对block的copy处理。举个例子,我们对上一份代码微调一下,把copy操作去掉,如下

#import <Foundation/Foundation.h>

void (^block)(void);//全局变量block

void test(){
    int a = 10;
    
    block =     ^(){ NSLog(@"a的值为---%d",a);   };
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        test();
        block();
        NSLog(@"block的类型为%@",[block class]);
    }
    return 0;
}

将ARC开关打开,运行程序我们得到如下结果

RUN>

*********************** 运行结果 **************************
2021-04-23 14:33:02.199804+0800 Interview01-Block的本质[4069:130439] a的值为---10
2021-04-23 14:33:02.200377+0800 Interview01-Block的本质[4069:130439] block的类型为__NSMallocBlock__

可以看到,这跟我们在MRC下手动将block进行copy之后的结果一样,说明ARC其实替我们做了相应的copy操作。在ARC环境下,编译器会自动把栈上的block copy到堆上。

特别备注

本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

赞(0) 打赏
未经允许不得转载:IDEA激活码 » OC-Block的本质(二)-Block的三种类型

一个分享Java & Python知识的社区