Block:带有自动变量值
的匿名函数
1、Blcok对象和语法
a、Block语法
^ 返回值类型 参数列表 表达式
^int (int count){return count+1;}
其中Block语法可省略好几个项目,如下
^ 参数列表 表达式
(省略返回值类型
)
^(int count) {return count+1;}
^ 表达式
(省略返回值类型
以及参数列表
)
示例:^void (void) {NSLog(@"Blocks");};
省略后:^{NSLog(@"Blocks");};
b、Block对象变量声明
关于变量呢,是只能保存若干字节长度的数值,无法保存对象自身。Block变量同样,只能保存block对象的地址。
// 如下,声明一个返回值为int,参数为int类型的a以及b,名称为blockName的Block变量
int (^blockName) (int a, int b);
// 无返回值的
void (^blockName) (int a, int b);
// 无参数、无返回值的
void (^blockName) (void);
c、Block对象
关于Block对象,block对象也需要为其分配内存空间,如下实例:
int (^blockName)(int ,int ) = ^ int(int a, int b) {
return a + b;
};
// 执行block对象
int a = blockName(3,5);
// 输出结果
NSLog(@"%d",a);
2、Block对象常见用途
- 变量
- 函数参数
- 属性
- 回调
a、变量
将Block赋值为Block类型变量
int (^blkName) (int) = ^(int count) {return count+1;};
也可以由Block类型变量向Block类型变量赋值
int (^blkName1) (int) = blkName;
int (^blkName2) (int);
blkName2 = blkName1;
b、函数参数
在函数参数中使用Block类型变量可以向函数传递Block
- (void)func:(int (^) (int))blkName{}
由上述代码可以发现,将Block座位函数参数使用时,方式比较复杂,因此可以使用typedef
来解决
语法为typedef 返回值 (^block名称) (参数列表)
typedef int (^TestBlock)(int);
上述示例则可以修改为:
- (void)func:(TestBlock)blkName{}
c、属性
1)直接使用
typedef int (^TestBlock)(int);
TestBlock blk = ^(int a){
NSLog(@"%d",a);
};
blk(1);
2)声明为对象的属性使用
typedef int (^TestBlock)(int);
@property (nonatomic, strong) TestBlock blk;
//类外声明,获取其值
self.blk = ^(int a) {
NSLog(@"%d",a);
};
//类内调用传值
self.blk(1);
d、回调
声明一个类,名为TestBlockVC
,在该类中声明名为ResultBlock
的block,然后声明一个方法,其中带有两个int
类型参数,ResultBlock
求和,并将结果回调。
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
typedef void(^ResultBlock)(int sum);
@interface TestBlockVC : UIViewController
- (void)calculate:(int)a paramB:(int)b sum:(ResultBlock)result;
@end
NS_ASSUME_NONNULL_END
#import "TestBlockVC.h"
@interface TestBlockVC ()
@end
@implementation TestBlockVC
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
}
-(void)calculate:(int)a paramB:(int)b sum:(ResultBlock)result{
if (result) {
result(a+b);
}
}
@end
然后在ViewController
中执行以下代码:
TestBlockVC *testBlkVc = [[TestBlockVC alloc] init];
[testBlkVc calculate:5 paramB:3 sum:^(int sum) {
NSLog(@"block返回的结果为:%d",sum);
}];
执行结果:
2019-04-17 16:40:59.689549+0800 ReviewIOS[47907:1504259] block返回的结果为:8
以上即为一个回调的示例,将参数给TestBlockVC
的calculate
方法,在方法中执行两数相加,通过block将结果回调。我们通常会在网络请求或者下载中使用到回调,可以使用Block在下载成功或者失败后得到反馈。
e、使用示例
创建一个名为BlockExecutor继承自NSObject的子类,在BlockExecutor.h中,为BlockExecutor类增加一个实例变量和两个方法
@interface BlockExecutor : NSObject
{
int (^equation)(int, int);
}
// setEquation参数为block
- (void)setEquation:(int (^)(int a, int b))block;
// 用来执行block对象,根据传入的实参,执行运算,并返回结果
- (int)computeWithValue:(int)value1 andValue:(int)value2;
@end
@implementation BlockExecutor
-(void)setEquation:(int (^)(int, int))block{
equation = block;
}
-(int)computeWithValue:(int)value1 andValue:(int)value2{
// 判断block变量有没有指向的block对象,如果没有,返回0
if (!equation) {
return 0;
}
// 向block对象传值
return equation(value1, value2);
}
@end
在viewcontroller中导入BlockExecutor,创建BlockExecutor对象,为实例变量赋值,并执行equation指向的block对象且得到计算结果。
int (^block_name)(int, int) = ^int(int a, int b){
return a + b;
};
BlockExecutor *executor = [[BlockExecutor alloc] init];
[executor setEquation:block_name];
int result = [executor computeWithValue:3 andValue:9];
NSLog(@"block------%d",result);
// ------结果输出------
2018-12-06 13:52:25.674905+0800 ReviewIOS[52178:1657298] block------12
也可以不通过block变量,直接对block对象进行赋值,如下:
// int (^block_name)(int, int) = ^int(int a, int b){
// return a + b;
// };
BlockExecutor *executor = [[BlockExecutor alloc] init];
// [executor setEquation:block_name];
[executor setEquation:^int(int a, int b) {
return a + b;
}];
int result = [executor computeWithValue:3 andValue:9];
NSLog(@"block------%d",result);
// ------结果输出------
2018-12-06 14:27:40.781856+0800 ReviewIOS[52477:1670495] block------12
block实例变量和其他实例变量一样,可以声明为类的属性,以替代方法。如下:
@interface BlockExecutor : NSObject
//{
// int (^equation)(int, int);
//}
//- (void)setEquation:(int (^)(int a, int b))block;
@property (nonatomic, copy) int (^equation)(int, int);
- (int)computeWithValue:(int)value1 andValue:(int)value2;
@end
@implementation BlockExecutor
//-(void)setEquation:(int (^)(int, int))block{
// equation = block;
//}
-(int)computeWithValue:(int)value1 andValue:(int)value2{
if (!_equation) {
return 0;
}
return _equation(value1, value2);
}
@end
4、Block常见问题以及解决办法(捕获变量、循环引用)
a、捕获变量
-
捕获自动变量
如下示例:
- (void)func{
int val = 10;
void (^blk)(void) = ^{
NSLog(@"val = %d",val);
// val = 3; //若修改val,则会报错误信息---Variable is not assignable (missing __block type specifier)
};
val = 5;
blk();
}
调用该方法后输出结果如下:
2019-04-15 11:52:53.999094+0800 ReviewIOS[73378:885770] val = 10
由以上结果显示,在这段代码中,Block表达式使用的是在它之前声明的val的值。
Block中,对外部变量的引用,默认是将其复制到Block数据结构中来实现访问的。也就是说Block表达式只截获内部使用的自动变量值,即保存该自动变量的瞬间值,且不能修改该值。
那怎么让Block获取到改变后的自动变量的值呢,或者说如果在Block要修改截获的自动变量值怎么办呢?这就需要使用到__block
说明符
我们先尝试一下对自动捕获的变量值进行修改,看一下结果:
__block
对代码进行修改:
- (void)func{
__block int val = 10;
void (^blk)(void) = ^{
val = 3;
NSLog(@"val = %d",val);
};
val = 5;
blk();
}
修改后输出结果:
2019-04-15 14:12:08.929247+0800 ReviewIOS[84265:973602] val = 3
以上可以看出添加了__block
后,捕获的自动变量可以被修改了。我们可以再看一下,添加__block
之后,在执行Block语法后再修改变量值,结果也是会改变的。
- (void)func{
__block int val = 10;
void (^blk)(void) = ^{
NSLog(@"val = %d",val);
};
val = 5;
blk();
}
输出结果:
2019-04-15 14:13:56.507424+0800 ReviewIOS[84415:975164] val = 5
-
修改Block捕获的自动变量值会编译错误,那么对于对象呢?
上段代码看一下:这段代码调用捕获对象的方法
- (void)func{
id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
id obj = [[NSObject alloc] init];
[array addObject:obj];
};
blk();
}
编译没有报错,说明使用截获的值没有问题,ok没问题,那我们再试一下赋值:
这种情况下,同样我们需要给截获的自动变量添加
__block
说明符。修改代码如下:
- (void)func{
__block id array = [[NSMutableArray alloc] init];
void (^blk)(void) = ^{
array = [[NSMutableArray alloc] init];
};
blk();
}
编译通过
实际上,用
__block
修饰的外部变量引用,Block是复制其引用地址来实现访问的。因此也可以修改__block
修饰的外部变量的值。这点后续我们通过源代码进行说明。
- Block实质
#include <stdio.h>
int main()
{
int val = 10;
void(^blk)(void) = ^{
const char *fmt = "val = %d\n";
printf(fmt, val);
};
val = 5;
blk();
return 0;
}
在终端使用gcc 源文件名
进行编译,然后执行clang -rewrite-objc 源文件名
命令,会生成一个后缀名为.cpp
的文件,打开该文件,会看到如下代码:(这里只贴出主要部分)
从图中(548行 int val;
),我们可以发现,在Block语法中所使用的自动变量是被作为成员变量追加到__main_block_impl_0
结构体中的,需要注意的是,在Block语法中没使用的自动变量是不会被添加的,上图570行 void(*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, val));
是对由自动变量追加的成员变量进行初始化,通过该函数确认其参数。当执行^{printf(fmt, val);}
时,对应转换后的代码为上图556-561行
,通过这个可以发现,在Block语法执行前即blk();
之前val
就已经被声明定义。所以后面再去修改val
的值block输出不会改变。
-
添加
__block
后转换的源代码:
如图可以发现,添加了__block
后,源码多出了很多
__block
其实相当于一个声明,用于指定将变量值设置到一个存储域中
上图中(544-550行
代码),即为__block的转换,可以发现,它是一个结构体。
上图中(563-569行
代码),是赋值代码转换后的源码,在552-562行
代码中,可以看到Block的__main_block_impl_0
结构体实例持有指向__block
变量的__Block_byref_val_0
结构体实例的指针。而__Block_byref_val_0
结构体实例的成员变量__forwarding
持有指向该实例自身的指针,相当于原自动变量。而将__Block_byref_val_0
结构体单独放起来,而不是放在__main_block_impl_0
结构体中,是由于方便第一个Block使用__block
变量。
这里提到了__forwarding
,后续会给出解释。
- 上面提到
__block
是将制定变量值设置到一个存储域中,那么接下来就看一下Block的存储域:
Block的三种类型:
- 全局块(_NSConcreateGlobalBlock) ----存储域在数据区
- 堆块(_NSConcreateMallocBlock)----存储域在堆区
- 栈块(_NSConcreateStackBlock)----存储域在栈区
全局块存在于全局内存中,相当于单例
堆块存在于堆内存中,是一个带引用计数的对象,需要自行管理其内存
栈块存在于栈内存中,超出其作用域则马上被销毁
那么如何确定一个Block的存储位置呢?概括以下两点:
a、Block不访问外界变量(包括栈中和堆中的变量)
全局块:Block既不在堆中也不在栈中,在代码段中,ARC和MRC下都是这样。
b、Block访问外部变量
MRC:访问外部变量的Block默认存储在栈中
ARC:访问外部变量的Block默认存储在堆中,自动释放(解释一点:其实际也是放在栈中的,在ARC环境下自动拷贝到了堆中)
那为什么会被拷贝到堆中呢?
这是由于在栈上的Block,如果其所属的变量作用域结束,该Block就会像一般的自动变量一样被废弃,Block中的__block变量也一同被废弃。
那么为了解决这个超出变量作用域就被废弃的问题,就需要将Block复制到堆中,以延长其生命周期。
这样一来,当Block变量作用域结束后,栈上的Block以及__block变量一同被废弃,而复制到堆上的Block以及__block变量在变量作用域结束时则不受影响。
目前为止呢,我们看到的Block的例子使用的都是_NSConcreateStackBlock
类,且都设置在栈上,但其实呢,在记述全局变量的地方使用Block语法时,生成的Block为_NSConcreateGlobalBlock
类对象。
示例:
上图中Block用结构体实例的内容不依赖于执行时的状态,所以在整个程序中只需要一个实例,因此将Block用结构体实例设置在与全局变量相同的数据区即可。
只有在捕获自动变量时,Block用结构体实例捕获的值才会根据执行时的状态变化。
示例一:
typedef int (^testBlk)(int);
- (void)func{
for (int i = 0; i < 5; i++) {
testBlk blk = ^(int count) {
return i + count;
};
NSLog(@"testBlk = %d", blk(10));
}
}
输出结果:
2019-04-15 18:56:13.330243+0800 ReviewIOS[6619:1164649] testBlk = 10
2019-04-15 18:56:13.330415+0800 ReviewIOS[6619:1164649] testBlk = 11
2019-04-15 18:56:13.330508+0800 ReviewIOS[6619:1164649] testBlk = 12
2019-04-15 18:56:13.330796+0800 ReviewIOS[6619:1164649] testBlk = 13
2019-04-15 18:56:13.330994+0800 ReviewIOS[6619:1164649] testBlk = 14
示例二:
typedef int (^testBlk)(int);
- (void)func{
for (int i = 0; i < 5; i++) {
testBlk blk = ^(int count) {
return count;
};
NSLog(@"testBlk = %d", blk(10));
}
}
2019-04-15 18:58:01.739097+0800 ReviewIOS[6778:1166291] testBlk = 10
2019-04-15 18:58:01.739235+0800 ReviewIOS[6778:1166291] testBlk = 10
2019-04-15 18:58:01.739317+0800 ReviewIOS[6778:1166291] testBlk = 10
2019-04-15 18:58:01.739389+0800 ReviewIOS[6778:1166291] testBlk = 10
2019-04-15 18:58:01.739460+0800 ReviewIOS[6778:1166291] testBlk = 10
如上两个示例,示例一种,捕获了自动变量i
,所以其每次执行的结果是不一样的,而在示例二中,没有捕获自动变量,其每次捕获的值则完全相同。
-
上面提到过
__forwarding
无论是在Block中还是Block外访问__block变量,也不论该变量是在堆上还是栈上,都能够访问到同一个__block变量。
b、循环引用
举个例子:
typedef void (^TestBlk)(void);
@interface ViewController ()
@property (nonatomic, strong) TestBlk blk;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.blk = ^{
[self func];
};
}
- (void)func{
NSLog(@"测试block");
}
@end
上述代码中,ViewController
将block作为自己的属性,然后在block表达式中又使用了该类本身,这样就会造成循环引用。(系统也会给出警告信息Capturing 'self' strongly in this block is likely to lead to a retain cycle
)
解决方法如下:
- ARC下:使用
__weak
__weak typeof(self) weakSelf = self;
self.blk = ^{
[weakSelf func];
};
- MRC下:使用
__block
__block typeof(self) blockSelf = self;
self.blk = ^{
[blockSelf func];
}
参考书籍:Objective-C高级编程(iOS与OS X)多线程和内存管理