探寻Block的本质(4)—— Block的类型

Block传送门🦋🦋🦋

探寻Block的本质(1)—— 基本认识程序员

探寻Block的本质(2)—— 底层结构markdown

探寻Block的本质(3)—— 基础类型的变量捕获函数

探寻Block的本质(5)—— 对象类型的变量捕获布局

探寻Block的本质(6)—— __block的深刻分析post

前面的章节里面,咱们了解到Block也是一个OC对象,由于它的底层结构中也有isa指针。例以下面这个block:ui

#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;
}
*********************** 运行结果 **************************
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
Program ended with exit code: 0
复制代码

上面的代码中,咱们经过 [xxx class][xxx supperclass] 方法,打印出block的类型以及父类的类型,能够看继承关系是这样的 __NSGlobalBlock__->__NSGlobalBlock->NSBlock->NSObject 这也能够很好地证实block是一个对象,由于它的基类就是NSObject。并且咱们也就知道了,block中的isa成员变量确定是从NSObject继承而来的。atom

它的编译后形式以下 图中的信息代表,该block的isa指向的class为_NSConcreteStackBlock。 奇怪,难道这里isa指向的class不该该和程序运行时打印出来的class一致吗?spa

这里补充一个细节:目前来讲,LLVM编译器生成的中间文件再也不是C++形式了,而咱们在命令行里面,其实是经过clang生成的C++文件,在语法细节上这二者是有差异的,可是大部分的逻辑和原理仍是相近的,因此经过clang生成的C++中间代码,仅供咱们做为参考,最终仍是必须以运行时的结果为准,由于Runtime仍是会在程序运行的时候,对以前编译事后的中间码进行必定的处理和调整的。命令行

Block的类型

Block有3种类型Block的类型 下面咱们来一一解析,首先咱们在回顾一下程序的内存布局设计

  • 代码段 占用空间很小,通常存放在内存的低地址空间,咱们平时编写的全部代码,就是放在这个区域
  • 数据段 用来存放全局变量
  • 堆区 是动态分配内存的,用来存放咱们代码中经过alloc生成的对象,动态分配内存的特色是须要程序员申请内存和管理内存。例如OC中alloc生成的对象须要调用releas方法释放【MRC下】,C中经过malloc生成的对象必需要经过free()去释放。
  • 栈区 系统自动分配和销毁内存,用于存放函数内生成的局部变量

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

(1) NSGlobalBlock(也就是_NSConcreteGlobalBlock)

若是一个block内部没有使用/访问 自动变量(auto变量),那么它的类型即为__NSGlobalBlock__,它会被存储在应用程序的 数据段

咱们用代码来验证一下block没有访问任何变量block访问了局部static变量block访问全局变量 以上三个图,展现了 除了auto变量外的其余几种变量被block访问的状况,打印的结果都是以下

2019-06-05 16:38:31.885797+0800 Interview03-block[17590:1712446] __NSGlobalBlock__
Program ended with exit code: 0
复制代码

结果显示block的类型都是__NSGlobalBlock__。其实这种类型的block没有太多的应用场景,因此出镜率的不多,这里仅做了解就行。

(2) NSStaticBlock(也就是_NSConcreteStaticBlock)

若是一个block有使用/访问 自动变量(auto变量),那么它的类型即为__NSStaticBlock__,它会被存储在应用程序的 栈区

咱们继续验证一波,以前代码调整以下 block访问了auto变量打印结果以下

2019-06-05 16:45:25.990687+0800 Interview03-block[17648:1721701] __NSMallocBlock__
Program ended with exit code: 0
复制代码

咦?怎么这里的结果是__NSMallocBlock__?不该该是__NSStaticBlock__吗?缘由在于当前处于ARC环境下,ARC机制已经为咱们作过了一些处理,为了看清本质,咱们先关掉ARC关闭ARC再跑一边代码,输出结果以下

2019-06-05 16:52:08.500787+0800 Interview03-block[17712:1730384] __NSStackBlock__
Program ended with exit code: 0
复制代码

好,咱们看到,再没有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;
}
复制代码

根据以上的代码,你的预期打印结果是多少呢,a的值10能被正确打印出来吗?看运行结果

2019-06-05 17:04:25.915160+0800 Interview03-block[17820:1746272] a的值为----272632584
Program ended with exit code: 0
复制代码

瞧,a如今的值为272632584,很显然,这样的值用在咱们的程序里面,确定就破坏了咱们原有的设计思路了。

那么就来分析一下:

  • 代码中,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了,因此能够看到打印结果是一个没法预期的数字。

❓❓那么该怎么解决这个问题呢?很天然的,咱们就会想到,须要将那个__NSStaticBlock__类型的Block转移到堆区上面去,这样它不会随着函数栈区的回收而被销毁,而能够由程序员在使用完它以后再去销毁它。

(3) NSMallocBlock(也就是_NSConcreteMallocBlock)

__NSMallocBlock__调用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;
}
复制代码

在给block赋值前,先进行copy操做,获得以下打印结果

2019-06-05 17:44:16.940492+0800 Interview03-block[18166:1799723] a的值为---10
2019-06-05 17:44:16.940752+0800 Interview03-block[18166:1799723] block的类型为__NSMallocBlock__
Program ended with exit code: 0
复制代码

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

你或许会好奇,若是对__NSGlobalBlock__调用copy方法呢?这里就直接告诉你,结果仍然是一个__NSGlobalBlock__,有兴趣能够自行代码走一波,这里再也不赘述。

总结

block的类型总结 对每一种类型的block调用copy后的结果以下

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开关打开,运行程序咱们获得以下结果

2019-06-05 20:29:31.503282+0800 Interview03-block[19472:1922021] ************10
2019-06-05 20:29:31.503652+0800 Interview03-block[19472:1922021] block的类型为__NSMallocBlock__
Program ended with exit code: 0
复制代码

能够看到,这跟咱们在MRC下手动将block进行copy以后的结果同样,说明ARC其实替咱们作了相应的copy操做。

在ARC环境下,编译器会根据状况自动将栈上的block复制到堆上,例如如下的状况

  • block做为函数参数返回的时候
  • 将block赋值给__strong指针的时候
  • block做为Cocoa API中方法名里面含有usingBlock的方法参数时
  • block做为GCD API的方法参数的时候

小细节--Block属性的书写方法

  • MRC下Block 属性的书写建议

@property (nonatomic, copy) void(^block)(void);

  • ARC下Block 属性的书写建议

@property (nonatomic, copy) void(^block)(void);//推荐 @property (nonatomic, strong) void(^block)(void);

ARC下关键字copystrongblock属性的做用是同样的,由于__strong指针指向block的时候,ARC会自动对block进行copy操做,可是为了保持代码的一致性,建议仍是使用copy关键字来修饰。

Block传送门🦋🦋🦋

探寻Block的本质(1)—— 基本认识

探寻Block的本质(2)—— 底层结构

探寻Block的本质(3)—— 基础类型的变量捕获

探寻Block的本质(5)—— 对象类型的变量捕获

探寻Block的本质(6)—— __block的深刻分析

相关文章
相关标签/搜索