1. 程式人生 > >有一種 Block 叫 Callback,有一種 Callback 叫 CompletionHandler

有一種 Block 叫 Callback,有一種 Callback 叫 CompletionHandler

【引言】iOS10推送部分的API,大量使用了 CompletionHandler 這種命名方式,那麼本文我們將對比下這種 Block 的特殊性,以便更好的理解和在自己的專案中實踐 CompletionHandler 樣式的 Blcok。

正文

我們作為開發者去整合一個 Lib (也可以叫輪子、SDK、下文統一叫 Lib)時,我們會發現我們遇到的 Block, 按照功能的角度劃分,其實可以分為這幾種:

  • Lib 通知開發者,Lib操作已經完成。一般命名為 Callback
  • 開發者通知 Lib,開發者的操作已經完成。一般可以命名為 CompletionHandler。

這兩處的區別: 前者是 “Block 的執行”,後者是 “Block 的填充”。

Callback vs CompletionHandler 命名與功能的差別,Apple 也沒有明確的編碼規範指出過,只不過如果按照“執行與填充”的功能劃分的話,callbackcompletionHandler 的命名可以區分開來對待。同時也方便呼叫者理解 block 的功能。但總體來說,Apple 官方的命名中,“Block 填充“這個功能一般都會命名為 “completionHandler”,“Block 執行”這個功能大多命名為了“callback” ,也有少部分命名為了 “completionHandler”。

比如:

NSURLSession 中,下面的函式將 “callback” 命名為了 “completionHandler”:

1 -(NSURLSessionDataTask *)dataTaskWithURL
:(NSURL *)url completionHandler:(void(^)(NSData *_Nullable data,NSURLResponse *_Nullable response,NSError *_Nullable error))completionHandler;

我們常常見到 CompletionHandler 被用到了第一種場景,而第一種場景“Block 執行”命名為 Callback 則更合適。

不是所有 Block 都適合叫做 CompletionHandler

一般情況下,CompletionHandler 的設計往往考慮到多執行緒操作,於是,你就完全可以非同步操作,然後線上程結束時執行該 CompletionHandler,下文的例子中會講述下 CompletionHandler 方式在多執行緒場景下的一些優勢。

CompletionHandler + Delegate 組合

在 iOS10 中新增加的 UserNotificaitons 中大量使用了這種 Block,比如:

123 -(void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler;

文件 對 completionHandler 的註釋是這樣的:

1 The block toexecute when you have finished processing the usersresponse.You must execute thisblock from your method andshould call it asquickly aspossible.The block has no returnvalue orparameters.

同樣在這裡也有應用:

123 -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)taskdidReceiveChallenge:(NSURLAuthenticationChallenge *)challengecompletionHandler:(void(^)(NSURLSessionAuthChallengeDisposition disposition,NSURLCredential *__nullable credential))completionHandler;

還有另外一個也非常普遍的例子(Delegate 方式使用URLSession 時候必不可少的 4個代理函式之一 )

123 -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTaskdidReceiveResponse:(NSURLResponse *)responsecompletionHandler:(void(^)(NSURLSessionResponseDisposition disposition))completionHandler;

在代理方法實現程式碼裡面,若是不執行 completionHandler(NSURLSessionResponseAllow) 話,http請求就終止了。

CompletionHandler + Block 組合

函式中將函式作為引數或者返回值,就叫做高階函式。

按照這種定義,Block 中將 Block 作為引數,這也就是高階函式。

結合實際的應用場景來看一個例子:

如果有這樣一個需求:

拿我之前的一個 IM 專案 ChatKit-OC (開源的,下面簡稱 ChatKit)為例,當你的應用想要整合一個 IM 服務時,可能這時候,你的 APP 已經上架了,已經有自己的註冊、登入等流程了。用 ChatKit 進行聊天很簡單,只需要給 ChatKit 一個 id 就夠了。聊天是正常了,但是雙方只能看到一個id,這樣體驗很不好。但是如何展示頭像、暱稱呢?於是就設計了這樣一個介面,-setFetchProfilesBlock:

這是上層(APP)提供使用者資訊的 Block,由於 ChatKit 並不關心業務邏輯資訊,比如使用者暱稱,使用者頭像等。使用者可以通過 ChatKit 單例向 ChatKit 注入一個使用者資訊內容提供 Block,通過這個使用者資訊提供 Block,ChatKit 才能夠正確的進行業務邏輯資料的繪製。

示意圖如下:

11224803-31a78b716832c471

具體實現如下:

方法定義如下:

123456789101112131415161718192021 /*! *  @brief The block to execute with the users' information for the userIds. Always execute this block at some point when fetching profiles completes on main thread. Specify users' information how you want ChatKit to show. *  @attention If you fetch users fails, you should reture nil, meanwhile, give the error reason. */typedefvoid(^LCCKFetchProfilesCompletionHandler)(NSArray>*users,NSError *error);/*! *  @brief When LeanCloudChatKit wants to fetch profiles, this block will be invoked. *  @param userIds User ids *  @param completionHandler The block to execute with the users' information for the userIds. Always execute this block at some point during your implementation of this method on main thread. Specify users' information how you want ChatKit to show. */typedefvoid(^LCCKFetchProfilesBlock)(NSArray *userIds,LCCKFetchProfilesCompletionHandler completionHandler);@property(nonatomic,copy)LCCKFetchProfilesBlock fetchProfilesBlock;/*! *  @brief Add the ablitity to fetch profiles. *  @attention  You must get peer information by peer id with a synchronous implementation. *              If implemeted, this block will be invoked automatically by LeanCloudChatKit for fetching peer profile. */-(void)setFetchProfilesBlock:(LCCKFetchProfilesBlock)fetchProfilesBlock;

用法如下所示:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546 #warning 注意:setFetchProfilesBlock 方法必須實現,如果不實現,ChatKit將無法顯示使用者頭像、使用者暱稱。以下方法迴圈模擬了通過 userIds 同步查詢 users 資訊的過程,這裡需要替換為 App 的 API 同步查詢[[LCChatKit sharedInstance]setFetchProfilesBlock:^(NSArray *userIds,LCCKFetchProfilesCompletionHandler completionHandler){if(userIds.count==0){NSInteger code=0;NSString *errorReasonText=@"User ids is nil";NSDictionary *errorInfo=@{@"code":@(code),NSLocalizedDescriptionKey:errorReasonText,};NSError *error=[NSError errorWithDomain:NSStringFromClass([selfclass])code:codeuserInfo:errorInfo];!completionHandler?:completionHandler(nil,error);return;}NSMutableArray *users=[NSMutableArray arrayWithCapacity:userIds.count];#warning 注意:以下方法迴圈模擬了通過 userIds 同步查詢 users 資訊的過程,這裡需要替換為 App 的 API 同步查詢[userIds enumerateObjectsUsingBlock:^(NSString *_Nonnull clientId,NSUInteger idx,BOOL*_Nonnull stop){NSPredicate *predicate=[NSPredicate predicateWithFormat:@"peerId like %@",clientId];//這裡的LCCKContactProfiles,LCCKProfileKeyPeerId都為事先的巨集定義,NSArray *searchedUsers=[LCCKContactProfiles filteredArrayUsingPredicate:predicate];if(searchedUsers.count>0){NSDictionary *user=searchedUsers[0];NSURL *avatarURL=[NSURL URLWithString:user[LCCKProfileKeyAvatarURL]];LCCKUser *user_=[LCCKUser userWithUserId:user[LCCKProfileKeyPeerId]name:user[LCCKProfileKeyName]avatarURL:avatarURLclientId:clientId];[users addObject:user_];}else{//注意:如果網路請求失敗,請至少提供 ClientId!LCCKUser *user_=[LCCKUser userWithClientId:clientId];[users addObject:user_];}}];// 模擬網路延時,3秒//         sleep(3);#warning 重要:completionHandler 這個 Bock 必須執行,需要在你**獲取到使用者資訊結束**後,將資訊傳給該Block!!completionHandler?:completionHandler([users copy],nil);}];

對於以上 Fetch 方法的這種應用場景,其實用方法的返回值也可以實現,但是與 CompletionHandler 相比,無法自由切換執行緒是個弊端。