简介
Block 是一种在C, Objective-C, 和 C++ 上的语言特性, 允许我们创建独立的代码段. 对于这些代码段, 我们可以把它们像传递值一样在方法之间传递.
本文主要讲述Block的基本语法与如何使用Block, 对于Block 更详细的介绍, 详见 Blocks Programming Topics.
Block 的基本语法
Block的定义
无参无返回值的Block
可以使用^
符号定义一个Block :
^{
NSLog(@"This is a block");
}
我们使用{}
限定Block的范围, 这一点与函数和方法的定义很相似.
类似于使用一个C函数指针, 我们可以声明一个变量来存储Block:
void (^simpleBlock)(void);
如果你不太熟悉C函数指针, 这个语法看起来可能会有点不寻常. 上面的例子中声明了一个叫做simpleBlock
的变量, 它指向一个无参无返回值的Block. 我们可以为Block变量这样赋值:
simpleBlock = ^{
NSLog(@"This is a block");
};
需要注意的是: Block赋值和其它类型的变量赋值一样, 需要以;
结尾.
我们也可以在声明变量的同时为其赋值:
void (^simpleBlock)(void) = ^{
NSLog(@"This is a block");
};
之后我们就可以像这样调用Block了:
simpleBlock();
Note: 如果我们调用了一个没有被赋值的Block变量, App会发生Crash.
带有参数和返回值的Block
和函数和方法类似, Block可以接收参数或者有返回值.
假如我们需要声明一个两个double类型相乘并返回结果的Block:
double (^multiplyTwoValues)(double, double);
对应的Block实现如下:
^ (double firstValue, double secondValue) {
return firstValue * secondValue;
}
在这个例子中, 返回值可以通过return 表达式推断出来. 当然我们也可以像这样显式声明返回值类型:
^ double (double firstValue, double secondValue) {
return firstValue * secondValue;
}
在我们声明并定义了Block之后, 我们可以就像调用函数一样, 调用Block了:
double (^multiplyTwoValues)(double, double) =
^(double firstValue, double secondValue) {
return firstValue * secondValue;
};
double result = multiplyTwoValues(2,4);
NSLog(@"The result is %f", result);
Block捕获上下文信息
Block 不仅仅包含了可执行的代码片段, Block 也有能力从临近作用域捕获上下文信息.
如果我们定义了一个方法内部的Block, Block可以捕获方法内作用域的上下文信息:
- (void)testMethod {
int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
testBlock();
}
在这个例子中, anInteger
在block外部声明, 但是在block定义时, 它的值被block"捕获"了.
这里捕获的仅仅是值, 当我们在block的定义和block的调用之间修改anInteger
的值时:
int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock();
testBlock()
的输出并不会被anInteger
的新值影响, 因为我们只捕获了值, 因此输出如下:
Integer is: 42
这也意味着Block不能改变捕获变量的原值(甚至连捕获的值也不可以改变 - 由于捕获的值是作为const
变量捕获的.).
__block的使用
如果我们需要从block内部改变外部捕获变量的值时, 我们可以在要修改的外部变量声明中使用__block
存储类型修饰符. 使用了__block
修饰符的变量的存储空间, 被它本身的作用域与引用了它的block的作用域所共享:
__block int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock();
由于使用了__block
修饰anInteger
, 因此anInteger
的存储和block的声明共享. 因此输出如下:
Integer is: 84
这也意味着在Block中可以修改原值:
__block int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
anInteger = 100;
};
testBlock();
NSLog(@"Value of original variable is now: %i", anInteger);
此时输出如下:
Integer is: 42
Value of original variable is now: 100
将Block作为函数/方法参数传递
之前的例子中的Block在定义之后就立即被调用了, 但在实际应用中, 会经常将Block在函数或方法中传递, 并在其它地方调用.
比如我们在使用GCD(Grand Central Dispatch)在子线程调用block, 或者在遍历集合时定义一个block重复调用它. 后文会详细描述这两种场景.
Block也可以用作回调(callbacks), 定义一段任务完成时执行的代码. 举例来说: App中可能从Web Service中请求信息, 由于这个请求可能花费较长的时间, 我们需要在请求过程中添加Loading, 在请求结束时隐藏Loading. 这个需求当然可以通过代理来实现: 创建一个合适的代理协议, 实现对应的方法, 设置任务代理对象, 当请求完成时调用代理方法. 如果我们使用Block, 由于我们在发出请求时就可以定义回调行为, 这使得同样的流程变得简单许多:
- (IBAction)fetchRemoteInformation:(id)sender {
[self showProgressIndicator];
XYZWebTask *task = ...
[task beginTaskWithCallbackBlock:^{
[self hideProgressIndicator];
}];
}
注意: 这个回调Block中为了调用hideProgressIndicator
方法而捕获了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(21.0, 2.0);
}
在一个方法中, 最好只包含一个Block参数. 如果该方法中还需要其它非Block参数, 这个Block类型参数应当放置在参数列表的末尾:
- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;
这使得内联Block代码时, 方法的可读性会更好:
[self beginTaskWithName:@"MyTask" completion:^{
NSLog(@"The task is complete");
}];
当我们需要定义多个block, 但是这些block拥有相同的签名(相同的参数列表+返回值类型), 我们可以为这个签名自定义一个类型:
typedef void (^XYZSimpleBlock)(void);
这里定义了一个无参无返回值的Block类型XYZSimpleBlock
.
我们可以使用它来作为方法参数类型或者创建Block变量:
XYZSimpleBlock anotherBlock = ^{
...
};
- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
...
callbackBlock();
}
自定义Block类型在处理返回Block类型的Block或者接收Block作为参数的Block时是很有用的, 比如:
void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
...
return ^{
...
};
};
这里complexBlock
指向一个接收一个block作为参数(aBlock),并返回一个Block的Block. (有点绕 😆, 或许你有疑问, 为什么不是void(^)(void)(^complexBlock)(void(^)(void))
.可以参考StackOverFlow的答案,里面描述了C解释器是如何一步步解释这个语法的). 使用自定义Block类型重写一下这个代码吧:
XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
...
return ^{
...
};
};
可读性立竿见影!
将Block作为对象的属性
如下代码定义了一个对象中的Block属性:
@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end
Note: 应当为Block指定copy
attribute, 因为block需要被拷贝才能够在原作用域外存储它的上下文信息. 当我们使用ARC(Automatic Reference Counting)时, 我们不需要担心这些, 这些是默认使用的, 但是最好在属性attribute中显式表示. 更多信息详见: Blocks Programming Topics.
Block属性被设置和调用和Block变量相似:
self.blockProperty = ^{
...
};
self.blockProperty();
也可以为属性使用类型定义:
typedef void (^XYZSimpleBlock)(void);
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end
避免Block中产生循环引用
当我们需要在Block中捕获self
时, 比如我们定义了一个回调Block, 考虑内存管理是很重要的.
Block对于其捕获的对象都维持了强引用, 包括self
. 这样就很容易产生强引用循环. 比如有一个对象, 它有一个用copy修饰的block属性, 而且这个block捕获了self
:
@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
self.block = ^{
[self doSomething]; // capturing a strong reference to self
// creates a strong reference cycle
};
}
...
@end
对于这种简单的循环引用编译器会给出警告, 但是对于更复杂一些的例子, 定位问题就很困难了.
为了避免这个问题, 最好为捕获的self
添加弱引用:
- (void)configureBlock {
XYZBlockKeeper * __weak weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // capture the weak reference
// to avoid the reference cycle
}
}
这样, Block将不会强引用XYZBlockKeeper
对象, 如果对象在block调用之前被销毁, weakSelf
将被置为nil
.
Block的应用
使用Block简化遍历
除了回调之外, 很多Cocoa和Cocoa Touch API 使用Block简化任务, 比如集合的遍历. NSArray
提供了一些和block相关的方法:
- (void)enumerateObjectsUsingBlock:(void (^)(id obj, NSUInteger idx, BOOL *stop))block;
这个方法接受单个Block参数, 此Block在每次遍历时被调用:
NSArray *array = ...
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
NSLog(@"Object at index %lu is %@", idx, obj);
}];
Block本身接受三个参数, 前两个参数分别代表当前遍历的对象和该对象在数组中的下标, 第三个参数是一个BOOL类型的指针, 可以通过这个指针停止遍历:
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
if (...) {
*stop = YES;
}
}];
我们也可以通过enumerateObjectsWithOptions:usingBlock:
方法自定义遍历方式--比如指定NSEnumerationReverse
,可以数组反向遍历元素.
如果在遍历Block中的代码是计算密集型的并且对于并发执行是安全的, 可以使用NSEnumerationConcurrent
参数选项来在多线程执行block以提升性能:
[array enumerateObjectsWithOptions:NSEnumerationConcurrent
usingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
...
}];
注意: 当使用这个选项时遍历顺序是不确定的.
NSDictionary
类也提供了Block相关的遍历方法:
NSDictionary *dictionary = ...
[dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) {
NSLog(@"key: %@, value: %@", key, obj);
}];
与传统的遍历方法相比, 这种方式更为便捷.
使用Block简化并发任务
Block代表着一份独立的任务 -- 它包含了一段任务代码以及从上下文捕获的状态信息. 这使得它成为OS X/iOS异步编程中的理想工具. 使用Block定义任务并让系统处理这些任务, 从而不必使用线程之类的系统底层机制.
OS X 和 iOS 提供了各种各样的异步技术, 包含两种任务调度机制 -- 操作队列 (Operation Queues) 以及 GCD(Grand Central Dispatch). 这些机制围绕着任务与任务队列来实现. 我们可以将任务Block添加到队列中, 系统将会在处理器时间和资源可用时执行任务.
串行(Serial)队列只允许每次执行一个任务, 前一个任务完成之前, 下个任务不会开始. 并发(Concurrent)队列尽可能同时执行多个任务, 不必等待前面的任务完成.
在Operation Queue 中使用 Block Operation
我们可以通过创建NSOperationQueue
队列, 并向其中添加NSOperation
任务实例来完成任务.
我们可以创建自定义的NSOperation
子类并自行实现复杂的任务功能, 也可以直接使用NSBlockOperation
来创建Block任务:
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
...
}];
虽然我们可以手动执行Operation
对象, 但是更常用的场景是将其添加到OperationQueue
中, 等待其执行:
// schedule task on main queue:
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue];
[mainQueue addOperation:operation];
// schedule task on background queue:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:operation];
在使用Operation Queue时, 我们可以设置操作的优先级以及操作之间的依赖关系, 比如指定某个Operation必须在其它的若干个Operation完成后才可以开始执行. 我们也可以通过KVO监视任务的执行情况. 更多Operation相关, 详见Operation Queues.
在GCD(Grand Central Dispatch)中使用Block
GCD提供了Dispatch Queues 任务队列来管理任务使其同步或异步执行.
我们可以创建新的DispatchQueue或者使用GCD提供的DispatchQueue. 如果我们想要创建一个并发队列, 可以直接使用已有的队列 -- 通过dispatch_get_global_queue()
返回, 并指定它的优先级:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
向队列中分发任务, 可以通过dispatch_async()
或者 dispatch_sync()
函数. dispatch_async()
函数会立即返回, 不会等待Block被触发.
而dispatch_sync()
函数会等到Block执行完成后才会返回.在需要异步执行的Block需要等到其它任务在主线程完成后继续执行时使用.
更多关于GCD的相关, 详见Dispatch Queues.