iOS4引入了一个新特性,支持代码块的使用,这将从根本上改变你的编程方式。代码块是对C语言的一个扩展,因此在Objective-C中完全支持。如果你学过Ruby,Python或Lisp编程语言,那么你肯定知道代码块的强大之处。简单的说,你可以通过代码块封装一组代码语句并将其当作一个对象。代码块的使用是一种新的编码风格,可以让你运用自如的使用iOS4中新增API。
我们先来看两个在iOS4中使用代码块的例子(你很有可能已经见过):view animati***** 和enumeration
使用代码块的例子
第一个例子,假设我们创建一个纸牌游戏,需要展现纸牌被派发到玩家面前的动画效果。幸运的是通过UIKit框架可以很容易的实现一个动画效果。但是最终是什么样的动画是由你的程序决定的。你可以在代码块中指定动画的内容然后再将代码块传给animateWithDuration:animati*****:方法,像下面这样:
[UIView animateWithDuration:2.0
animati*****:^ {
self.cardView.alpha = 1.0;
self.cardView.frame = CGRectMake(176.0, 258.0, 72.0, 96.0);
self.cardView.transform = CGAffineTransformMakeRotation(M_PI);
}
];
当这个动画代码块执行时,我们的纸牌会展现三种方式的动画:改变它的alpha值从而淡入显示,改变它的位置到右下角(玩家的位置),以及自转180度(为了使其效果更好)。
第二个代码块的例子是迭代一个纸牌的集合,并打印其名字和在集合里的索引值。
你可以通过使用for循环来达到目的,但是在iOS4中NSArray类有一个使用了代码块的方便方法:enumerateObjectsUsingBlock:。下面是如何使用它:
NSArray *cards = [NSArray arrayWithObjects:@"Jack", @"Queen", @"King", @"Ace", nil];
[cards enumerateObjectsUsingBlock:^(id object, NSUInteger index, BOOL *stop) {
NSLog(@"%@ card at index %d", object, index);
}];
这个代码块使用了三个参数:数组中的一个对象,该对象的索引,以及一个标识迭代是否结束的标志。我们稍候再对其进一步探讨。enumerateObjectsUsingBlock: 这个方法会将集合中的每一个元素传入相应的参数并调用代码块中的方法。
因此在你的mac和iOS程序中使用代码块的优势是:它允许你附加任意的代码到苹果官方提供的方法上。尽管在概念上与代理相似,但是在方法中使用简短的内联代码块往往更加方便,更加优雅。
这是一个好的开始,但重要的是要明白它内部的处理。当我学习新东西的时候,我喜欢先将其分为一个个简单的部分,了解它们如何工作,然后再将它们组装到一块,这样我会对自己写的代码以及快速解决出现的问题充满信心。因此,让我们先回头学习下如何声明和调用简单的代码块。
代码块的基本概念
一个代码块可以简单看作是一组可执行的代码。例如,下面是一个打印当前日期和时间的代码块:
^ {
NSDate *date = [NSDate date];
NSLog(@"The date and time is %@", date);
};
插入符号(^)声明一个代码块的开始,一对大括号{}构成了代码块的体部。你可以认为代码块与一个匿名函数类似。那么,如果是一个匿名的函数,我们该怎么调用这个代码块呢?最常见使用代码块的方式是将其传入方法中供方法回调,就像之前我们已经见到了view animati***** 和enumeration。另一种使用代码块的方式是将其赋予代码块变量,然后可使用该变量来直接调用代码块。以下是如何声明我们的代码块并将它赋予代码块变量now:
void (^now)(void) = ^ {
NSDate *date = [NSDate date];
NSLog(@"The date and time is %@", date);
};
声明一个块变量的语法需要一些时间适应,这才有趣。如果你使用过函数指针,代码块变量与其类似。在上面代码等号右边是我们已经介绍过的代码块。等号左边我们声明了一个代码块变量now。
代码块变量之前有^符号并被小括号包着,代码块变量有类型定义的。因此,上图中的now变量可以应用任何无参,无返回值的代码块。我们之前声明的代码块符合这要求,,所以我们可以放心的把它分配给now变量。
只要有一个代码块变量,并在其作用域范围内,我们就可以像调用函数一样来调用它。下面是如何调用我们的代码块:
now();
你可以在C函数或者Objective-c方法中声明代码块变量,然后在同一作用域内调用它,就像我们前面说明那样。当代码块执行时,它打印当前的日期和时间。目前为止,进展顺利。
代码块是闭包
如果这就是代码块的全部的话,那么他与函数是完全相同的。但事实是代码块不仅仅是一组可执行的代码。代码块能够捕捉到已声明的同一作用域内的变量,同时由于代码块是闭包,在代码块声明时就将使用的变量包含到了代码块范围内。为了说明这一点,让我们改变一下前面的例子,将日期的初始化移到代码块之外。
NSDate *date = [NSDate date];
void (^now)(void) = ^ {
NSLog(@"The date and time is %@", date);
};
now();
当你第一次调用这个代码块的时候,它与我们之前的版本结果完全一致:打印当前的日期和时间。但是当我们改变日期后再调用代码块,那么就会有显著的不同了,
sleep(5);
date = [NSDate date];
now();
尽管我们在调用代码块之前改变了日期,但是当代码块调用时仍然打印的是之前的日期和时间。就像是日期在代码块声明时停顿了一样。为什么会这样呢,当程序执行到代码块的声明时,代码块对同一作用域并且块内用到的变量做一个只读的备份。你可以认为变量在代码块内被冻结了。因此,不论何时当代码块被调用时,立即调用或5秒钟之后,只要在程序退出之前,它都是打印最初的日期和时间。
事实上,上面那个展示代码块是闭包的例子并不十分完善,毕竟,你可以将日期作为一个参数传入到代码块中(下面讲解)。但是当你将代码块在不同方法间传递时闭包的特性就会变得十分有用,因为它里面的变量是保持不变的。
代码块参数
就像函数一样,代码块可以传入参数和返回结果。例如,我们想要一个能够返回指定数的三倍的代码块,下面是实现的代码块:
^(int number) {
return number * 3;
};
为代码块声明一个变量triple,如下:
int (^triple)(int) = ^(int number) {
return number * 3;
};
上面说过,我们需要熟悉等号左边声明代码块变量的语法。现在让我们从左到右分开来说明:
最左边的int是返回值类型,中间是小括号包围插入符号^及代码块变量的名字,最后又一个小括号,包围着参数的类型(上面例子中只有一个int参数)。等号右边的代码块声明必须符合左侧的定义。有一点要说明的是,为了方便,可以不声明代码块的返回类型,编译器会从返回语句中做出判断。
要调用这个代码块,你需要传入一个需要乘3的参数,并接受返回值,像这样:
int result = triple(2);
下面你将知道如何声明并创建一个需要两个int型参数,将它们相乘然后返回结果的代码块:
int (^multiply)(int, int) = ^(int x, int y) {
return x * y;
};
这是如何调用这个代码块:
int result = multiply(2, 3);
声明代码块变量使我们有机会探讨代码块类型以及如何调用。代码块变量类似函数指针,调用代码块与调用函数相似。不同于函数指针的是,代码块实际上是Objective-C对象,这意味着我们可以像对象一样传递它们。
调用代码块的方法
在实际中,代码块经常被作为参数传入方法中供其回调。当把代码块作为一个参数时,相比分配一个代码块变量,更通常的做法是作为内联代码块。例如,我们之前看到的例子:view animati***** 和enumeration。
苹果官方已经增加了一些使用代码块的方法到他们的框架中。你也可以写一些使用代码块的API了。例如,我们要创建一个Worker类的使用代码块的类方法,该方法重复调用代码块指定的次数,并处理代码块每次返回的结果。下面是我们使用内联代码块调用这个方法,代码块负责返回1到10的每个数的三倍。
[Worker repeat:10 withBlock:^(int number) {
return number * 3;
}];
这个方法可以将任何接受一个int型参数并返回一个int型结果的代码块作为参数,如果想得到数字的二倍,只需要改变传入方法的代码块。
编写使用代码块的方法
在第一部分我们留下了一个任务:写一个Work类的调用代码块的类方法,并且重复调用代码块指定的次数,还要处理每次代码块的返回值。如果我们想要得到1到5的三倍的话,那么下面是我们该如何调这个带有内联代码块的方法:
[Worker repeat:5 withBlock:^(int number) {
return number * 3;
}];
我经常这样设计一个类,首先写代码调用一个虚构的方法,这也是在提交之前一种形成API的简单方式,一旦认为这个方法调用正确,我就去实现这个方法。这样,那个方法的名字是repeat:withBlock:,我认为不合适(我知道在第一部分是叫这个名字,但我已经改变注意了)。这个名字容易使人混淆,因为该方法实际上并不是重复做相同的事情。这个方法从1迭代到指定的次数,并处理代码块的返回。所以让我们开始正确的重命名它:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
return number * 3;
}];
我对这个使用两个参数的方法的名字iterateFromOneTo:withBlock:很满意,一个int型参数表示调用代码块的次数和一个要被调用的代码块参数。现在让我们去实现这个方法。
对于初学者,我么该如何声明这个 iterateFromOneTo:withBlock:方法呢?首先我们需要知道所有参数的类型,第一个参数很容易,是个int类型;第二个参数是一个代码块,代码块是有返回类型的。在这个例子中,这个方法可以接受任何有一个int型参数并返回int型结果的代码块作为参数。下面是实际的代码块类型:
int (^)(int)
已经有了方法的名字和它的参数类型,我们就可以声明这个方法了。这是Worker类的类方法,我们在worker.h中声明它:
@interface Worker : NSObject {
}
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;
@end
第一眼看去,代码块参数不容易理解。有个要记住诀窍是:在Objective-C中所有的方法参数有两个部分组成。被括起来的参数类型以及参数的名称。这个例子中,参数的要求是一个是int型和一个是int(^)(int)型的代码块(你可以为参数命名为任意的名字,不一定非得是block)。这个方法的实现是在Worker.m文件文件中,比较简单:
#import "Worker.h"
@implementation Worker
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block {
for (int i = 1; i <= limit; i++) {
int result = block(i);
NSLog(@"iteration %d => %d", i, result);
}
}
@end
方法通过一个循环来每次调用代码块,并打印出代码块的返回结果。记住一旦我们在作用域内有一个代码块变量,那么就可以像函数一样使用它。在这里代码块参数就是一个代码块变量。因此,当执行block(i)时就会调用传入的代码块。当代码块返回结果后会继续往下执行。现在我们可以使用内联代码块的方式调用iterateFromOneTo:withBlock:方法,像这样:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
return number * 3;
}];
我们也可以不使用内联代码块的方式,传入一个代码块变量作为参数:
int (^tripler)(int) = ^(int number) {
return number * 3;
};
[Worker iterateFromOneTo:5 withBlock:tripler];
不论那种方式,我们得到的输出如下:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15
当然我们可以传入进行任何运算的代码块。想要得到数字的平方吗?没问题,只要传入一个不同的代码块:
[Worker iterateFromOneTo:5 withBlock:^(int number) {
return number * number;
}];
现在我们的代码是可以运行的,下面将代码稍微整理下吧。
善于使用Typedef
匆忙的声明代码块的类型容易混乱,即使在这个简单的例子中,函数指正的语法还是有许多不足之处:
+ (void)iterateFromOneTo:(int)limit withBlock:(int (^)(int))block;
试想代码块要使用多个参数,并且有些参数是指针类型,这样的话你几乎需要完全重写你的代码。为了提高可读性和避免在.h和.m中出项重复,我们可以使用typedef修改Worker.h文件:
typedef int (^ComputationBlock)(int);
@interface Worker : NSObject {
}
+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block;
@end
typedef是C语言的一个关键字,其作用可以理解为将一个繁琐的名字起了一个昵称。在这种情况下,我们定义一个代码块变量ComputationBlock,它有一个int型参数和一个int型返回值。然后,我们定义iterateFromOneTo:withBlock:方法时,可以直接使用ComputationBlock作为代码块参数。同样,在Worker.m文件,我们可以通过使用ComputationBlock简化代码:
#import "Worker.h"
@implementation Worker
+ (void)iterateFromOneTo:(int)limit withBlock:(ComputationBlock)block {
for (int i = 1; i <= limit; i++) {
int result = block(i);
NSLog(@"iteration %d => %d", i, result);
}
}
@end
嗯,这样就好多了,代码易于阅读,没有在多个文件重复定义代码块类型。事实上,你可以使用ComputationBlock在你程序的任何地方,只要import “Worker.h”,你会碰到类似的typedef在新的iOS4的API中。例如,ALAssetsLibrary类定义了下面的方法:
- (void)assetForURL:(NSURL *)assetURL
resultBlock:(ALAssetsLibraryAssetForURLResultBlock)resultBlock
failureBlock:(ALAssetsLibraryAccessFailureBlock)failureBlock
这个方法调用两个代码块,一个代码块时找到所需的资源时调用,另一个时没找到时调用。它们 的 typedef如下:
typedef void (^ALAssetsLibraryAssetForURLResultBlock)(ALAsset *asset);
typedef void (^ALAssetsLibraryAccessFailureBlock)(NSError *error);
然后在你的程序中可以使用ALAssetsLibraryAssetForURLResultBlock和ALAssetsLibraryAccessFailureBlock去表示相应的代码块变量。
我建议在写一个使用代码块的公用方法时就用typedef,这样有助于你的代码整洁,并可以让其他开发人员方便使用。
再来看一下闭包
你应该还记得代码块是闭包,我们简要的讲述一下在第一部分提及的闭包。在第一部分闭包的例子并不实用,而且我说闭包在方法间传递时会变得特别有用。现在我们已经知道如何写一个实用代码块的方法,那么就让我们分析下另一个闭包的例子:
int multiplier = 3;
[Worker iterateFromOneTo:5 withBlock:^(int number) {
return number * multiplier;
}];
我们使用之前写的iterateFromOneTo:withBlock:方法,有一点不同的是没有将要得到的倍数硬编码到代码块中,这个倍数被声明在代码块之外,为一个本地变量。该方法执行的结果与之前一致,将1到5之间的数乘3:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 9
iteration 4 => 12
iteration 5 => 15
这个代码的运行是一个说明闭包强大的例子。代码打破了一般的作用域规则。实际上,在iteratefromOneTo:withBlock:方法中调用multiplier变量,可以把它看作是本地变量。
记住,代码块会捕捉周围的状态。当一个代码块声明时它会自动的对其内部用到的变量做一个只读的快照。因为我们的代码块使用了multiplier变量,这个变量的值被代码块保存了一份供之后使用。也就是说,multiplier变量已经成为了代码块状态啊的一部分。当代码块被传入到iterateFromOneTo:withBlock:方法,快的状态也传了进去。
好吧,如果我们想在代码块的内部改变multiplier变量该怎么办?例如,代码块每次被调用时要让multiplier变为上一次计算的结果。你可能会试着在代码块里直接改变multiplier变量,像这样:
int multiplier = 3;
[Worker iterateFromOneTo:5 withBlock:^(int number) {
multiplier = number * multiplier;
return multiplier; // compile error!
}];
这样的话是通不过编译的,编译器会报错“Assignment of read-only variable 'mutilplier'”。这是因为代码块内使用的是变量的副本,它是堆栈里的一个常量。这些变量在代码块中是不可改变的。
如果你想要修改一个在块外面定义,在块内使用的变量时,你需要在变量声明时增加新的前缀_block,像这样:
__block int multiplier = 3;
[Worker iterateFromOneTo:5 withBlock:^(int number) {
multiplier = number * multiplier;
return multiplier;
}];
NSLog(@"multiplier => %d", multiplier);
这样代码可以通过编译,运行结果如下:
iteration 1 => 3
iteration 2 => 6
iteration 3 => 18
iteration 4 => 72
iteration 5 => 360
multiplier => 360
要注意的是代码块运行之后,multiplier变量的值已经变为了360。换句话说,代码块内部修改的不是变量的副本。声明一个被_block修饰的变量是将其引用传入到了代码块内。事实上,被_block修饰的变量是被所有使用它的代码块共享的。这里要强调的一点是:_block不要随便使用。在将一些东西移入内存堆中会存在边际成本,除非你真的确定需要修改变量,否则不要用_block修饰符。
编写返回代码块的方法
有时我们会需要编写一个返回代码块的方法。让我先看一个错误的例子:
+ (ComputationBlock)raisedToPower:(int)y {
ComputationBlock block = ^(int x) {
return (int)pow(x, y);
};
return block; // Don't do this!
}
这种方法简单的创建了一个计算y的x次幂的代码块然后返回它。它使用了我们之前通过typedef使用的ComputationBlock。下面是我们对所返回代码块的期望效果:
ComputationBlock block = [Worker raisedToPower:2];
block(3); // 9
block(4); // 16
block(5); // 25
在上面的例子中,我们使用的得到代码块,传入相应的参数,它应该会返回传入值的平方。但是当我们运行它时,会得到运行时错误”EXC_BAD_ACCESS”。
怎么办?解决这个问题的关键是了解代码块是怎么分配内存的。代码块的生命周期是在栈中开始的,因为在栈中分配内存是比较块的。是栈变量也就意味着它从栈中弹出后就会被销毁。方法返回结果就会发生这样的情况。
回顾我们的raisedToPower:方法,可以看到在方法中创建了代码块并将它返回。这样创建代码块就是已明确代码块的生存周期了,当我们返回代码块变量后,代码块其实在内存中已经被销毁了。解决办法是在返回之前将代码块从栈中移到堆中。这听起来很复杂,但是实际很简单,只需要简单的对代码块进行copy操作,代码块就会移到堆中。下面是修改后的方法,它可以满足我们的预期:
+ (ComputationBlock)raisedToPower:(int)y {
ComputationBlock block = ^(int x) {
return (int)pow(x, y);
};
return [[block copy] autorelease];
}
注意我们使用了copy后就必须跟一个autorelease从而平衡它的引用计数器,避免内存泄露。当然我们也可以在使用代码块之后将其手动释放,不过这就不符合谁创建谁释放的原则了。你不会经常需要对代码块进行copy操作,但是如果是上面所讲的情况你就需要了,这点请留意。
将所学的整合在一起
那么,让我们来把所学的东西整合为一个更实际点的例子。假设我们要设计一个简单的播放电影的类,这个类的使用者希望电影播放完之后能够接受一个用于展现应用特定逻辑的回调。前面已经证明代码块是处理回调很方便的方法。
让我们开始写代码吧,从一个使用这个类的开发人员的角度来写:
MoviePlayer *player =
[[MoviePlayer alloc] initWithCallback:^(NSString *title) {
NSLog(@"Hope you enjoyed %@", title);
}];
[player playMovie:@"Inception"];
可以看出我们需要MoviePlayer类,他有两个方法:initWithCallback:和playMovie:,初始化的时候接受一个代码块,然后将它保存起来,在执行playMovie:方法结束后再调用代码块。这个代码块需要一个参数(电影的名字),返回void类型。我们对回调的代码块类型使用typedef,使用property来保存代码块变量。记住,代码块是对象,你可以像实例变量或属性一样使用它。这里我们将它当作属性使用。下面是MoviePlayer.h:
typedef void (^MoviePlayerCallbackBlock)(NSString *);
@interface MoviePlayer : NSObject {
}
@property (nonatomic, copy) MoviePlayerCallbackBlock callbackBlock;
- (id)initWithCallback:(MoviePlayerCallbackBlock)block;
- (void)playMovie:(NSString *)title;
@end
下面是MoviePlayer.m:
#import "MoviePlayer.h"
@implementation MoviePlayer
@synthesize callbackBlock;
- (id)initWithCallback:(MoviePlayerCallbackBlock)block {
if (self = [super init]) {
self.callbackBlock = block;
}
return self;
}
- (void)playMovie:(NSString *)title {
// play the movie
self.callbackBlock(title);
}
- (void)dealloc {
[callbackBlock release];
[super dealloc];
}
@end
在initWithCallback:方法中将要使用的代码块声明为callbackBlock属性。由于属性被声明为了copy方式,代码块会自动进行copy操作,从而将其移到堆中。当playMovie:方法调用时,我们传入电影的名字作为参数来调用代码块。
现在我们假设一个开发人员要在程序中使用我们的MoviePlayer类来管理一组你打算观看的电影。当你看完一部电影之后,这部电影就会从组中移除。下面是一个简单的实现,使用了闭包:
NSMutableArray *movieQueue =
[NSMutableArray arrayWithObjects:@"Inception",
@"The Book of Eli",
@"Iron Man 2",
nil];
MoviePlayer *player =
[[MoviePlayer alloc] initWithCallback:^(NSString *title) {
[movieQueue removeObject:title];
}];
for (NSString *title in [NSArray arrayWithArray:movieQueue]) {
[player playMovie:title];
};
请注意代码块使用了本地变量movieQueue,它会成为代码块状态的一部分。当代码块被调用,就会从数组movieQueue中移除一个电影,尽管此时数组是在代码块作用域之外的。当所有的电影播放完成之后,movieQueue将会是一个空数组。下面是一些需要提及的重要事情:
1、movieQueue变量是一个数组指针,我们不能修改它的指向。我们修改的是它指向的内容,因此不需要使用_block修饰。
2、为了迭代movieQueue数组,我们需要创建一个它的copy,否则如果我们直接使用movieQueue数组,就会出现在迭代数组的同事还在移除它的元素,这会引起异常。
3、如果不使用代码块,我们可以声明一个协议,写一个代理类,并注册这个代理作为回调。很明显该例子使用内联代码块更方便。
4、在不改变MoviePlayer类的前提下可以给他增加新功能。比如另一个开发者可以在看完一部电影后将其分享到twitter或对电影进行评价等。