我们在讲block的本质的时候已经知道了,block的本质就是一个 OC 对象(它的底层结构中也有isa指针),那么既然它是一个 OC 对象,它就会有类型,本文就将讲解block
的三种类型.
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存放在栈区
下面借助一个经典的图例,来看一看不同类型的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,再来打印一下:*********************** 运行结果 ************************** 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__
,
总结
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对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!