概要
Blocks时c语言的扩充功能,就是带有自动变量(局部变量)的匿名函数
模式
语法:
^ 返回值类型 参数列表 表达式
例如:^int (int count) {return count + 1;}
可省略返回值类型,Block语法将按照return语句的类型返回int型返回值
^(int count) {return count + 1;}
如果不使用参数,参数列表也可省略
^{printf("Blocks\n");}
Block类型变量
int (^block) (int) = ^(int count) {return count + 1;}
block(10)
进行调用
也可以typedef定义类型:
typedef int (^block_t) (int);
block_t block = ^(int count) {return count + 1;}
block(10);
Block截获自动变量值
也就时block中的带有自动变量。
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{printf(fmt, val);};
fmt = "the value has changed %d";
val = 11;
blk();
在上述代码中,Block语法的表达式使用的是它之前声明的自动变量fmt和val。Block中,Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值。因为Block表达式保存了自动变量的值,所以在执行Block语法后,即使改写了Block中使用的自动变量的值也不会影响Block执行时自动变量的值。
打印出来后结果为val = 10
这就是Block的截获。
Block说明符
我们尝试改写截获的自动变量的值。
int val = 10;
void (^blk)(void) = ^{val = 1};
blk();
NSLog(@"%d", val);
此时会报错,要想在block语法中将值赋给Block语法外的自动变量,需在自动变量前加个__block说明符。
__block int val = 10;
void (^blk)(void) = ^{val = 1;};
blk();
NSLog(@"%d", val);
打印结果:val = 1
截获的自动变量:
NSMutableArray *a = [@[@1, @2, @3] mutableCopy];
void (^blk)(void) = ^{[a addObject:@4];};
blk();
NSLog(@"%@", a);
以上代码可以正确执行,该代码中截获的变量是NSMutableArray类的对象。如果用c语言描述,即是截获NSMutableArray类对象用的结构体实例指针。如果使用截获的值的话没有问题,但是不能对这个变量进行赋值,比如以下代码会报错,要想成功运行的话要给自动变量a前加__block。
NSMutableArray *a = [@[@1, @2, @3] mutableCopy];
void (^blk)(void) = ^{NSArray *f = @[@1]; a = f;};
blk();
NSLog(@"%@", a);
Blocks的实现
Blocks的实质
Blocks的实质是objc的对象,转为c的本质就是一个类的对象结构体实例,结构体具有isa指针和blocks中引用的自动变量,作为结构体的成员变量,isa指针指向存储类信息的class_t结构体(存储着类的属性,成员变量,方法名称,方法实现(即函数指针))。
所以在截获变量时截获的自动变量存到block结构体实例中去了,之后再改变自动变量然后再调用blocks函数并不会使结果发生改变。
而之所以不能改变自动变量的值是因为blocks结构体中仅使用自动变量的值然后赋给结构体实例中的成员变量但是并不能改变它。所以改变时就会报错。
__block int val = 0;
那么__block的实现则比较复杂,它相当于为block声明符声明的变量另创建一个结构体,在结构体中将含有isa,flags,结构体size等信息外,还存有一个自身类型的指针forwarding成员变量指向自身,存有一个声明的val成员变量。
名称 | 实质 |
---|---|
Block | 栈上Block的结构体实例 |
__block | 栈上__block的结构体实例 |
Blocks的存储域
Block结构体存储位置:
- 记述全局变量的地方有Block语法
- Block 语法的表达式中不使用应截获的自动变量
以上两种情况将Block存于数据区,其他情况存于栈区。而何时将Block配置在堆上呢?看如下情况
typedef int (^blk_t)(int);
blk_t func(int rate)
{
return ^(int count){return rate * count;};
}
像以下情况下Block也是作为返回值返给b的,所以也会被复制到堆上并被b所持有。
void(^b)(void) = ^{NSLog(@"hahaha");};
该源代码返回配置在栈上的Block函数。即程序执行中从该函数返回函数调用方时变量作用域结束,因此栈上的Block也被废弃。但实际处理时会:(将Block作为函数值返回时,编译器会自动生成复制到堆上的代码)
blk_t tmp = Block栈上的结构体实例
tmp = _Block_copy(tmp);
// copy函数将栈上的Block复制到堆上。
// 复制后将堆上的地址作为指针赋值给变量tmp;
return objc_autoreleaseReturnValue(tmp);
// 将堆上的Block作为Objective-c对象
// 注册到autoreleasepool中,然后返回该对象
大多数情况编译器会适当进行判断自动生成复制到堆上的代码,但有些情况需要使用copy实例方法手动生成代码。什么情况呢?
- 向函数或者方法的参数中传递Block时。
但是如果在方法或是函数中适当地复制了传递过来的参数,那么就不用在调用该方法或函数前手动复制了。以下方法或函数不用手动复制:
- Cocoa 框架的方法且方法名中含有usingBlock等。例如NSArry类的enumerateObjectUsingBlock实例方法
- GCD的API。比如dispatch_async函数。
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [self getBlockArray];
typedef void (^blk_t) (void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();
}
- (id) getBlockArray
{
int val = 10;
return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0: %d", val);}, ^{NSLog(@"blk1: %d", val);}, nil];
}
以上情况下使用会报错。因为getBlockArray函数执行结束后,栈上的Block会被废弃。可惜此时编译器无法判断是否需要复制,此时我们只能进行手动复制。
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [self getBlockArray];
typedef void (^blk_t) (void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();
}
- (id) getBlockArray
{
int val = 10;
return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0: %d", val);} copy], [^{NSLog(@"blk1: %d", val);} copy], nil];
}
此时按照预想的结果成功运行并打印出结果。
但是如果我们对已经存在于堆上的Block进行copy,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
id obj = [self getBlockArray];
typedef void (^blk_t) (void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk_t c = [blk copy];
NSLog(@"%p", blk);
NSLog(@"%p", c);
}
- (id) getBlockArray
{
int val = 10;
return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0: %d", val);} copy], [^{NSLog(@"blk1: %d", val);} copy], nil];
}
结果是并不会在进行一次深拷贝,只是浅拷贝,引用计数加一。
总结一下何时栈上的Block会复制到堆上:
- 调用Block的copy实例方法。
- Block作为函数返回值返回时。
- 将Block赋值给附有
__strong
修饰符id类型的类或者Block类型的成员变量时(编译器更新后赋值给block类型也会复制到栈上)。 - 方法名中含有usingBlock的Cocoa框架方法或GCD的API传递Block时。
__block变量存储域
使用__block
变量的Block从栈复制到堆上时,__block
变量存储域也会受影响。
__block变量的配置存储域 | Block从栈复制到堆上的影响 |
---|---|
栈 | 从栈复制到堆上并被Block持有 |
堆 | 被Block持有 |
-
若在一个Block中使用
__block
变量,则当该Block从栈复制到堆时,使用的所有__block
变量也必定配置在栈上。这些__block
变量也全被从栈上复制到堆。此时,Block持有__block
变量。即使在该Block已复制到堆的情形下,复制Block也对所使用的__block
变量没有任何影响。 -
在多个Block中使用
__block
变量时,因为最先会将所有的Block配置在栈上,所以__block
变量也会配置在栈上。在任何一个Block从栈复制到堆时,__block
变量也会一并从栈复制到堆并被该Block所持有。当剩下的Block从栈复制到堆上时,被复制的Block持有__block
变量,并增加__block
变量的引用计数。
下面再谈一下__block
变量用结构体成员变量__forwarding
的原因。此时,不管__block
变量配置在堆上还是栈上,都能够正确地访问该变量。
__block int val = 1;
void (^blk) (void) = [^{NSLog(@"val = %d", val); val++;} copy];
val++;
blk();
NSLog(@"val = %d",val);
结果打印出来分别为2,3。
以下两种val++的源代码都可以转为如下形式:
++(val.__forwarding->val);
在Block从栈复制到堆上时,__forwarding
指针原本指向自身,复制完后指向复制在堆上的__block
变量。
所以通过该指针功能,无论在Block语法中、Block语法外使用__block
变量,还是__block
变量配置在栈上或是堆上,都可以顺利访问到同一个__block
变量。
同时也刚好验证了之前说的使用__block
变量的话会使得Block持有该变量的结构体,所以截获的也就不是定义Block前的值了,而是调用Block前的值。
[super viewDidLoad];
int val = 1;
void (^blk) (void) = ^{NSLog(@"val = %d", val);};
val++;
blk();
NSLog(@"val = %d",val);
这个的结构就是1,2。
截获对象
block blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj) {
[array addObject:obj];
NSLog(@"array count = %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
array已经超出其作用域,变量array应该被废弃,其强引用失效,因此赋值给变量array的NSMutableArray类的对象并定应该被释放并废弃。但该源代码运行正常,结果如下:
array count = 1
array count = 2
array count = 3
这就意味着赋值给变量array的NSMutableArray类的对象在Block的执行部分超出其变量作用域而存在
原因是在Block用的结构体中赋有了__strong
修饰符的成员变量array。
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
id __strong array;
};
虽然c语言结构体内不能附有__strong
修饰符的变量。因为编译器不知道何时进行c语言结构体的初始化和废弃操作,不能很好地管理内存。
但是OC运行时库能够准确地把握Block从栈复制到堆以及堆上的Block被废弃的时机,因此Block用结构体中即使含有附有__strong
修饰符或__weak
修饰符的变量,也可以恰当进行初始化和废弃,为此需要增加copy(retain)和dispose(release)函数来给Block中的成员变量array赋值。
避免循环引用:
即对象持有Block,Block又持有对象(self)。
除了__weak
外,可使用__block
修饰符避免循环引用,将__block
修饰的变量指向self,然后在Block中用完self后将__block
变量置为nil,但是必须要执行Block函数,不然仍会引起循环引用。
PS:之前遇到的一个概念混淆,再提一点属性和成员变量的区别:
属性 = ivar(成员变量)+getter+setter(存取方法)