Block是C, Objective-C和C++引入的一种语言特性, 它允许你定义一块单独的代码, 这块代码可以被当做方法和函数的参数来传递, 就好像它们是一个数值一样. Block是一个Objective-C对象, 这意味着它们可以被加入到集合中, 比如NSArray和NSDictionary. 它们还能从周围的环境中捕获数值, 类似于其他编程语言中的closures或者lambdas.
这篇文章主要讲解声明和引用Block的语法, 以及如何使用Block来完成一些简单的任务, 比如遍历集合.
Block语法
用^符号来定义一个block字面值:
^{NSLog(@"This is a block.");}
与方法和函数类似, 大括号标志着block的开始和结束. 在上面的例子中, block没有返回值, 也没有参数.
就像用函数指针来引用一个C函数一样, 你也可以声明一个变量来引用一个block:
void (^simpleBlock)(void);
如果你没有接触过C语言的函数指针, 上面的语法看起来可能会有些陌生. 上面的例子声明了一个叫做simpleBlock的变量来引用一个block, 这个block没有参数, 并且也没有返回值, 这就意味着可以用它来引用上面的block字面值:
simpleBlock = ^{
NSLog(@"This is a block.");
};
这就和其他的赋值语句一样, 所以在语句末尾必须加分号. 也可以把变量声明和赋值结合起来:
void (^simpleBlock)(void) = ^{
NSLog(@"This is a block.");
};
一旦声明了一个block变量, 并且进行了赋值, 你就可以用它来调用block了:
simpleBlock(); // 输出This is a block.
注意, 如果你试图调用一个未赋值的block变量(block为nil), 你的APP就会崩溃.
Block可以有参数和返回值
就像方法和函数一样, block也可以有参数和返回值. 举个例子, 考虑一个block变量, 它引用的block用来返回两个数相乘的结果:
double (^multiplyTwoValues)(double, double);
对应的block字面值可能看起来像下面这样:
^ (double firstValue, double secondValue) {
return firstValue * secondValue;
}
在调用block时, firstValue和secondValue用来引用所传入的参数, 就像函数一样. 在例子中, 返回值可以从block内部的返回语句中推断出来. 如果你喜欢, 也可以明确地指定返回值类型, 在^符号和参数列表之间:
^ double (double firstValue, double secondValue) {
return firstValue * secondValue;
};
一旦声明和定义了block, 就可以像函数一样来调用它了:
double (^multiplyTwoValues)(double, double) = ^ (double firstValue, double secondValue) {
return firstValue * secondValue;
};
double result = multiplyTwoValues(2.34, 1.5);
NSLog(@"The result is %f.", result);
Block可以从周围的环境中捕获数值
不仅能包含可执行的代码块, block也可以从它周围的环境中捕获变量的状态. 如果你在一个方法内部定义了一个block, 这个block就可以捕获所有它可以访问到的数值, 像这样:
- (void)testMethod {
int anInt = 42;
void (^testBlock)(void) = ^{
NSLog(@"Int is %d.", anInt);
};
testBlock();
}
在例子中, anInt是在block外面定义的, 但是在定义block时就可以捕获到它的值.
需要注意的是, block捕获到的仅仅是变量当时的值(或状态), 这意味着如果你在定义block之后又修改了变量的值, 那么block内部捕获到的值并不会受到影响:
- (void)testMethod {
int anInt = 42;
void (^testBlock)(void) = ^{
NSLog(@"Int is %d.", anInt);
};
anInt = 84; // 改变anInt的值
testBlock(); // 仍然输出Int is 42.
}
同时, 在block内部也不能修改它所捕获到的变量(变量其实是作为常量被捕获的), 如下面的例子:
- (void)testMethod {
int anInt = 42;
void (^testBlock)(void) = ^{
anInt = 84; // 此行报错
NSLog(@"Int is %d.", anInt);
};
testBlock();
}
__block关键字
如果你确实需要在block内部修改一个在外部定义的变量的值, 那么可以在定义变量时加上__block关键字. 这意味着变量所在的存储区域可以被它所在的作用域内的所有block共享.
我们重写之前的例子:
- (void)testMethod {
__block int anInt = 42;
void (^testBlock)(void) = ^{
NSLog(@"Int is %d.", anInt);
};
anInt = 84;
testBlock(); // 输出Int is 84.
}
这也意味着在block内部可以修改变量的值:
- (void)testMethod {
__block int anInt = 42;
void (^testBlock)(void) = ^{
NSLog(@"Int is %d.", anInt);
anInt = 84;
};
testBlock();
NSLog(@"Int is now %d.", anInt);
}
输出结果:
2017-07-03 14:18:00.412 Blog[1456:126163] Int is 42.
2017-07-03 14:18:00.412 Blog[1456:126163] Int is now 84.
Block作为方法或函数的参数
在之前的例子中, 我们都是在block定义之后就立即调用它. 而在实际的开发中, block经常被作为方法或函数的参数在别处被调用. 我们可能会用GCD在后台调用一个block, 或者定义一个block来描述一个需要多次重复执行的任务, 比如遍历集合的时候. 并发和遍历在文章后面也会简单提及.
Block也经常被用于回调, 在一个任务结束的时候执行一段特定的代码. 举个例子, 我们的APP需要进行一项复杂的任务来响应用户的动作, 比如从网络请求数据. 因为这个任务可能花费很长的时间, 我们就需要在任务进行时给用户展示一个进度指示器, 而在任务结束时把它隐藏起来.
可以试着用代理来实现这个需求: 我们需要创建一个协议, 实现协议方法, 指定代理对象, 并且在任务完成时让代理对象调用协议方法. 这看起来相当繁琐, 而使用Block则会简单得多, 我们可以在创建任务的同时就创建任务完成时的回调:
- (IBAction)fetchRemoteInformation:(id)sender {
[self showProgressIndicator];
XYZWebTask *task = ...
[task beginTaskWithCallbackBlock:^{
[self hideProgressIndicator];
}];
}
这个例子调用了一个方法来展示进度指示器, 然后创建了一个任务并且开始执行, 而block回调则包含了任务完成时需要执行的代码(比如隐藏进度指示器). 注意, 为了调用hideProgressIndicator方法, block捕获了self. 在捕获self时一定要千万小心, 因为很容易形成强引用循环, 文章稍后会讲到.
从代码的可读性来考虑, block使我们更容易的看到在任务开始前和结束后将会发生什么, 从而避免了去其他地方查找代理方法的麻烦.
beginTaskWithCallbackBlock: 方法的声明看起来应该像下面这样:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;
(void (^)(void))说明了参数是一个block, 它没有参数, 也没有返回值. 在方法的实现中可以像平常一样来调用这个block:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
// ...
callbackBlock();
}
对于有参数的block, 写法应该像下面这样:
- (void)doSomethingWithBlock:(void (^)(double, double))block {
// ...
block(1.5, 2.5);
}
Block永远应该作为方法的最后一个参数
最好的实践是, 在方法中最多只有一个block参数. 如果方法中还有其他非block的参数, 那么block参数应该写在最后面:
- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;
这可以使得方法调用更容易阅读:
[self beginTaskWithName:@"MyTask" completion:^{
NSLog(@"The task is complete");
}];
使用类型定义来简化Block语法
如果你需要定义具有相同签名的多个block, 那么可以对这个签名进行类型定义:
举个例子, 可以定义一个简单的block类型, 没有参数也没有返回值:
typedef void(^SimpleBlock)(void);
此时就可以用自定义的类型来创建block变量或block参数:
SimpleBlock anotherBlock = ^{
// ...
};
- (void)beginTaskWithCallbackBlock:(SimpleBlock)callbackBlock {
// ...
callbackBlock();
}
Block作为对象的属性
定义一个block属性的语法和定义block变量的语法类似:
@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end
可以像其他block变量一样来设置和调用一个block属性:
self.blockProperty = ^{
// ...
};
self.blockProperty();
当然, 可以使用类型定义来简化代码:
typedef void (^XYZSimpleBlock)(void);
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end
捕获self时避免强引用循环
如果你需要在block中捕获self, 比如在定义一个block回调时, 那么考虑内存管理问题是非常重要的.
block会对它捕获到的对象维持一个强引用, 包括self, 这就意味着很容易发生强引用循环, 比如, 一个对象持有一个block属性, 而在block内部又捕获了self:
@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
self.block = ^{
[self doSomething]; // 捕获self的强引用, 形成强引用循环
};
}
...
@end
对于上面的简单情况, 编译器会给出警告, 但是更复杂的情况就会大大增加诊断的难度.
为了避免这个问题, 最好的方式是捕获self的弱引用:
- (void)configureBlock {
XYZBlockKeeper * __weak weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // 捕获弱引用来避免强引用循环
}
}
此时, block不会持有对self的强引用. 如果对象在调用block之前被销毁了, 那么weakSelf也会被自动设置为nil, 不会发生危险.
Block能简化遍历
许多Cocoa和Cocoa Touch API使用block来简化任务, 比如遍历集合. NSArray类就提供了一些基于block的方法, 包括:
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
这个方法只有一个block参数, 对于数组中的每个元素都将调用一次:
NSArray *array = ...
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"Object at index %lu is %@", idx, obj);
}];
block本身有3个参数, 前两个代表了当前的对象以及它在数组中的索引, 第三个参数是个指向布尔值的指针, 你可以用它来结束遍历:
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
if (...) {
*stop = YES;
}
}];
NSDictionary类也提供了基于block的方法, 比如:
NSDictionary *dictionary = ...
[dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) {
NSLog(@"key: %@, value: %@", key, obj);
}];
比起传统的循环语句, 这将更容易地遍历每个键值对.
在操作队列中使用block
操作队列是Cocoa 和 Cocoa Touch的任务调度方式. 你可以创建一个NSOperation的实例, 它包含了所需执行的代码块以及所必须的数据, 然后把它加入到一个NSOperationQueue中来执行.
你可以自己创建NSOperation的子类来实现复杂的任务, 但更多时候可以使用NSBlockOperation来以block形式创建一个任务, 像这样:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
// ...
}];
一般地, 任务都会被添加到一个已存在的队列, 或者你自己创建的队列中来执行:
// 在主队列执行任务
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
// 在后台队列执行任务
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
如果使用了操作队列, 那么就可以指定任务的优先级或者依赖关系, 比如, 指定一个任务只有在另一些任务执行完的时候才能开始执行. 也可以使用KVO来监测任务的状态变化, 这就方便我们去更新进度指示器, 比如在任务完成的时候把它隐藏起来.
在派发队列中使用block
派发队列是由GCD控制的, 使用它可以让同步或者异步执行任务变得容易. 你可以创建自己的派发队列, 也可以使用GCD自动提供的队列. 如果你需要并发执行一个任务, 那么可以使用dispatch_get_global_queue()函数来得到一个队列的引用, 并且可以指定它的优先级:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
为了把block加入到队列中执行, 我们可以使用dispatch_async() 或者 dispatch_sync()函数. dispatch_async()函数会立即返回, 不会等待block被调用:
dispatch_async(queue, ^{
NSLog(@"Block for asynchronous execution");
});
而dispatch_sync()函数则会等到block执行完毕才返回:
dispatch_sync(queue, ^{
NSLog(@"Block for synchronous execution");
});
如果一个block需要等待另一个任务完成之后才能继续执行, 就可以使用dispatch_sync()函数.