iOS开发-代码块的使用(二)

翻译自 http://pragmaticstudio.com/blog/2010/7/28/ios4-blocks-2
译者水平有限,建议阅读原帖

    在本系列的第一部分,我们学会了如何声明和调用基本的Objective-C代码块。动机是为了了解如何有效的使用iOS4提供的使用代码块作为参数的API。在这一部分我们将重点转向写我们自己的使用代码块的方法。通过理解在自己的代码中如何使用代码块,你将会掌握一种新的设计技术。而且你可能会意识到,代码块会使你的代码易于阅读和维护。
编写使用代码块的方法
    在第一部分我们留下了一个任务:写一个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或对电影进行评价等。
接下来做什么呢
这是这个系列的结束,谢谢您的阅读。关于代码块的知识还有很多,当然我们所学的已经够平常的使用了。如果你想对其做深入研究,我推荐以下资源:
A Short Practical Guide to Blocks by Apple
Blocks Programming Topics by Apple
Block Basics by Bill Bumgarner
Blocks Tips & Tricks by Bill Bumgarner
Friday Q&A: Blocks by Mike Ash
How blocks are implemented (and the consequences) by Matt Gallagher
Language Specification for Blocks
WWDC Session 206 - Introducing Blocks and Grand Central Dispatch on iPhone
    作为最后的一些提示,我希望你已经明白代码块是怎么提供了一种不同编码风格的,并且在你的程序设计中使用它。尝试着使用代码块,期待你的评论。
祝你愉快!
(Thanks to Matt Drance (@drance) and Daniel Steinberg (@dimsumthinking) for reviewing drafts of this article.)

猜你喜欢

转载自duohuoteng.iteye.com/blog/1717479