《编写高质量iOS与OS X代码的52个有效方法》--第六章 第39条
(ps:此乃读书笔记,加深记忆,仅供大家参考)
第39条:用handler块降低代码分散程度
为用户界面编码时,一种常用的范式就是“异步执行任务”(perform task asynchro nously)。这种范式的好处在于:处理用户界面的显示及触摸操作所用的线程,不会因为要执行I/O或网络通信这类耗时的任务而阻塞。这个线程通常称为主线程(main thread)。某些情况下,如果应用程序在一定时间内无响应,那么就会自动终止。“系统监控器”(system watchdog)在发现某个应用程序的主线程已经阻塞了一段时间之后,就会令其终止。
异步方法在执行完任务之后,需要以某种手段通知相关代码。实现此功能有很多办法。常用的技巧是设计一个委托协议(参见第23条),令关注此事的对象遵从该协议。对象成为delegate之后,就可以在相关事件发生时(例如某个异步任务执行完毕时)得到通知了。
如果改用块来写的话,代码会更清晰。块可以令这种API变得更紧致,同时也令开发者调用起来更加方便。
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler: (EOCNetworkFetcherCompletionHandler)handler;
@end
这和使用委托协议很像,不过多了个好处,就是可以在调用start方法时直接以内联形式定义completion handler,以此方式来使用“网络数据获取器”(network fetcher),可以令代码比原先易懂很多。
与使用委托模式的代码相比,用块写出来的代码闲的更为整洁。委托模式有个缺点:如果类要分别使用多个获取器下载不同的数据,那么就得在delegate回调方法里根据传入的获取器参数来切换。
异步执行任务完毕后所需运行的业务逻辑,和启动异步任务所用的代码放在了一起。无须保存获取器,也无需再回调方法里切换,每个completion handler的业务逻辑,都是和相关获取器对象一起来定义的。
NSURL *url = [[NSURL alloc] initWithString:@"http:www.baidu.com"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
_fetchedFooData = data;
}];
这种写法还有其他用途,比如,现在很多基于块的API都使用块来处理错误。可以分别用两个处理器来处理操作失败的情况和操作成功的情况。也可以把处理失败情况所需的代码,与处理正常情况所用的代码,都封装到同一个completion handler块里。
采用两个独立的处理程序:
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void (^EOCNetworkFetcherErrorHandler)(NSError *error);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)completion failureHandler:(EOCNetworkFetcherErrorHandler)failure;
@end
这种API设计风格很好,由于成功和失败的情况要分别处理,所以调用此API的代码也就会按照逻辑,把应对成功和失败情况的代码分开来写,这将令代码更易读懂。
把处理成功情况和失败情况所用的代码全放在一个块里:
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 startWithCompletionHandler:^(NSData *data, NSError *error) {
if (error) {
//Handler failure
} else {
//Handler success
}
}];
这种方法需要在块代码中检测传入的error变量,并且要把所有逻辑代码都放在一处。这种写法的缺点是:由于全部逻辑都写在一起,所以会令块变得比较长,且比较复杂。然而只用一个块的写法也有好处,那就是更为灵活。
把成功情况和失败情况放在同一个块中,还有个优点:调用API的代码可能会在处理成功相应的过程中发现错误。比方说,返回的数据可能太短了。这种情况需要和网络数据获取器所认定的失败情况按同一方式处理。此时,如果采用单一块的写法,那么就能把这种情况和所认定的失败情况统一处理了。
总体来说,笔者建议使用同一块来处理成功与失败的情况,苹果公司似乎也是这样设计其API的。例如,Twitter框架中的TWRequest及MapKit框架中的MKLocalSearch都只是用一个handler块。
有时需要在相关时间点执行回调操作,这种情况也可以使用handler块。比方说,调用网络数据获取器的代码,也许想在每次有下载进度时都得到通知。这可以通过委托模式实现。不过也可以使用本节的handler块,把处理下载进度的handler定义成块类型,并新增一个此类型的属性:
typedef void (^EOCNetworkFetcheProgressHandler)(float progress);
@property (nonatomic, copy) EOCNetworkFetcheProgressHandler progressHandler;
这种写法很好,因为它还是能把所有业务逻辑都放在一起,也就是把创建网络数据获取器和定义progress handler所用的代码写在一处。
基于handler来设计API还有个原因,就是某些代码必须运行在特定的线程上。因此,最好能由调用API的人来决定handler应该运行在那个线程上。NSNotificationCenter就属于这种API,它提供了一个方法,调用者可以经由此方法来注册想要接收的通知,等到相关事件发生时,通知中心就会执行注册好的那个块。调用者可以指定某个块应该安排在哪个执行队列里,然而这不是必需的。若没有指定队列,按默认方式执行。
- (id <NSObject>)addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
要点
- 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明。
- 在有多个实例需要监控时,如果采用委托模式,那么经常需要根据传入的对象来切换,而若改用handler块来实现,则可直接将块与相关对象放在一起。
- 设计API时如果用到了handler块,那么可以增加一个参数,使调用者可通过此参数来决定应该把块安排在哪个队列上执行。