探寻Block的本质(2)—— 底层结构markdown
探寻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有3种类型 下面咱们来一一解析,首先咱们在回顾一下程序的内存布局设计
- 代码段 占用空间很小,通常存放在内存的低地址空间,咱们平时编写的全部代码,就是放在这个区域
- 数据段 用来存放全局变量
- 堆区 是动态分配内存的,用来存放咱们代码中经过alloc生成的对象,动态分配内存的特色是须要程序员申请内存和管理内存。例如OC中alloc生成的对象须要调用releas方法释放【MRC下】,C中经过malloc生成的对象必需要经过free()去释放。
- 栈区 系统自动分配和销毁内存,用于存放函数内生成的局部变量
下面借助一个经典的图例,来看一看不一样类型的block到底存储在哪里!
若是一个block内部没有使用/访问 自动变量(auto变量),那么它的类型即为
__NSGlobalBlock__
,它会被存储在应用程序的 数据段
咱们用代码来验证一下 以上三个图,展现了 除了auto变量外的其余几种变量被block访问的状况,打印的结果都是以下
2019-06-05 16:38:31.885797+0800 Interview03-block[17590:1712446] __NSGlobalBlock__
Program ended with exit code: 0
复制代码
结果显示block的类型都是__NSGlobalBlock__
。其实这种类型的block没有太多的应用场景,因此出镜率的不多,这里仅做了解就行。
若是一个block有使用/访问 自动变量(auto变量),那么它的类型即为
__NSStaticBlock__
,它会被存储在应用程序的 栈区
咱们继续验证一波,以前代码调整以下 打印结果以下
2019-06-05 16:45:25.990687+0800 Interview03-block[17648:1721701] __NSMallocBlock__
Program ended with exit code: 0
复制代码
咦?怎么这里的结果是__NSMallocBlock__
?不该该是__NSStaticBlock__
吗?缘由在于当前处于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转移到堆区上面去,这样它不会随着函数栈区的回收而被销毁,而能够由程序员在使用完它以后再去销毁它。
对
__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调用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的方法参数的时候
@property (nonatomic, copy) void(^block)(void);
@property (nonatomic, copy) void(^block)(void);
//推荐@property (nonatomic, strong) void(^block)(void);
ARC下关键字copy
和strong
对block属性
的做用是同样的,由于__strong
指针指向block
的时候,ARC会自动对block
进行copy操做,可是为了保持代码的一致性,建议仍是使用copy
关键字来修饰。