Block
1. Block是啥?
答:Block是将函数
及其执行上下文
封装起来的对象
。
#import "FXBlock.h"
@implementation FXBlock
- (void)method{
int mulNum = 6;
int (^Block)(int) = ^int(int num){
return num * mulNum;
};
Block(2);
}
@end
使用终端编译.m内容:
编译之后会生成FXBlock.cpp
文件
打开FXBlock.cpp
文件,找到下面这段代码
static void _I_FXBlock_method(FXBlock * self, SEL _cmd) {
int mulNum = 6;
// Block编译之后的代码
int (*Block)(int) = ((int (*)(int))&__FXBlock__method_block_impl_0((void *)__FXBlock__method_block_func_0, &__FXBlock__method_block_desc_0_DATA, mulNum));
((int (*)(__block_impl *, int))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block, 2);
}
// @end
分析:
-
method
方法经过编译器编译成为_I_FXBlock_method
函数(I
代表的是当前类的实例方法,FXBlock
代表的就是当前类的类名,method
就是oc
中的方法名称),有两个参数分别为self
和选择器因子。 -
__FXBlock__method_block_impl_0
是一个结构体,有三个参数,第一个__FXBlock__method_block_func_0
是一个空类型的函数指针,第二__FXBlock__method_block_desc_0_DATA
是关于Block
的相关描述的结构体,第三个是被Block所使用的局部变量。 - 把
&__FXBlock__method_block_desc_0_DATA
结构体地址做int强制类型转换,赋值给定义的Block
变量,所以本质上讲Block
定义的变量是一个函数指针,然后指向了一个结构体。
下面来看下Block结构体指针指向的结构体__FXBlock__method_block_impl_0
所代表的实际含义:
// @implementation FXBlock
struct __FXBlock__method_block_impl_0 {
struct __block_impl impl;
struct __FXBlock__method_block_desc_0* Desc; //关于Block的相关描述
int mulNum; //Block中使用的局部变量
__FXBlock__method_block_impl_0(void *fp, struct __FXBlock__method_block_desc_0 *desc, int _mulNum, int flags=0) : mulNum(_mulNum) {
impl.isa = &_NSConcreteStackBlock; // isa赋值
impl.Flags = flags; // 标记位赋值
impl.FuncPtr = fp; // 函数指针的赋值
Desc = desc; // Block描述赋值
}
};
分析:
-
__FXBlock__method_block_impl_0
其中__
代表的是结构体构造函数的声明或者说是定义,在前面的定义Block
的时候,已经使用到了这个构造函数。 - 第一个参数
fp
是一个函数指针,第二个desc
是Block
的描述,第三个_mulNum
是被Block
所使用到的局部变量,第四个flags
是标记。 - :
mulNum(_mulNum)
这句代码的意思是将这个函数所传进来的_mulNum
直接赋值给结构体中mulNum
这个变量。
下面来看下__block_impl
结构体所代表的含义,首先找到__block_impl
的定义
struct __block_impl {
void *isa; //isa指针,Block是对象的标志
int Flags;
int Reserved;
void *FuncPtr; //函数指针
};
// Runtime copy/destroy helper functions (from Block_private.h)
分析:有四个成员变量,因为有isa
指针与objc_class
具有同样的特性,所以可以把Block
理解为一个对象,另一个关键的成员变量就是FuncPtr
,在Block
中所定义的{}
执行体,最终会产生一个函数,然后Block
通过一个函数指针来执行对于的函数实现。
我们在看以下,前面Block结构体定义后面的一段代码:
static int __FXBlock__method_block_func_0(struct __FXBlock__method_block_impl_0 *__cself, int num) {
int mulNum = __cself->mulNum; // bound by copy
return num * mulNum;
}
分析:
-
__FXBlock__method_block_func_0
其中FXBlock
为类的名称,method
是BLock
所在的方法,第三个是这个block
,后面func
是函数的意思。 - 函数有两个参数,第一个是我们前面分析过的
__FXBlock__method_block_impl_0
结构体,第二个num
是我们所定义的Block
所传递进来的参数。 - 函数内部通过
__cself
也就是我们的结构体入参,然后__cself->mulNum
取出成员变量mulNum
,然后与num
进行数值的乘法操作,这个就是对应我们在xcode工程中定义的return num * mulNum;
这段代码所转化成函数指针。
总结:
Block
实际上就是一个对象封装了函数以及函数的执行上下文。
Block调用
还是在.cpp
文件中,下面这段代码是method
方法编译之后的代码:
static void _I_FXBlock_method(FXBlock * self, SEL _cmd) {
int mulNum = 6;
/*
int (^Block)(int) = ^int(int num){
return num * mulNum;
};
*/
int (*Block)(int) = ((int (*)(int))&__FXBlock__method_block_impl_0((void *)__FXBlock__method_block_func_0, &__FXBlock__method_block_desc_0_DATA, mulNum));
/*
Block(2);
*/
((int (*)(__block_impl *, int))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block, 2);
}
分析:
- 可以看到
Block(2);
编译之后其实就是函数的调用,首先是对Block
强制类型转换,然后取出FuncPtr
函数指针对应的函数执行体,把对应的参数传进入,第一个参数是Block
,第二个是传进入的参数2
。 - 然后通过调用前面描述的
__FXBlock__method_block_func_0
函数,通过从传递进来的Bloc
k这个结构体中mulNum
成员变量的提取,进行Block
的时机调用。 - 所以说
Block
调用就是函数调用。
截获变量
下面这段代码输出多少?
- (void)method1{
int mulNum = 6;
int (^Block)(int) = ^int(int num){
return num * mulNum;
};
mulNum = 4;
NSLog(@"result is %d", Block(2));
}
打印如下:
答案分析:因为mulNum是一个基本数据类型的局部变量在定义的时候就以值的方法传递到了Block所对应的结构体中,而具体的函数调用是使用Block具体对应的结构体中的mulNum,而不是我们在方法声明中的mulNum,所以输出的值是12。
问题:针对不同的类型的变量,Block对其截获的特点也是不一样的,对于局部变量的截获,分为基本数据类型和对象类型,Block对于这两种局部变量数据类型的截获特点是不一样的,对于静态局部变量,全局变量,静态全局变量也是不一样的,下面我们来具体分析一下对于不同数据类型的变量截获的处理细节。
如果使用静态局部变量来修饰的话,就可以输出8,因为在Block结构体中保存了静态局部变量的指针,在调用的时候也是通过指针来找到对应静态局部变量的值。
基本数据类型:对于基本数据类型的局部变量截获其值。
对象类型:对于对象类型的局部变量联通所有权修饰符一起截获。
局部静态变量:以指针形式截获局部静态变量。
全局变量和静态全局变量:不截获。
下面我们来验证下:
#import "FXBlockVar.h"
@implementation FXBlockVar
// 全局变量
int global_var = 4;
// 静态全局变量
static int static_global_var = 5;
- (void)method
{
//基本数据类型的局部变量
int var = 1;
//对象类型的局部变量
__unsafe_unretained id unsafe_obj = nil;
__strong id strong_obj = nil;
//局部静态变量
static int static_var= 3;
void(^Block)(void) = ^{
NSLog(@"局部变量<基本数据类型> var %d", var);
NSLog(@"局部变量<__unsafe_unretained 对象类型> var %@", unsafe_obj);
NSLog(@"局部变量<__strong 对象类型> var %@", strong_obj);
NSLog(@"静态变量 %d", static_var);
NSLog(@"全局变量 %d", global_var);
NSLog(@"静态全部变量 %d", static_global_var);
};
Block();
}
@end
使用clang -rewrite-objc -fobjc-arc FXBlockVar.m
命令编译文件
int global_var = 4;
static int static_global_var = 5;
struct __FXBlockVar__method_block_impl_0 {
struct __block_impl impl;
struct __FXBlockVar__method_block_desc_0* Desc;
//截获局部变量
int var;
//连同所有权修饰符一起截获
__unsafe_unretained id unsafe_obj;
__strong id strong_obj;
//以指针形式截获静态局部变量
int *static_var;
//对全局变量,静态全局变量不截获
__FXBlockVar__method_block_impl_0(void *fp, struct __FXBlockVar__method_block_desc_0 *desc, int _var, __unsafe_unretained id _unsafe_obj, __strong id _strong_obj, int *_static_var, int flags=0) : var(_var), unsafe_obj(_unsafe_obj), strong_obj(_strong_obj), static_var(_static_var) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
分析:
- 如果在调用过程中对静态局部变量进行值的修改,再调用Block的时候,用的就是最新的静态局部变量的值。
- 而且没有看到全局变量,静态全局变量的截获,对全局变量,静态全局变量不截获
下面看下__FXBlockVar__method_block_impl_0构造实现位置
static void _I_FXBlockVar_method(FXBlockVar * self, SEL _cmd) {
int var = 1;
__attribute__((objc_ownership(none))) id unsafe_obj = __null;
__attribute__((objc_ownership(strong))) id strong_obj = __null;
static int static_var= 3;
void(*Block)(void) = ((void (*)())&__FXBlockVar__method_block_impl_0((void *)__FXBlockVar__method_block_func_0, &__FXBlockVar__method_block_desc_0_DATA, var, unsafe_obj, strong_obj, &static_var, 570425344));
((void (*)(__block_impl *))((__block_impl *)Block)->FuncPtr)((__block_impl *)Block);
}
分析:同样的__FXBlockVar__method_block_impl_0传递的参数,__FXBlockVar__method_block_func_0为Block的执行体的函数指针,&__FXBlockVar__method_block_desc_0_DATA为Block的描述,var为我们定义的整型局部变量或者可以说是基本数据类型,unsafe_obj和strong_ob是直接透传到结构体中的对象,对于静态局部变量是直接传递的&static_var静态局部变量的指针到结构体当中,对结构体中的整型指针变量进行赋值。
__Bock 修饰符
一般情况下,对被截获变量进行赋值操作需要添加__Bock 修饰符。
下面这段代码是否需要对array使用__Bock修饰符
- (void)method2{
NSMutableArray *array = [NSMutableArray array];
void(^Block)(void) = ^{
[array addObject:@123];
};
Block();
}
分析:这里是不需要使用__Block修饰符的,[array addObject:@123];只是对array进行了使用并没有进行赋值。
下面这种情况就需要使用__Block修饰符
- (void)method3{
__block NSMutableArray *array = [NSMutableArray array];
void(^Block)(void) = ^{
array = [NSMutableArray array];
};
Block();
}
对于变量进行赋值时不论是基本数据类型,还是对象类型,都需要使用__Block进行修饰。
那么什么情况下不需要使用__Block修饰符?
- 对于静态局部变量,全局变量,静态全局变量进行赋值的时候是不需要使用__Block来修饰的。
- 全局变量,静态全局变量,对于这两种变量是不进行截获操作,是直接进行进行使用的,所以在对其值进行操作的时候是不需要__Block修饰的。
- 对于静态局部变量是通过使用指针形式来使用和操作对应变量的,实际上就是操作的就是Block外部的变量,所以也是不要使用__Block修饰符的。
回到之前的例子,稍作修改,下面这段代码输出多少?(__block所起的作用或者的原理是啥?)
- (void)method1{
__block int mulNum = 6;
int (^Block)(int) = ^int(int num){
return num * mulNum;
};
mulNum = 4;
NSLog(@"result is %d", Block(2));
}
打印如下:
分析:
__block修饰的变量变成了对象,还是用clang编译
struct __Block_byref_mulNum_0 {
void *__isa;
__Block_byref_mulNum_0 *__forwarding;
int __flags;
int __size;
int mulNum;
};
分析:
- 发现用__block修饰的变量变成了结构体或者对象。
- 对于mulNum=4这段代码,编译成了(mulNum.__forwarding->mulNum) = 4;,通过mulNum这个对象中的__forwarding指针,对其成员变量mulNum进行赋值。
Block内存管理
Block编译之后有一段代码:
impl.isa = &_NSConcreteStackBlock;
分析:其实这段代码就是用来标识当前block是那种类型的block。
Block有三种:
_NSConcreteGlobalBlock //全局类型的
_NSConcreteStackBlock //栈类型的
_NSConcreteMallocBlock //堆类型的
三种Block在内存中的布局:
对于Block使用copy操作:
分析:对于栈上的Block进行copy,会在堆上产生一个Block,对于全局Block进行copy,和什么都没做是一样的,对于堆上的Block使用copy,会增加其引用计数。
我们声明一个对象的成员变量是一个Block,而在栈上面创建这个block赋值给对象成员变量,如果这个成员变量没有使用copy关键字修饰的话,比如说使用assign,那么我们如果通过成员变量去访问这个block的话,可能会由于栈所对应的函数推出之后在内存中销毁,在这个时候在继续访问会导致内存崩溃。
栈上BLock的销毁
分析:
如果我们在栈上有一个__block变量同时有一个Block,在变量的作用域结束之后,或者说栈上的函数退出之后,栈上面的__block和Block都会随之销毁,由于__block修饰的变量实际上已经变成了对象,所以会有一个__block销毁的逻辑。
栈上Block的copy
假如我们在栈上有一个Block,Block中使用到了一个__block变量,当我们对栈上的Block进行copy操作之后,会在堆上面
产生一个和原来栈上面一样的对应的Block和对应的__block变量,但是会分占两块不同的空间,左侧在栈上,右侧在堆上。
随着变量作用域的结束,栈上面的Block和__block都会销毁掉,但是堆上面的Block和__block依然存在
这里可能有人会问,当我们对栈上面的Block进行copy之后,假如在MRC环境下是否会引起内存泄露?答案是会引起内存泄露。因为当我们对栈上的Block进行copy之后,同时堆上面的Block没有其他成员变量去指向的话其实和我们调用alloc创建一个对象没有release是一个道理,会产生内存泄露。
接下来我们看下栈上__block变量的Copy操作发生了什么?
分析:假如我们在栈上有一个__block变量,在__block变量中有一个__forwarding指针,__forwarding其实是指向自身的(前提是它是一个栈上__block变量),当对__block变量进行copy之后,在堆上面会产生一个__block变量(与栈上的是完全一致的,是两块内存空间)
注意:这里有一个额外的变化,我们对栈上的__block变量进行copy操作之后,栈上的__forwarding指针实际上指向的是堆上面的__block变量,而堆上面的__forwarding指针指向的是其自身,所以说对于前面multiplier这么一个整型值进行改变的时候,使用的都是转换过来的同一行代码(mulNum.__forwarding->mulNum) = 4;不论对于栈上的还是堆上的Block操作都是生效的,比如说我们对栈上的一个__block进行值的修改,如果说我们已经对于栈上的__blcok已经进行copy操作之后,实际上我们不是修改的栈上的__block变量对应的值,而是通过栈上面__block变量里面__forwarding指针找到堆上面的__block变量,然后对堆上面对应的multiplier值进行修改赋值,同样的如果__block变量被一个成员变量的一个block所持有的话,当我们在其他地方去调用__block中对应的变量进行修改的时候,实际上是通过自身的__forwarding指向来修改的。
如果说没有对栈上面__block进行copy操作,那么修改的就是对应栈上的__block变量。
那么经过上面的讨论,请看下面这段代码输出的内容?
- (void)method5{
__block int mulNum = 10;
_blk = ^int(int num){
return num * mulNum;
};
mulNum = 6;
[self executeBlock];
}
- (void)executeBlock{
int result = _blk(4);
NSLog(@"result is %d", result);
}
打印如下:
__forwarding存在的意义:不论在热河内存位置,都可以顺利的访问同一个__block变量
解释一下:如果我们没有对__block进行copy,实际上操作的就是栈上面的__block变量,如果发生了copy之后,不论在栈上还是堆上,我们对__block变量的修改或者赋值,都是操作的堆上的__block变量,同时栈上的关于__block变量的使用的也是堆上__block变量。
Block的引用循环
下面这段代码会形成循环引用
- (void)method6{
_array = [NSMutableArray arrayWithObject:@"block"];
_strBlk = ^NSString*(NSString *num){
return [NSString stringWithFormat:@"hello_%@", _array[0]];
};
_strBlk(@"hello");
}
分析: 由于当前对象是通过copy属性声明的的_strBlk,所以当前对象对于_strBlk是有一个强引用关系在的,而block表达式中又使用到了_array成员变量,在前面截获变量中介绍过Block中所使用到的对象类型的局部变量或者说成员变量,会连同其属性关键字共同进行截获,array属性关键字是strong来修饰的,所以在block当中就有一个strong类型的指针,指向了当前对象,由此就产生了一个循环引用。
那么如何去规避这种情况呢?
可以通过在当前栈去什么或者创建一个__weak所有权修饰符修饰的一个weakArray指针变量,来指向原对象的_array成员变量,然后在block中使用创建的weakArray,这样就可以解除原来的自循环引用。(这种方式是通过避免形成循环引用来循环引用的)
为什么通过weak属性关键字修饰就可以避免循环引用呢?
由于我们block对其所截获变量如果是对象类型的,会连同其所有权修饰符一起截获,所以如果定义是__weak修饰的,在block生成的结构体里面所持有的成员变量也是__weak类型的,由此就可以解释通过外部定义__weak修饰的变量,就可以避免循环引用。
下面这段代码有什么问题?
- (void)method7{
__block FXBlockVC *blockSelf = self;
_blk = ^int(int num){
return num * blockSelf.var;
};
_blk(3);
}
分析:在MRC下,不会产生循环引用。在ARC下回产生循环引用,引起内存泄露。
分析: 对象有一个成员变量持有Block,Block中使用到了__block所修饰的变量,所以Block持有了__block变量,__block变量对原对象有一个强引用的持有,因为__block指向是原来的对象,这种循环引用就是打环引用,可以通过断环或者避免的方式来解除循环引用。
断开__block对原对象的持有,代码修改如下:
- (void)method8{
__block FXBlockVC *blockSelf = self;
_blk = ^int(int num){
int result = num * blockSelf.var;
blockSelf = nil;
return result;
};
_blk(3);
}
注意:这种方案有一个弊端,如果把我们不调用这个block的话,循环引用就一直存在。
Block总结:
- Block是关于函数及其上下文封装起来的一个对象。(对函数的封装,同时也是对函数执行上下文的封装,而且它是一个对象)
- Block产生循环引用的原因,如果当前Block对当前成员变量进行截获的话,Block会对相应变量有一个强引用,而当前Block又由于当前对象对其有一个强引用,就产生了一个自循环引用方式的一种循环引用问题,我们可以通过声明其为__weak变量来进行循环引用的消除。
如果我们定义了一个__Blcok修饰符的话,也会产生循环引用,在MRC下不会产生循环引用,在ARC下会产生循环引用,可以通过断环的方式来解除循环引用,但是有一个弊端,如果我们一直不调用这个Block,这个循环引用就会一直存在。 - Block在截获变量对于不同类型的变量有不同的处理,对于基本数据类型的局部变量是对其值进行截获,而对于对象类型的局部变量,是对其进行强引用,或者连同其所有权修饰符共同截获,对于静态局部变量对其指针进行截获,对于全局变量和静态全局变量是不产生截获的。
- 遇到Block循环引用,由于block所引起的循环引用,有两方面,一方面是Block中所捕获变量是当前对象的成员变量,当前Block也是当前对象的成员变量,就会造成自循环的循环引用,可以通过加__weak修饰符,来避免形成循环引用,同时__Blcok也会形成循环引用,前面已经讨论过,通过断环的方式来解决。