《編寫高質量iOS與OS X代碼的52個有效方法》--第六章 第39條
(ps:此乃讀書筆記,加深記憶,僅供大家參考)
第39條:用handler塊降低代碼分散程度
為用戶界面編碼時,一種常用的范式就是“異步執(zhí)行任務”(perform task asynchro nously)。這種范式的好處在于:處理用戶界面的顯示及觸摸操作所用的線程,不會因為要執(zhí)行I/O或網絡通信這類耗時的任務而阻塞。這個線程通常稱為主線程(main thread)。某些情況下,如果應用程序在一定時間內無響應,那么就會自動終止?!跋到y(tǒng)監(jiān)控器”(system watchdog)在發(fā)現(xiàn)某個應用程序的主線程已經阻塞了一段時間之后,就會令其終止。
異步方法在執(zhí)行完任務之后,需要以某種手段通知相關代碼。實現(xiàn)此功能有很多辦法。常用的技巧是設計一個委托協(xié)議(參見第23條),令關注此事的對象遵從該協(xié)議。對象成為delegate之后,就可以在相關事件發(fā)生時(例如某個異步任務執(zhí)行完畢時)得到通知了。
如果改用塊來寫的話,代碼會更清晰。塊可以令這種API變得更緊致,同時也令開發(fā)者調用起來更加方便。
typedef void (^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (id)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler: (EOCNetworkFetcherCompletionHandler)handler;
@end
這和使用委托協(xié)議很像,不過多了個好處,就是可以在調用start方法時直接以內聯(lián)形式定義completion handler,以此方式來使用“網絡數(shù)據(jù)獲取器”(network fetcher),可以令代碼比原先易懂很多。
與使用委托模式的代碼相比,用塊寫出來的代碼閑的更為整潔。委托模式有個缺點:如果類要分別使用多個獲取器下載不同的數(shù)據(jù),那么就得在delegate回調方法里根據(jù)傳入的獲取器參數(shù)來切換。
異步執(zhí)行任務完畢后所需運行的業(yè)務邏輯,和啟動異步任務所用的代碼放在了一起。無須保存獲取器,也無需再回調方法里切換,每個completion handler的業(yè)務邏輯,都是和相關獲取器對象一起來定義的。
NSURL *url = [[NSURL alloc] initWithString:@"http:www.baidu.com"];
EOCNetworkFetcher *fetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
_fetchedFooData = data;
}];
這種寫法還有其他用途,比如,現(xiàn)在很多基于塊的API都使用塊來處理錯誤??梢苑謩e用兩個處理器來處理操作失敗的情況和操作成功的情況。也可以把處理失敗情況所需的代碼,與處理正常情況所用的代碼,都封裝到同一個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變量,并且要把所有邏輯代碼都放在一處。這種寫法的缺點是:由于全部邏輯都寫在一起,所以會令塊變得比較長,且比較復雜。然而只用一個塊的寫法也有好處,那就是更為靈活。
把成功情況和失敗情況放在同一個塊中,還有個優(yōu)點:調用API的代碼可能會在處理成功相應的過程中發(fā)現(xiàn)錯誤。比方說,返回的數(shù)據(jù)可能太短了。這種情況需要和網絡數(shù)據(jù)獲取器所認定的失敗情況按同一方式處理。此時,如果采用單一塊的寫法,那么就能把這種情況和所認定的失敗情況統(tǒng)一處理了。
總體來說,筆者建議使用同一塊來處理成功與失敗的情況,蘋果公司似乎也是這樣設計其API的。例如,Twitter框架中的TWRequest及MapKit框架中的MKLocalSearch都只是用一個handler塊。
有時需要在相關時間點執(zhí)行回調操作,這種情況也可以使用handler塊。比方說,調用網絡數(shù)據(jù)獲取器的代碼,也許想在每次有下載進度時都得到通知。這可以通過委托模式實現(xiàn)。不過也可以使用本節(jié)的handler塊,把處理下載進度的handler定義成塊類型,并新增一個此類型的屬性:
typedef void (^EOCNetworkFetcheProgressHandler)(float progress);
@property (nonatomic, copy) EOCNetworkFetcheProgressHandler progressHandler;
這種寫法很好,因為它還是能把所有業(yè)務邏輯都放在一起,也就是把創(chuàng)建網絡數(shù)據(jù)獲取器和定義progress handler所用的代碼寫在一處。
基于handler來設計API還有個原因,就是某些代碼必須運行在特定的線程上。因此,最好能由調用API的人來決定handler應該運行在那個線程上。NSNotificationCenter就屬于這種API,它提供了一個方法,調用者可以經由此方法來注冊想要接收的通知,等到相關事件發(fā)生時,通知中心就會執(zhí)行注冊好的那個塊。調用者可以指定某個塊應該安排在哪個執(zhí)行隊列里,然而這不是必需的。若沒有指定隊列,按默認方式執(zhí)行。
- (id <NSObject>)addObserverForName:(nullable NSString *)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
要點
- 在創(chuàng)建對象時,可以使用內聯(lián)的handler塊將相關業(yè)務邏輯一并聲明。
- 在有多個實例需要監(jiān)控時,如果采用委托模式,那么經常需要根據(jù)傳入的對象來切換,而若改用handler塊來實現(xiàn),則可直接將塊與相關對象放在一起。
- 設計API時如果用到了handler塊,那么可以增加一個參數(shù),使調用者可通過此參數(shù)來決定應該把塊安排在哪個隊列上執(zhí)行。