在编程领域里,一个牛逼程序员和一个二逼程序员之间的区别主要是其对所用编程语言优秀特性的运用方式。要说到Objective-C语言时,那么一般开发者和大牛的区别可能就是对Block
书写代码的运用能力了。
Block编程并不是Objective-C语言独创的一个编程方式,Block也同时也以其他的命名方式存在于其他的编程语言中,例如在Javascript中闭包;Block首次于iOS 4.0版本中引入,其后便被广泛地接受和运用。在随后的iOS版本中,为了适用Block,Apple重写了很多的framework方法。似乎Block在一定程度上已经成为了未来的一种编程方式。但是Block到底是什么呢?
Block是什么
Block是一种添加到C、Objective-C和C++语言中的一个语言层面的特性,它允许您创建不同的代码段,并像值一样的传递到方法或函数中。Block是一个Objective-C对象,这就意味着其可以被保存在NSArray或者NSDictionary中,Block还能够在自己的封闭作用域中截获到值(即所谓的变量截获),Block其实和其他编程语言中的closure(闭包)或lambda是很类似的。
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");
};
这和任何其他变量赋值一样,所以语法上必须以大括号后面的分号作为结束。 您也可以将Block变量的声明和赋值组合起来:
void (^simpleBlock)(void) = ^{
NSLog(@"This is a block");
};
一旦Block被声明且赋值后,您就可以调用Block了,调用方法如下:
simpleBlock();
注意:如果你试图调用一个没有被赋值过的Block变量,你的应用会崩溃的。
Block的参数和返回值
像方法和函数一样,Block即接受参数也有返回值;例如,一个返回两个值乘积的Block变量:
double (^multiplyTwoValues)(double, double);
对应于上面的Block变量,其相应的Block应该是这样的:
^ (double firstValue, double secondValue) {
return firstValue * secondValue;
}
firstValue和secondValue用于引用在调用Block时提供的值,就像任何函数定义一样。 在此示例中,返回类型是从Block内的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,它可以截获该方法作用域内可访问的任何变量的值,如下所示:
- (void)testMethod {
int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
testBlock();
}
在此示例中,anInteger是在Block之外声明的一个变量,但是Block却在定义时截获了变量的值。
int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock();
Block截获的变量的值没有改变。 这意味着日志的输出将显示为:
Integer is: 42
这也意味着Block不能改变原始变量的值,甚至是截获变量值(被截获的变量变成了一个常量)。
__block修饰的变量
当一个Block被复制后(当Block截获到外部变量时,Block就会被复制到堆上),__block
声明的栈变量的引用也会被复制到了堆里,复制完成之后,无论是栈上的Block还是刚刚产生在堆上的Block(栈上Block的副本)都会引用该变量在堆上的副本。
你可以像下面这样重写当前的例子:
__block int anInteger = 42;
void (^testBlock)(void) = ^{
NSLog(@"Integer is: %i", anInteger);
};
anInteger = 84;
testBlock();
因为变量anInteger被声明为一个__block变量,它的内存地址与声明中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在后台调用Block,或者定义一个要重复调用任务的Block,例如枚举集合时。 并发和枚举将在后面讨论。
Block也用于回调,即定义任务完成时要执行的代码。 例如,您的应用程序可能需要通过创建执行复杂任务的对象(例如从Web服务请求信息)来响应用户操作。 因为任务可能需要很长时间,您应该在任务发生时显示某种进度指示器(菊花),然后在任务完成后隐藏该指示器(菊花)。
当然,你可以使用委托来完成这个任务:你需要创建一个合适的委托协议,实现所需的方法,将你的对象设置为任务的委托,然后等待,一旦任务完成时它在你的对象上调用一个委托方法。
然而,Block可以让这些更加容易,因为您可以在启动任务时定义回调行为,如下所示:
- (IBAction)fetchRemoteInformation:(id)sender {
[self showProgressIndicator];
XYZWebTask *task = ...
[task beginTaskWithCallbackBlock:^{
[self hideProgressIndicator];
}];
}
此示例调用一个方法来显示进度指示器(菊花),然后创建任务并指示它开始。 回调Block指定任务完成后要执行的代码; 在这种情况下,它只是调用一个方法来隐藏进度指示器(菊花)。 注意,这个回调block截获了self
,以便能够在调用时调用hideProgressIndicator
方法。 在截获self
时要小心,因为它很容易创建一个strong
类型的循环引用,详情见后面的如何在block截获了self后避免循环引用。
在代码可读性方面,该Block使得在一个位置上很容易看到在任务完成之前和完成之后会发生哪些情况,从而避免需要通过委托方法来查找将要发生的事情。
此示例中显示的beginTaskWithCallbackBlock:
方法的声明如下所示:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock;
(void(^)(void))
上一个没有参数没有返回值的Block。 该方法的实现可以以通常的方式调用Block:
- (void)beginTaskWithCallbackBlock:(void (^)(void))callbackBlock {
...
callbackBlock();
}
Block作为方法的参数,其所拥有的多个或一个参数在形式上应与单纯的Block变量相同:
- (void)doSomethingWithBlock:(void (^)(double, double))block {
...
block(21.0, 2.0);
}
Block应该始终作为方法的最后一个参数
如果方法中含有Block以及其他非Block的参数, 那么Block参数应该始终作为方法的最后一个参数写出,如:
- (void)beginTaskWithName:(NSString *)name completion:(void(^)(void))callback;
这使得在指定Block内联时更容易读取方法的调用,如下所示:
[self beginTaskWithName:@"MyTask" completion:^{
NSLog(@"The task is complete");
}];
使用类型定义来简化Block语法
如果需要使用相同的Block类型来定义多个Block,您可能需要为该类型进行重新的定义。
例如,您可以为没有参数没有返回值的简单Block定义类型(即为Block类型取一个别名):
typedef void (^XYZSimpleBlock)(void);
然后,可以使用自定义类型的Block作为方法的参数或用自定义类型来创建Block变量:
XYZSimpleBlock anotherBlock = ^{
...
};
- (void)beginFetchWithCallbackBlock:(XYZSimpleBlock)callbackBlock {
...
callbackBlock();
}
自定义类型定义在处理作为返回值的Block或将其他Block用作参数的Block时特别有用。 请看以下示例:
void (^(^complexBlock)(void (^)(void)))(void) = ^ (void (^aBlock)(void)) {
...
return ^{
...
};
};
complexBlock变量指的是将另一个Block作为参数(aBlock)并返回另一个Block的Block。
使用类型定义来重写上面的代码,这使的这段代码更加可读:
XYZSimpleBlock (^betterBlock)(XYZSimpleBlock) = ^ (XYZSimpleBlock aBlock) {
...
return ^{
...
};
};
对象使用Block作为属性
定义一个Block属性的语法类似于声明一个Block变量:
@interface XYZObject : NSObject
@property (copy) void (^blockProperty)(void);
@end
注意:您应该将copy
指定为属性修饰符,变量被Block截获后,会改变自身在内存的位置,由栈区变为堆区,所以Block也需要将自己复制到堆区,以应对这种改变。 当使用自动引用计数时,你是不需要担心的,因为它会自动发生的,但是属性修饰符的最佳做法是显示结果行为。 有关更多信息,请参阅Blocks Programming Topics。
Block属性的设置及调用和其他的Block变量是一样的:
self.blockProperty = ^{
...
};
self.blockProperty();
同时也可以使用类型定义的方式声明一个Block属性,如下:
typedef void (^XYZSimpleBlock)(void);
@interface XYZObject : NSObject
@property (copy) XYZSimpleBlock blockProperty;
@end
<a id="no1"></a>如何在Block截获了self后避免循环引用
如果在定义一个Block回调时,需要在Block中截获self
,内存管理的问题是需要引起重视的。
Block对任何截获的对象都是强引用,包括self
;记住这一点后,想要解开循环引用就不是很难了,如下,一个拥有Block属性的对象,在Block内截获了self
:
@interface XYZBlockKeeper : NSObject
@property (copy) void (^block)(void);
@end
@implementation XYZBlockKeeper
- (void)configureBlock {
self.block = ^{
[self doSomething]; // Block对self是强引用的
// 这就产生了循环引用
};
}
...
@end
像上面这样的一个简单例子中,编译器是会在你编写代码时报警告的;但是对于有多个强应用对象在一起产生的循环引用问题,编译器是很难发现循环引用问题的:
为了避免出现这种问题,最好的方式是截获一个弱引用的self
,如下所示:
- (void)configureBlock {
XYZBlockKeeper * __weak weakSelf = self;
//或__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // 截获一个弱引用self
// 以此来避免循环引用
}
}
通过在Block内截获了一个弱指针指向的self
,这样Block就不会再维持对XYZBlockKeeper对象的强引用关系了。如果对象在Block被调用之前释放了,指针weakSelf
就会被置为空;
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需要三个参数,前两个参数指向当前对象及其在数组中的索引。 第三个参数是一个指向布尔变量的指针,可以用来停止枚举,如下所示:
[array enumerateObjectsUsingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
if (...) {
*stop = YES;
}
}];
还可以使用enumerateObjectsWithOptions:usingBlock:
方法自定义枚举。 例如,指定NSEnumerationReverse
这一选项将会反向遍历集合。
如果枚举Block中的代码是处理器密集型(processor-intensive)并且是安全的并发执行 -- 您可以使用NSEnumerationConcurrent
选项:
[array enumerateObjectsWithOptions:NSEnumerationConcurrent
usingBlock:^ (id obj, NSUInteger idx, BOOL *stop) {
...
}];
这个flag指示Block枚举的调用可能会是多线程分布的,如果Block代码是专门针对处理器密集型的,那么这样做对性能会有潜在的提升。注意,当使用这个选项时,这个枚举的顺序是未定义的。
NSDictionary同时也提供一些基于Block的方法,如下所示:
NSDictionary *dictionary = ...
[dictionary enumerateKeysAndObjectsUsingBlock:^ (id key, id obj, BOOL *stop) {
NSLog(@"key: %@, value: %@", key, obj);
}];
如上面的例子所示:相比使用传统的循环遍历,使用枚举键值对的方式会更加方便,
Block可以用来简化并发任务
每个Block代表一个不同的工作单元,就是可执行代码与Block周围作用域中截获的可选状态组合。 这使的Block成为OS X和iOS中理想的异步并发调用可选项之一。 且无需弄清楚如何使用线程等低级机制,您可以使用Block定义任务,然后让系统在处理器资源可用时执行这些任务。
OS X和iOS提供了多种并发技术,包括两种任务调度机制:Operation queues和GCD。 这些机制围绕着一个等待被调用的任务队列而设。 您按照需要调用它们的顺序将Block添加到这一队列中,当处理器时间和资源可用时,系统将对这一队列中的Block进行调用。
串行队列只允许一次执行一个任务 -- 队列中的下一个任务直到前一个任务完成才会被调用,在此期间这一任务将不会离开队列。 并发队列会调用尽可能多的任务,而不必等待前面的任务完成。
使用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的方式监听操作状态的改变,然后在任务完成时,更新进度指示器(菊花):
更多关于操作和队列操作的信息,见Operation Queues
使用GCD在调度队列中给Block进行进度安排。
如果需要安排任意Block代码执行的话,您可以直接使用由Grand Central Dispatch(GCD)控制的调度队列(dispatch queues)。 调度队列使得相对于调用者同步或异步地执行任务变得容易,并且以先进先出的顺序执行它们的任务。
您可以创建自己的调度队列(dispatch queue)或使用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");
});
更多关于队列调度和GCD的问题见Dispatch Queues.