iOS中我们经常采用异步执行任务的方式,来避免主线程的阻塞。因为“系统监控器”(system watchdog)
在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。导致程序崩溃。
但是异步方法执行任务后,需要以某种手段来通知相关代码。实现这一功能有很多方法,常用的技巧是设计一个委托协议,令关注此事件的对象遵从该协议。对象成为delegate
之后,就可以在相关事件发生时(例如某个异步任务执行完毕时)得到通知了。例如:
#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
@protocol EOCNetworkFetcherDelegate <NSObject>
-(void)networkFetcher:(EOCNetworkFetcher*)networkFetcher
didFinishWithData:(NSData*)data;
@end
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak) id <EOCNetworkFetcherDelegate> delegate;
-(id)initWithURL:(NSURL*)url;
-(void)start;
@end
其它类可以像下面这样来使用:
-(void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:
@"http: //www.example.com/foo.dat"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
fetcher.delegate = self;
[fetcher start];
}
-(void)networkFetcher:(EOCNetworkFetcher*)networkFetcher didFinishWithData:(NSData*)data
{
_fetchedFooData = data;
}
上面在EOCNetworkFetcher
类中声明了一个协议,协议中有一个方法用于通知对象已获取完数据。
之后想要获取到通知的对象遵守该协议,成为它的委托对象。这样在执行完start
方法之后,EOCNetworkFetcher
会调用委托对象所遵守的协议方法,让委托对象获取收到的数据(也就是通知它)。
上面这种做法没有错误,确实可行。但是如果我们改用块来写的话,代码会更清晰。就是把completion handler
定义为块类型,将其当作参数直接传给start
方法:
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
-(id)initWithURL:(NSURL*)url;
-(void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)handler;
@end
这和使用委托协议很想,不过多了个好处,就是可以在调用start
方法时直接以内联形式 定义completion handler
,以此方式来使用“网络数据获取器”(network fetcher)
,可以令代码比原先易懂很多。例如,下面这个类就以块的形式来定义completion handler
,并以此为参数调用API
:
-(void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:
@"http://www.example.com/foo.dat"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data){
_fetchedFooData = data;
});
与使用委托模式的代码相比,用块写出来的代码显然更为整洁。异步任务执行完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。而且,由于块声明在创建获取器的范围里,所以它可以访问此范围内的全部变量。
这种写法其实最重要的用途是处理错误。现在很多基于块的API
都使用块来处理错误,可以分别用两个处理程序来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码,与处理正常情况所用的代码,都封装到同一个completion handler
块里,我们建议使用后者,因为苹果公司也是这样设计API
的。我们举例来说:
#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
typedef void(^EOCNetworkFetcherCompletionHandler)
(NSData *data, NSError *error);
@interface EOCNetworkFetcher : NSObject
-(id)initWithURL:(NSURL*)url;
-(void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)completion;
@end
此种API的调用方式如下:
EOCNetworkFetcher *fetcher =
[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHander:
^(NSData *data, NSError *error){
if (error) {
//Handle failure
}else {
// Handle success
}
}];
要点
- 在创建对象时,可以使用内联的
handler
块将相关业务逻辑一并声明。 - 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切 换,而若改用
handler
块来实现,则可直接将块与相关对象放在一起。 - 设计API时如果用到了
handler
块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。