示例代码下载
Blocks概要
Blocks是c语言的扩充功能:带有自动变量(局部变量)值的匿名函数。
语法
完整的语法形式:
^ 返回值类型 (参数列表) {表达式}
省略返回值的语法形式:
^ (参数列表) {表达式}
省略参数的语法形式:
^ 返回值类型 {表达式}
^ int (int a, int b) { return a + b; };
^ (int a, int b) { printf(@"a + b = %i", a + b); };
^ { printf(@"block"); };
block变量完整语法:
返回值类型 (^变量名称) (参数列表)
int (^aBlock) (int, int) = ^ int (int a, int b) { return a + b; };
void (^bBlock) (int a, int b) = ^ (int a, int b) { NSLog(@"a + b = %i", a + b); };
void (^cBlock) (void) = ^ { NSLog(@"block"); };
typedef可申明Block类型变量:
typedef int (^ABlock) (int a, int b);
ABlock aBlock = ^int (int a, int b) {
return a + b;
};
Bolocks的调用和c语言函数相同,别无二致。
截获自动变量值
Block表达式截获所使用的自动变量的值,即保存该自动变量的瞬间值,所以在执行Block语法后,即使改写了Block中使用的自动变量的值也不会影响Block执行时的值——Block-带有自动变量值的匿名函数。
int a = 1;
void (^block) (void) = ^{
NSLog(@"%i", a);
};
a = 2;
block();
打印如下:
2019-03-29 13:54:43.758298+0800 ProfessionalExample[2688:18267238] 1
自动变量截获只能保存执行Block语法瞬间的值,保存后就不能在Block中更改该值,否则产生编译错误。但是使用截获的值是没有问题的。
若想在Block语法表达式中将值Block语法外的自动变量,需要在该自动变量上附加__block说明符。
注意: 截获自动变量值没有对c语言数组的支持,如果使用会产生编译错误,需要使用指针代替可以解决这个问题。
int nums[4] = {1, 2, 3, 4};
int *p = nums;
void (^block) (void) = ^{
NSLog(@"%i", *(p + 1));
};
block();
“截获自动变量值”,意味着执行Block语法时,Block表达式所使用的自动变量值被保存到Block的结构体实例(即Block自身)中。
因为Block不能改写截获的自动变量的值,当编译器检查到有赋值操作时便产生编译错误。这样就无法在Block中保存值了,极为不便。有两种方法解决这个问题:
- 允许使用静态变量、静态全局变量、全局变量的值。
- __block说明符,用来指定Block中要变更值的自动变量。
Blocks实现
Block本质是一个结构体,就相当于基于objc_object结构体的Objective-C类对象的结构体。也就是可以将Block作为Objective-C对象来处理。如下图:
[外链图片转存中…(img-D8GCnuJR-1696748840205)]
__block变量的本质是一个结构体变量,并不在Block结构体中。这样就可以多个Block使用同一个__block变量。如下图:
[外链图片转存中…(img-ACUfeN4x-1696748840206)]
Block与__block变量本质:
名称 | 实质 |
---|---|
Block | Block结构体实例 |
__block变量 | __block变量结构体实例 |
Block的类有3种:
Block类 | 对象存储域 | 说明 |
---|---|---|
_NSConcreteStackBlock | 栈 | 在记述自动变量的地方使用Block语法生成的Block和不截获自动变量的Block |
_NSConcreteGlobalBlock | 程序的数据区域(.data区) | 在记述全局变量的地方使用Block语法生成的Block |
_NSConcreteMallocBlock | 堆 | Blocks提供将Block和__block变量从从栈复制到堆的方法 |
栈上的Block,__block变量也在栈上,当作用域结束,就会被废弃。将配置在栈上的Block复制到堆上,可以在作用域结束时堆上的Block还继续存在。copy方法能将Block从栈复制到堆,并修改Block的isa指针为_NSConcreteMallocBlock。
事实上ARC有效时,大多数情况下ARC会进行恰当的判断,自动生成Block从栈复制到堆上的代码。
向方法或函数的参数中传递Block时,编译器不会判定生成复制到堆上的代码。但是Cocoa框架中方法名含有usingBlock和GCD的API除外。
NSArray *array;
{
int a = 2;
array = [NSArray arrayWithObjects:[^{ NSLog(@"a + a = %i", a + a); } copy], ^{ NSLog(@"a * a = %i", a * a); }, nil];
}
typedef void (^Block) (void);
Block first = [array firstObject];
first();
Block类 | 源对象存储域 | 复制效果 |
---|---|---|
_NSConcreteStackBlock | 栈 | 从栈复制到堆 |
_NSConcreteGlobalBlock | 程序的数据区域(.data区) | 什么也不做 |
_NSConcreteMallocBlock | 堆 | 引用计数增加 |
由此可见,不管Block在何处配置,用copy方法都不会引起任何问题,在不确定时调用copy方法即可。
复制Block时对__block变量的影响:
__block变量存储域 | Block重栈复制到堆 |
---|---|
栈 | 从栈复制到堆并被Block持有 |
堆 | 被Block持有 |
__block变量结构体中有一个指向自身的__forwarding成员变量指针,当__block变量从栈复制到堆时,会将__forwarding成员变量的值替换为复制到堆上的__block变量结构体实例的指针。通过该功能,无论在Block语法中、语法外使用__block变量,还是__block变量配置在栈上、堆上,都可以顺利的访问同一个__block变量。如下图所示:
[外链图片转存中…(img-z31Z2oLQ-1696748840295)]
如下几种情况,Block会复制到堆:
- 调用Block的copy实例方法
- Block作为函数返回值
- Block赋值给__strong修饰符的变量
- 在方法名含有usingBlock的Cocoa框架方法和GCD的API中传递Block
复制到堆上的Block会持有__strong修饰符的自动变量和复制到堆上的__block变量,使其超出作用域而存在。如下示例:
typedef void (^Block) (id);
__unsafe_unretained Block block;
Block block1;
{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:1];
NSMutableArray *array1 = [NSMutableArray arrayWithCapacity:1];
block = ^(id obj) {
[array addObject:obj];
NSLog(@"array: %@, array count: %li", array, array.count);
};
block1 = ^(id obj) {
[array1 addObject:obj];
NSLog(@"array1: %@, array1 count: %li", array1, array1.count);
};
}
block([[NSObject alloc] init]);
block1([[NSObject alloc] init]);
打印结果:
2019-04-02 11:42:30.155444+0800 ProfessionalExample[71676:22254773] array: (null), array count: 0
2019-04-02 11:42:30.155680+0800 ProfessionalExample[71676:22254773] array1: (
"<NSObject: 0x600000d8e0f0>"
), array1 count: 1
- __block变量为附有__strong修饰符的id类型和对象类型的自动变量,当__block变量从栈复制到堆时,持有赋值给__block变量的对象。当堆上的__block变量释放时,释放赋值给__block变量的对象。
- __block变量为附有__weak修饰符的id类型和对象类型的自动变量,__block变量不持有对象。
- __block变量不能为附有__autoreleasing修饰符的id类型和对象类型的自动变量。
- __block变量为附有__unsafe_unretained修饰符的id类型和对象类型的自动变量,与指针相同。
typedef void (^Block) (id);
Block block;
{
NSMutableArray *array = [NSMutableArray arrayWithCapacity:1];
__block NSMutableArray __weak *array1 = array;
block = ^(id obj) {
[array1 addObject:obj];
NSLog(@"array1: %@, array1 count: %li", array1, array1.count);
};
}
block([[NSObject alloc] init]);
打印结果:
2019-04-02 15:20:35.135112+0800 ProfessionalExample[74596:22578767] array1: (null), array1 count: 0
Block使用__strong修饰符的自动变量,当Block从栈复制到堆时该对象被Block持有,容易形成循环引用。
__weak修饰符、__unsafe_unretained修饰符(iOS4中替代__weak修饰符)可以避免循环引用。
另外__block变量也可以避免循环引用,例如如下对象:
@interface BlockObject : NSObject
@property (strong, nonatomic) void (^block)(void);
@end
@implementation BlockObject
- (void)dealloc {
NSLog(@"%@销毁了", self);
}
@end
创建一个BlockObject对象,并设置其block属性的值。在执行block之前,__block变量obj持有BlockObject对象,BlockObject对象持有block,block持有__block变量,形成了循环引用。执行block时,向__block变量obj赋值nil,至此__block变量不再持有object对象,打破循环引用。如下所以:
__block BlockObject *obj = [[BlockObject alloc] init];
obj.block = ^{
NSLog(@"%@", obj);
obj = nil;
};
obj.block();
使用__block变量与__weak修饰符、__unsafe_unretained修饰符(iOS4中替代__weak修饰符)避免循环引用的比较:
- 通过__block变量可控制对象的持有时间
- 在不能使用__weak修饰符、__unsafe_unretained修饰符(iOS4中替代__weak修饰符)的环境下,如此__block变量能够赋值
- 使用__block变量避免循环引用,必须执行Block并手动释放__block变量对对象的引用(如置空或其它值)
ARC无效时使用Blocks注意事项
ARC无效时,一般需要手动用copy方法将Block从栈复制到堆,并手动用release方法来释放。
__block说明符用来避免循环引用。这是由于当Block从栈复制到堆时,若Block使用__block说明符的id类型或对象类型的自动变量,不会被retain;若没有__block说明符,则会被retain。