目录
- block概要
- 自动变量的截获
- block的调用本质
- block的内存管理
- block的循环引用
1.block概要
在刚接触iOS的时候,block真是一个让人头疼的东西,基本上所有的第三方框架都用了block,然后在我们阅读这些大神框架的时候,如果没有学好block,不了解block的用法,那基本上无法把这些第三方框架给吃透。
那么什么是block呢,我从一些书上得到的答案是:带有自动变量的匿名函数,顾名思义,匿名函数就是不带有名称的函数。带有自动变量这个意思我们在后面再具体讲。下面我们看看block的语法:
block语法:
^ 返回值类型 参数列表 表达式
例如
^ int ( int a){
return a+1;
}
- 一般在开发过程中,我们可以省略返回值类型
^ 参数列表 表达式
例如
^( int a){
return a+1;
}
注意: 当我们省略返回值时,如果表达式中有return语句,则返回值类型为该返回值的类型,如果没有return语句,则返回值类型为void类型。如果表达式中有多个return语句,所有的返回值必须是相同类型,不然会报错。 报错信息为:
Return type '类型1 *' must match previous return type '类型2' when block literal has unspecified explicit return type
- 其次在没有参数的时候,我们还可以省略其参数
例如
^{
NSLog(@"省略参数");
}
Block类型变量
声明BLock属性的时候,需要定义一个属性类型:
@property (nonatomic,copy) void (^cpBlock)(void);
_cpBlock = ^{
NSLog(@"111");
};
_cpBlock();
- 当block作为参数的时候:
- (void)add:(void (^)(void))block{
block();
}
[self add:^{
NSLog(@"111");
}];
- 当block作为返回值的时候:
- (void (^)(void))sum{
return ^{
NSLog(@"111");
};
}
[self sum]();
但是 我们一般在开发中并不会直接这么用,因为这样写也太繁琐了。我们一般使用typedef
来解决问题:
typedef void (^Blk)(void);
@property (nonatomic,copy) Blk block;
- (void)add:(Blk)block{
block();
}
- (Blk)sum{
return ^{
NSLog(@"111");
};
}
2.自动变量的截获
上面我们讲了block是带有自动变量的匿名函数,我们已经解释了匿名函数,那么什么是带有自动变量呢,我们先来看看下面的代码:
typedef int (^Blk)(void);
int age = 10;
Blk block = ^{
NSLog(@"age = %d", age);
};
age = 20;
block();
相信有点开发经验的同学,应该马上知道结果:
结果是:age = 10
block在使用过程中,会截获表达式中所使用的自动变量的值,即保存该自动变量的瞬间值。因为block在内部保存了这个自动变量的值,所以在执行block语法后,即使在修改block中使用的自动变量的值也不会影响block执行时自动变量的值。如果block表达式中不使用自动变量,则不会截获,因为截获的自动变量会存储于block的结构体内部, 会导致block体积变大。特别要注意的是默认情况下block只能访问不能修改局部变量的值。
截获变量的类型
- 局部变量
对于基本数据类型的局部变量截获其值
对于对象类型的局部变量连同所有权修饰符一起截获
- 静态局部变量
以指针形式截获局部静态变量
- 全局变量
不截获
- 静态全局变量
不截获
__block修饰符
默认情况下,block只能访问不能修改局部变量的值,而且在表达式后再修改block语法外声明的自动变量,无法影响block内部的自动变量。那么如果我们想在block语法的表达式将值赋给block语法外声明的自动变量,则需要在该自动变量上附加__block修饰符。
typedef void (^Blk)(void);
__block int age = 10;
Blk block = ^{
NSLog(@"age = %d", age);
age = 5;
NSLog(@"age = %d", age);
};
age = 20;
block();
结果是:
age = 20
age = 5
那么这是为什么呢,我们可以在命令行输入代码 clang -rewrite-objc 需要编译的OC文件.m,看一下block底层是怎么实现的。
__block修饰的时候
__block int age = 10;
Blk block = ^{
age;
};
age = 20;
block();
转化成cpp后是
Blk block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
struct __main_block_impl_0 {
struct __block_impl impl;//封装了函数实现的结构体
struct __main_block_desc_0* Desc;// 里面有内存管理函数,Block_size表示block的大小
__Block_byref_age_0 *age; // by ref
// 这个结构体的初始化函数 , 入参 : fp,函数实现的函数指针, __main_block_desc_0,占用大小的描述,age传入的是一个地址
// 返回一个__main_block_impl_0类型的结构体,赋值给了block
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;//栈block
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
无__block修饰的时候
int age = 10;
Blk block = ^{
age;
};
age = 20;
block();
转化成cpp后是
Blk block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
struct __main_block_impl_0 {
struct __block_impl impl;//封装了函数实现的结构体
struct __main_block_desc_0* Desc;// 里面有内存管理函数,Block_size表示block的大小
int age;// 捕获到的普通局部变量
// 这个结构体的初始化函数 , 入参 : fp,函数实现的函数指针, __main_block_desc_0,占用大小的描述
// 返回一个__main_block_impl_0类型的结构体,赋值给了block
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _age, int flags=0) : age(_age) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
相比之下,__block修饰后,把变量变成来__Block_byref_age_0结构体对象,而没有__block修饰时,捕获的是变量的瞬间值,所以结果显而易见。
struct __Block_byref_age_0 {
void *__isa;
__Block_byref_age_0 *__forwarding;
int __flags;
int __size;
int age;
};
这个结构体有一个__forwarding指针,这个指针指向这个结构体,通过这个__forwarding可以访问结构体的成员变量age
下面我们来看下下面的代码:
{
NSMutableArray *array =[NSMutableArray array];
void (^Block)(void) = ^{
[array addObject:@123];
};
Block()
}
这段代码需要给array加__block吗
不需要,因为这个只是使用,并没有对array赋值
注意 静态局部变量、全局变量、静态全局变量不需要添加__block
3.block的调用本质,
block的调用其实就是函数的调用,我们通过源码去看
(1)block定义的时候,把表达式传给__main_block_func_0
Blk block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
看一下__main_block_impl_0的结构体实现:
struct __main_block_impl_0 {
struct __block_impl impl;//封装了函数实现的结构体
struct __main_block_desc_0* Desc;/ 里面有内存管理函数,Block_size表示block的大小
__Block_byref_age_0 *age; // by ref
//构造函数
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
//把__main_block_func_0函数赋值给__block_impl结构体的FuncPtr
impl.FuncPtr = fp;
Desc = desc;
}
};
再看下这个函数实现结构体__block_impl的代码:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
(2)然后我们再看__main_block_func_0的函数定义
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int age = __cself->age; // bound by copy
age;
}
(3) 然后再看下block调用的代码
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
先是把block强转成__block_impl类型,然后取出FuncPtr属性,而这个属性就是__main_block_impl_0结构体里面构造函数的fp,也就是(1)里面的__main_block_func_0函数。
所以,block的调用其实就是__main_block_func_0函数的调用,也就是表达式的调用。
4.block的内存管理
block的存储域
block是一个对象,根据block对象的存储域,可以分为以下几种:
1._NSConcreteStackBlock 存储在栈上
2._NSConcreteGlobalBlock 存储在程序的数据区域(.data区)
3._NSConcreteMallocBlock 存储在堆上
根据结构体__block_impl里面的的isa指针,可以判断block的存储域
Block的Copy操作
1.对于栈上的block,copy后会在堆上产生Block
2.对于已初始化数据取的全局block,copy后什么也不做
3.对于堆上的block,copy后会增加引用计数
那么什么时候block会被copy呢?
1.调用Block的copy实例方法时
2.Block作为函数返回值返回时
3.将Block赋值给附有__strong修饰符id类型的类或Block类型成员变量时
4.在方法名中含有usingBlock的Cocoa框架方法或GCD的Api中传递Block时
__block变量的存储域
上面我们讲了block的复制,那么对__block变量又是怎么处理的呢?
__block变量的配置存储域 | Block从栈复制到堆时的影响 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
(1)若只有1个block中使用__block变量,则当该Block从栈复制到堆时,使用的所有__block变量也必定配置在栈上,在复制block的时候,也会把__block变量从栈复制到堆。此时block持有__block变量。如果block已经在堆上,再进行copy,那么堆block所使用的__block变量没有任何影响。
(2) 若多个block中使用__block变量时,因为最先将所有的block配置在栈上,所以 __block变量也会配置在栈上。在任何一个block从栈复制到堆时,__block变量也会一并从栈复制到堆并被该block持有。当剩下的block从栈复制到堆时,被复制的block持有__block变量,并增加__block变量的引用计数。
注意 当__block变量从栈复制到堆时,会同时把栈上的__block变量的__forwarding指针指向堆上的__block变量,通过这一操作,无论在Block表达、Block表达式外使用__block变量,不管__block变量配置在栈上还是堆上,都可以顺利地访问同一个__block变量。
总结:__forwarding的意义:不论在任何内存位置,都可以顺利的访问同一个__block变量。
5.block的循环引用
在开发过程中,block的循环引用应该是我们经常碰到也让人头疼的问题。比如我们看下面的代码块:
_array = [NSMutableArray arrayWithObject:@"block"];
_cpBlock = ^NSString *(NSString *str){
return [NSString stringWithFormat:@"hello_%@",_array[0]];
};
_cokong(@"hello");
这个代码块有什么问题的,相信有开发经验的同学应该能立马看出来,这段代码有block循环引用的问题。
因为我们当前block是用copy
修饰的,而array是用strong
修饰的,而在截获变量中,我们知道,关于block中对象类型的局部变量会连同其属性关键字一起截获,所以在block中有一个strong指针指向self,所以造成了自循环引用。
那么我们怎么去解决呢?
(1)我们可以使用__weak
来解决循环引用,如下代码块
_array = [NSMutableArray arrayWithObject:@"block"];
__weak NSArray *weakArray = _ayyay;
_cpBlock = ^NSString *(NSString *str){
return [NSString stringWithFormat:@"hello_%@",weakArray[0]];
};
_cokong(@"hello");
这样,因为对象截获的时候会连同其属性关键字一起截获,这边block指向的是一个__weak修饰的self,所以不会造成循环引用。
(2)同理,我们可以使用__unsafe_unretained来解决循环引用。如下代码块
_array = [NSMutableArray arrayWithObject:@"block"];
_unsafe_unretained NSArray *unsafe_unretainedArray = _ayyay;
_cpBlock = ^NSString *(NSString *str){
return [NSString stringWithFormat:@"hello_%@",unsafe_unretainedArray[0]];
};
_cokong(@"hello");
因为持有cpBlock的self一定存在,所以使用__unsafe_unretainedArray修饰符,不必担心悬垂指针。
(3)我们还可以通过__block来解决循环引用。如下代码块:
_block id blockSelf = self;
_cpBlock = ^int(NSString *str){
return blockSelf.a;
};
_cokong(@"hello");
其实上面的代码在MRC下一点问题都没有,但是在ARC下存在问题。
用__block修饰self后,self持有cpBlock,cpBlock持有self,这样会造成循环引用。所以我们要在block内部做一个断环的操作,才能解决循环引用。如下代码块:
_block id blockSelf = self;
_cpBlock = ^int(NSString *str){
int a = blockSelf.a;
blockSelf = nil;//断环
return blockSelf.a;
};
_cokong(@"hello");
但是其实,这样做还是有点问题,因为如果我们只是定义了block,而一直不去调用block,那么就永远不能断环,循环引用就一直存在。
总的来说: __block相比较于上面两种方法,优点在于,__block变量可以控制对象的持有时间。缺点在于为了避免循环引用,必须执行block。