第三十九条 用handler块降低代码分散程度

2017/9/4 posted in  第六章 块与GCD

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块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。