Block原理探究(上篇)-Block本质及存储域问题
主要内容:
- 分析
Block
的源码 - 验证
Block
的本质是对象 - 理解
Block
的存储域分类 - 验证
Block
的不同存储域 - 分析
Block
的Copy
原理
一、分析Block的源码
为了分析Block
的源码,从一个最简单的Block
使用示例说起,测试代码如下:
//main.m文件:
#import <Foundation/Foundation.h>
int main(int argc, char * argv[]) {
int num = 10;
void (^block)(void) =^{NSLog(@"num = %d",num);};
block();
return 0;
}
Objective-C
语言是基于C
、C++
的,为了深入理解Block
的底层结构,我们可以通过如下的编译器命令将上述代码转换成C++
源码:
clang -rewrite-objc 源代码文件名(如此例中的main.m)
转化后的C++
源码如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_wd_fhcn9bn91v56nlzv9mt5z8ym0000gn_T_main_9e3646_mi_0,num);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
}
__main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char * argv[]) {
int num = 10;
void (*myBlock)(void) =((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
对比OC
代码与C++
源码中的main
函数,我们发现:
- 创建
Block
其实是调用了__main_block_impl_0
结构体的构造函数; -
Block
中待执行代码也都被封装到了__main_block_func_0
函数中;
另外值得注意的是,这些C++
的结构体和函数的命名,是根据Block
语法所属的函数名(此处为main
)和Block
语法在该函数出现的顺序值(此处为0
)来设定的;
根据这些对应关系,我们对C++
源码中的内容一一分析:
1.__main_block_imp_0结构体
__main_block_impl_0
结构体对应了Block
的定义,结构体内部包含了三个成员变量impl
、Desc
、num
。
num
其实就是被捕获的变量(后续再讲),另外还有一个同名的构造函数__main_block_impl_0
。可以看到相关的代码如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
Block
通过调用这里的构造函数得以创建,调用时需传入了四个参数:(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0)
,前三个参数对应成员变量的初始化,而最后一个参数flags
携带默认值可暂不考虑;
2.__block_impl结构体
__main_block_imp_0
结构体的第一个成员变量impl
,就是__block_impl
结构体类型;
尤其注意:
- 该结构体中包含有
isa
指针,从这一点就可以说明Block
本质上还是一个OC
对象,因为OC
中只有对象才会具有isa
指针的概念; -
FuncPtr
是一个函数指针,在__main_block_imp_0
构造函数调用时被赋值;
3.__main_block_desc_0结构体
__main_block_imp_0
结构体构造函数中传入参数desc
,其实就是__main_block_desc_0
对象。该结构体包含两个成员变量:
-
reserved
:系统保留值; -
Block_size
:代表Block
的大小;
4.__main_block_func_0函数
__main_block_imp_0
结构体构造函数中传入函数指针fp
,其实就是__main_block_func_0
函数的地址;
该函数是将Block
中所有的代码封装为函数,以待被调用;
5.总结Block的特点
-
Block
本质上一个OC
对象:比如这里的Block
,其底层对应了__main_block_impl_0
结构体,而且内部包含有isa
指针; -
Block
中携带了函数执行的环境:此处Block
里待执行的代码,在底层被封装为__main_block_func_0
函数,以实现调用; -
Block
相当于其他语言中的闭包或者匿名函数:它与函数区别在于,Block
相当于函数+函数执行的上下文环境
(捕获外部变量下面会讲到);
二、验证Block的本质是对象
下面通过打印的方式验证Block
对象本质,测试代码如下:
- (void)testBlock5 {
void(^block)(int a) = ^(int a) {
NSLog(@"This is a block");
};
NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
}
//打印结果:
//__NSGlobalBlock__
//__NSGlobalBlock
//NSBlock
//NSObject
观察打印结果:
- 我们看到
Block
最终继承于NSObject
类型,这里再一次验证了Block
本质就是OC
对象的结论; - 而打印结果中出现的
__NSGlobalBlock__
,说明此处的Block
的存储域为静态区;
三、理解Block的存储域分类
在之前Block
结构体构造函数中,我们很容易能找到这样一句代码:
impl.isa = &_NSConcreteStackBlock;
我们已经知道Block
也是一个OC
对象,而每个OC
对象都有一个isa
指针指向其类对象,这里的情况也是类似的;
这里Block
的isa
指针指向了_NSConcreteStackBlock
类对象,即此时的Block
是以_NSConcreteStackBlock
类为模板创建的实例;
除此之外,其实还有两个与之类似的类_NSConcreteGlobalBlock
和_NSConcreteMallocBlock
,不同的Block
类创建的对象用于不同的存储域,也对应了对应不同的OC
类型,具体整理如下:
clang类 | OC类 | 内存区域 |
---|---|---|
_NSConcreteStackBlock |
__NSStackBlock__ |
栈区 |
_NSConcreteMallocBlock |
__NSMallocBlock__ |
堆区 |
_NSConcreteGlobalBlock |
__NSGlobalBlock__ |
静态区 |
四、验证Block的不同存储域
不同存储域的Block
使用方式有很大差别,而正确区分Block
类型的关键在于:Block中是否引用了自动变量(需要MRC下测试),总结起来如下:
Block类型 | 环境 | 内存区域 |
---|---|---|
_NSConcreteGlobalBlock(__NSGlobalBlock__) |
没有访问自动变量; 或者只用到静态区变量 |
静态区 |
_NSConcreteStackBlock( __NSStackBlock__) |
访问了自动变量 | 栈区 |
_NSConcreteMallocBlock(__NSMallocBlock__) |
__NSStackBlock__ 调用了copy
|
堆区 |
为了验证上述情况,我们需要切换到MRC
环境下,因为在ARC
环境下的编译器为我们做了很多优化的工作,比如自动将栈区的Block
拷贝到堆区,这样我们也就不容易捕获到Block
初始状态的位置了。
所以,这里暂时将开发环境切换至MRC
下来测试,相关的测试代码如下:
- (void)testBlock7 {
//1.Block内部没有调用外部自动变量
void (^block1)(void) = ^{
NSLog(@"Block");
};
//2.Block内部调用外部自动变量
int a = 10;
void (^block2)(void) = ^{
NSLog(@"Block-%d",a);
};
//3.拷贝栈上的block
void (^block3)(void) = ^{
NSLog(@"Block-%d",a);
};
//打印Block类型
NSLog(@"%@ %@ %@", [block1 class], [block2 class], [[block3 copy] class]);
}
//打印结果:
//__NSGlobalBlock__ __NSStackBlock__ __NSMallocBlock__
1.NSGlobalBlock(静态区)
- 判断依据:
Block
中没有引用自动变量或者只用到静态区变量; - 此类型的
Block
与全局变量一样设置在程序的静态区,直到程序结束才会被回收; - 此类型的
Block
不依赖执行时的状态,所以整个程序只需一个实例,用的也较少;
2.NSStackBlock(栈区)
- 判断依据:
Block
中访问自动变量,并且存放在栈中; - 栈中的内存由系统自动分配和释放,作用域执行完毕之后就会被立即释放;
- 所以我们有可能遇到
Block
内存销毁之后才使用它的情况,开发中遇到的很多问题也都是因此而起;
3.NSMallocBlock(堆区)
-
_NSStackBlock__
执行copy
操作会生成__NSMallocBlock__
; - 栈
Block
被拷贝后存放在堆中后,需要我们自己进行内存管理,否则还可能造成一些循环引用的问题;
五、分析Block的Copy原理
Block
有着不同的存储域类型,尤其是配置在栈上的Block
(即__NSStackBlock__
类型的Block
),如果其所属的作用域结束,该Block
就会被释放,此时若继续使用Block
,就会造成野指针问题;
所以,我们通常的做法就是执行copy
操作,将其由栈区拷贝到堆区得到__NSMallocBlock__
,而__NSMallocBlock__
也会在其引用计数为0
的时候被释放;
进一步分析Block
的拷贝,需要分为MRC
和ARC
两种环境来考虑。
1.MRC下的Block拷贝
在MRC
环境下,我们只能显式的通过copy
来实现Block
的拷贝;通常为了避免Block
的释放,我们定义Block
属性的时候必须使用copy修饰符
也正是基于这个原因。
下面是在MRC
环境下测试栈Block
的使用,具体代码如下:
typedef void(^PrintBlock)(void);
@interface ViewController ()
@property (nonatomic ,copy)PrintBlock block1;
@property (nonatomic ,copy)PrintBlock block2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self createBlock];
self.block1();
self.block2();
NSLog(@"block1:%@", [self.block1 class]); //报错Thread 1: EXC_BAD_ACCESS (code=1, address=0x7ffeeb90b8c0)
NSLog(@"block2:%@", [self.block2 class]);
}
- (void)createBlock {
int a = 10;
//此处采用直接赋值的方式,不会触发setter方法
_block1 = ^{
NSLog(@"This is block1-%d",a);
};
self.block2 = ^{
NSLog(@"This is block2-%d",a);
};
//离开此作用域,block1就会被释放
NSLog(@"block1:%@、block2:%@", [self.block1 class],[self.block2 class]);
}
@end
打印结果及分析如下:
block1:__NSStackBlock__、block2:__NSMallocBlock__
This is block1-10
This is block2-10
由于block1
采用的是直接赋值的方式,没有调用setter
方法,所以block1
并没有被拷贝到堆上,是一个栈上的Block
,这样也就直接导致了第二次打印block1
时所发生的野指针崩溃;
2.ARC下的Block拷贝
在ARC
环境下,编译器会根据情况自动将栈上的Block
复制到堆上,总结有以下几种情况:
-
Block
作为函数返回值时;这就类似于MRC
中对返回值Block
执行了[[returnedBlock copy] autorelease]
; -
Block
被强引用,如Block
被赋值给__strong
或者id
类型; -
Block
作为GCD API
的方法参数时; -
Block
作为系统方法名含有usingBlock
的方法参数时;
下面的代码演示了这些情况:
typedef void(^Block)(void);
-(Block)getBlock{
//ARC下的Block中访问了auto变量,此时block类型应为__NSStackBlock__
int a = 10;
return ^{
NSLog(@"---------%d", a);
};
}
- (void)testBlock9 {
//1.测试block作为函数返回值时
NSLog(@"bock1-:%@",[[self getBlock] class]);
//2.测试将block赋值给__strong指针时
int a = 10;
//2.1.block内没有访问auto变量
Block block21 = ^{
NSLog(@"block21");
};
NSLog(@"block21-%@",[block21 class]);
//2.2.block内访问了auto变量,但没有赋值给__strong指针
NSLog(@"block22-%@",[^{
NSLog(@"block22-%d", a);
} class]);
//2.3.block赋值给__strong指针
Block block23 = ^{
NSLog(@"block23");
};
NSLog(@"block23-%@",[block23 class]);
//3.block作为Cocoa API中方法名含有usingBlock的方法参数时
NSArray *array = @[@"1",@"2",@"3"];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
//4.block作为GCD API的方法参数时
//Block中的延时操作完成时,系统将会对Block进行释放
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
}
//打印结果如下:
//bock1-:__NSMallocBlock__
//block21-__NSGlobalBlock__
//block22-__NSStackBlock__
//block23-__NSGlobalBlock__
3.其他存储域Block的拷贝
上面讲述的重点都于对栈Blok
的拷贝,若是对于已经配置在堆上或者配置在静态区的上的Block
调用copy
方法又将如何呢?下面是不同存储域的Block
执行copy
进行的总结:
Block 类型 |
副本源的配置存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock |
栈区 | 从栈复制到堆 |
_NSConcreteGlobalBlock |
静态区 | 什么也不做 |
_NSConcreteMallocBlock |
堆区 | 引用增加 |
4. 总结Block需要拷贝的原理
Block
默认创建于其所在函数的函数栈上,所以当函数作用域结束时就会随之销毁;
在MRC
环境下,没有编译器的优化,所以我们非常强调要使用copy
将Block
拷贝到堆上,从而避免Block
在其作用域结束时被直接释放;
在ARC
环境下,编译器会根据情况自动将栈上的Block
复制到堆上,对于Block
使用copy
还是strong
效果是一样的,所以写不写copy
都行。在ARC
环境下对于Block
依然使用copy
,更像是从MRC
遗留下来的“传统”,时刻提醒我们:编译器自动对Block
进行了拷贝操作。如果不写copy
,该类的调用者有可能会忘记或者根本不知道“编译器会自动对Block
进行了拷贝操作”,他们有可能会在调用之前自行拷贝属性值,这种操作多余而低效。
最后,总结Block
修饰符的使用:
//MRC下block属性的建议写法:
@property (copy, nonatomic) void (^block)(void);
//ARC下block属性的建议写法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);