本文档版权归NickTang所有,没有本人书面或电子邮件允许,不许转载,摘录,发表。多谢!
一段时间以来,块代码已经成为Ruby,Python,Lisp等脚本语言和编译语言中的一部分(在这些语言中,可能被命名为“closures”或“lambdas”)。从Mac OS X v10.6和iOS 4.0开始,块代码,一个强大的C语言功能点,已经是Cocoa应用开发的一部分了。虽它的语法初看起来有点奇怪,但是你会发现它是很好用的。
下面的讨论都是大概的描述,如果你希望非常详细,定义性的解释,请参看Blocks Programming Topics。
为何使用块代码?
块代码是一个能工作的代码单元,可以在任何时候被执行。它们本质上是轻量级的,匿名的函数,并且可以作为函数的参数,或者返回值。块代码本身可能有一个参数列表,返回类型,或者没有返回值。你可以把块代码赋给一个变量,并在合适的时候调用它,就行调用一个普通函数一样。
插入符(^)被用来做为块代码的开始标记。例如,下面的代码就声明了一个块块代码,它有两个整形的参数,返回类型也是整形。这里在第一个插入符后面列出参数列表,在一对大括号中包含实现代码,并且把这个快代码赋值给变量Multiply
:
int (^Multiply)(int, int) = ^(int num1, int num2) { |
return num1 * num2; |
}; |
int result = Multiply(7, 4); // result is 28 |
作为函数或者方法的参数的时候,块代码其实就是一个回调类型的函数,可以使用在局限于函数或方法类型的代理上。作为参数传入后,在调用块代码,可以使得函数和方法可以实现个性化运行。当调用这些函数或方法的时候,它们会在合适的时候,执行这些块代码,去取的附加信息,或者执行特定的行为。
使用块代码作为函数或方法的参数的一个好处就是,你可以在调用函数或方法的地方写回调代码。由于这些代码不需要在额外的函数或方法中,所以这样的实现方式简单易懂。使用通知中类NSNotification
作为一个例子。在过去的模式下,一个对象把自己加入到一个通知的观察者对象中,它需要实现一个额外的函数(在调用addObserver:..
函数的时候使用选择器作为参数传入)去处理这个消息:
- (void)viewDidLoad { |
[super viewDidLoad]; |
[[NSNotificationCenter defaultCenter] addObserver:self |
selector:@selector(keyboardWillShow:) |
name:UIKeyboardWillShowNotification object:nil]; |
} |
|
- (void)keyboardWillShow:(NSNotification *)notification { |
// Notification-handling code goes here. |
} |
在新的函数addObserverForName:object:queue:usingBlock:
中,你可以替换通知的回调函数为下面的形式:
- (void)viewDidLoad { |
[super viewDidLoad]; |
[[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillShowNotification |
object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { |
// Notification-handling code goes here. |
}]; |
} |
使用块代码的另一个好处是,块代码可以共享本地作用域内有效的变量。如果你在一个函数中实现了一个块代码,这个块代码可以使用本地变量和函数参数(即栈上的变量),还有在函数中能用包含实例变量等的全局变量。块代码在使用上述变量的时候是只读的,即不能修改这些变量,不过如果你使用__block来修饰变量,那么这个变量对于块代码就是可写的。即使方法或函数已经退出,本地环境已经释放,这些在块代码中使用的变量还存在,只要还有对这个块代码的引用存在。
系统框架API中的块代码
一个显著的使用块代码的动机是在系统类库中的函数越来越多的开始使用这个作为参数,下面是在部分系统函数中使用块代码的情况:
-
结束回调
-
通知处理
-
出错处理
-
枚举
-
视图动画和翻转
-
排序
下面各节就是对上面的讨论。不过在开始讨论之前,我们还需要看一下系统函数中的块代码的声明。考虑下面的这个在类NSSet
中的声明:
- (NSSet *)objectsPassingTest:(BOOL (^)(id obj, BOOL *stop))predicate |
上面的这个声明标示着传入函数的参数是一个动态对象类型和一个布尔类型,返回一个布尔类型的代码块。(传入参数和返回类型都是用在“Enumeration” 中的for循环的类型)。当声明你的块代码的时候,使用一个插入符^开始,并且使用一对括号包起来的参数列表,后面跟着被一对大括号包着的代码:
[mySet objectsPassingTest:^(id obj, BOOL *stop) { |
// Code goes here; end by returning YES or NO. |
}]; |
结束回调和错误管理
结束处理是一个回调函数,用在当一个调用端使用系统函数或方法完成一个任务后的处理。很多时候调用端在结束回调中实现释放状态或者更新界面。很多框架方法函数使用块代码作为结束回调。
UIView
类中有很多个实现动画或视图翻转的函数使用块代码作为结束回调参数。(“View Animation and Transitions”对这些函数做出了描述)代码1-1演示了如何使用animateWithDuration:animations:completion:
函数的例子。这个例子中的动画结束回调,在结束后使得做动画的视图回复原位,并且几秒后把alpha设置为1。
代码1-1 一个结束回调块代码
- (IBAction)animateView:(id)sender { CGRect cacheFrame = self.imageView.frame; [UIView animateWithDuration:1.5 animations:^{ CGRect newFrame = self.imageView.frame; newFrame.origin.y = newFrame.origin.y + 150.0; self.imageView.frame = newFrame; self.imageView.alpha = 0.2; } completion:^ (BOOL finished) { if (finished) { // Revert image view to original. sleep(3); self.imageView.frame = cacheFrame; self.imageView.alpha = 1.0; } }]; } |
一些框架函数具有出错处理,这个和结束处理是一样的。函数会在默写错误发生的时候不能完成任务的情况下调用出错处理(并且传入一个NSError
对象)。你可以自定义这个出错处理来通知用户有错误发生。
通知管理
NSNotificationCenter类中的方法
addObserverForName:object:queue:usingBlock:让你在设置一个通知的观察者的时候就可以实现一个处理代码块。代码
1-2演示了调用这个函数的情况,为某个通知定义一个通知管理的代码块。作为一个通知管理的函数,一个NSNotification
对象被传入。这个函数还使用了一个NSOperationQueue
实例,你的应用可以使用它来传递执行上下文到代码块中。
Listing 1-2 Adding an object as an observer and handling a notification using a block
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { |
opQ = [[NSOperationQueue alloc] init]; |
[[NSNotificationCenter defaultCenter] addObserverForName:@"CustomOperationCompleted" |
object:nil queue:opQ |
usingBlock:^(NSNotification *notif) { |
NSNumber *theNum = [notif.userInfo objectForKey:@"NumberOfItemsProcessed"]; |
NSLog(@"Number of items processed: %i", [theNum intValue]); |
}]; |
} |
枚举
基础类库中的容器类—NSArray
,NSDictionary
,NSSet
,和NSIndexSet
—定义的可以实现枚举的函数,使用块代码作为参数,以便调用者可以个性化枚举动作。换句话说,这些方法提供了一个快速枚举的环境:
for (id item in collection) { |
// Code to operate on each item in turn. |
} |
有两种类型枚举函数使用块代码。第一种是函数名称以enumerate开头并且没有返回值,这些函数使用块代码对没有被枚举的对象进行处理;第二种是以
passingTest
;结束的函数,这样函数返回一个整形或者NSIndexSet对象,这类函数中的代码块对每一个枚举对象进行测试,如果通过测试返回YES。函数返回的整形或者索引表示通过测试的原始位置或所有通过测试的对象的位置。
代码1-3中对三个中没一个都调用NSArray中的方法。第一个方法(一个“passing test”方法)的块代码在数组中的每一个字符串对象如果含有一个固定的前缀就返回YES。后面的代码使用方法返回的索引创建一个临时的数组。第二个块代码把每一个第一个数组中的前缀去掉,把后面的加入到一个新数组中。
代码1-3 使用两个块代码枚举数组
NSString *area = @"Europe"; |
NSArray *timeZoneNames = [NSTimeZone knownTimeZoneNames]; |
NSMutableArray *areaArray = [NSMutableArray arrayWithCapacity:1]; |
NSIndexSet *areaIndexes = [timeZoneNames indexesOfObjectsWithOptions:NSEnumerationConcurrent |
passingTest:^(id obj, NSUInteger idx, BOOL *stop) { |
NSString *tmpStr = (NSString *)obj; |
return [tmpStr hasPrefix:area]; |
}]; |
|
NSArray *tmpArray = [timeZoneNames objectsAtIndexes:areaIndexes]; |
[tmpArray enumerateObjectsWithOptions:NSEnumerationConcurrent|NSEnumerationReverse |
usingBlock:^(id obj, NSUInteger idx, BOOL *stop) { |
[areaArray addObject:[obj substringFromIndex:[area length]+1]]; |
}]; |
NSLog(@"Cities in %@ time zone:%@", area, areaArray); |
上面的每一个枚举函数中的stop参数(这里没有使用)可以用来传入一个YES,在合适的时候停止枚举。你可以使用它在枚举到第一个你需要的项的时候停止。
NSString类,尽管不是一个容器类,也提供两个使用块代码的函数,它们分别是
enumerateSubstringsInRange:options:usingBlock:
和enumerateLinesUsingBlock:。第一个函数枚举一个字符串,使用一个子串进行分割,第二个只是使用换行符进行分割。代码
1-4演示了第一个函数如何使用:
代码1-4 使用块代码在一个字符串中查找匹配的子串
NSString *musician = @"Beatles"; |
NSString *musicDates = [NSString stringWithContentsOfFile: |
@"/usr/share/calendar/calendar.music" |
encoding:NSASCIIStringEncoding error:NULL]; |
[musicDates enumerateSubstringsInRange:NSMakeRange(0, [musicDates length]-1) |
options:NSStringEnumerationByLines |
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { |
NSRange found = [substring rangeOfString:musician]; |
if (found.location != NSNotFound) { |
NSLog(@"%@", substring); |
} |
}]; |
视图动画和转换
iOS4.0中的UIView
类引入几个使用块代码,实现动画和转换的类方法,这些块代码类型的参数有两种(不是所有的函数都使用两个类型):
-
块代码用来改变视图的属性来形成动画
-
动画完成后的管理
代码1-5演示了使用animateWithDuration:animations:completion:
的使用,这个函数使用了这两种块代码参数。在这个例子中,动画使得视图消失(设置alpha值为0)并在动画完成后把视图移除。
代码1-5 视图使用块代码实现简单动画
[UIView animateWithDuration:0.2 animations:^{ |
view.alpha = 0.0; |
} completion:^(BOOL finished){ |
[view removeFromSuperview]; |
}]; |
UIView中其他的一些类方法实现了两个不同视图的转换,包括翻转和盘旋。下面的例子代码就是使用transitionWithView:duration:options:animations:completion:
做一个从左面开始的翻转(代码中没有实现动画完成的处理):
代码1-6 在两个视图间做翻转
[UIView transitionWithView:containerView duration:0.2 |
options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{ |
[fromView removeFromSuperview]; |
[containerView addSubview:toView] |
} |
completion:NULL]; |
排序
基础类库中声明了NSComparator块代码类型来进行两个条目的比较:
typedef NSComparisonResult (^NSComparator)(id obj1, id obj2); |
NSComparator是一个块代码类型,并且使用两个对象作为参数,返回一个
NSComparisonResult类型的值。它是
NSSortDescriptor
,NSArray
, 和NSDictionary等类的方法的参数,用来进行排序,如下所示:
代码1-7 使用NSComparator块代码功能对数组进行排序
NSArray *stringsArray = [NSArray arrayWithObjects: |
@"string 1", |
@"String 21", |
@"string 12", |
@"String 11", |
@"String 02", nil]; |
static NSStringCompareOptions comparisonOptions = NSCaseInsensitiveSearch | NSNumericSearch | |
NSWidthInsensitiveSearch | NSForcedOrderingSearch; |
NSLocale *currentLocale = [NSLocale currentLocale]; |
NSComparator finderSort = ^(id string1, id string2) { |
NSRange string1Range = NSMakeRange(0, [string1 length]); |
return [string1 compare:string2 options:comparisonOptions range:string1Range locale:currentLocale]; |
}; |
NSLog(@"finderSort: %@", [stringsArray sortedArrayUsingComparator:finderSort]); |
上面的代码是从Blocks Programming Topics里面选取的。
块代码和并发
块代码是一个匿名对象,适合于异步调用,由于上面的这些特征,块代码和适合GCD(Grand Central Dispatch (GCD))和NSOperationQueue
,
-
GCD中两个关键函数
dispatch_sync
(同步派发)和dispatch_async
(异步派发)的第二个参数都是一个块代码类型。
-
一个NSOperationQueue是一个用来调度任务的对象,这些任务可能顺序执行,或者有相互依赖关系。这些任务其实是
NSOperation
对象,这些对象一般使用块代码来实现任务。
NSOperationQueue和
NSOperation的更多信息,请参看
Concurrency Programming Guide。