[OC] 關於block回撥、高階函式“回撥再呼叫”及專案實踐
使用block
進行回撥處理是十分便利的處理方式,在UIKit
的設計中也屢見不鮮,例如:
-
UIView
動畫,動畫執行後呼叫completion
內的block
程式碼。
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion; 複製程式碼
-
模態展示一個頁面,在展示結束後呼叫
completion
內的block
程式碼。
- (void)presentViewController:(UIViewController *)viewController animated:(BOOL)flag completion:(void (^)(void))completion; 複製程式碼
-
用於實現紙質列印的控制器
UIPrintInteractionController
,其模態展示方式,同樣是展示結束後呼叫completion
的block
程式碼。
- (BOOL)presentAnimated:(BOOL)animated completionHandler:(nullable UIPrintInteractionCompletionHandler)completion; 複製程式碼
此外,在WKWebView
中分析JavaScript
程式碼時也有類似應用,實際開發中存在completion
、completionHandler
或者callBack
等不同的命名方式,歸根結底目的都是實現一個事件完成後的“回撥作用”,與使用“委託模式”的delegate + protocol
有異曲同工之妙,這也是很多二級頁面控制器或者檢視的回撥流行使用一個block
屬性來做回撥處理的原因。
而類似 UIView 動畫的animations
的block
動畫引數,以及自動佈局框架Masonry
的設定約束的make/update/remake
中 block 使用,以及例項初始化方法中的block
的應用,則用於更便利地囊括介面設計者的意圖,比如開源網路框架XMNetworking
的請求構造方法或者七牛雲上傳的管理類QNUploadManager
的配置構造方法等,即可在 block 內便利地對請求引數進行配置,對外提供API
時省去了類似[[XMRequest alloc] init]
例項初始化這一步。
[XMCenter sendRequest:^(XMRequest * _Nonnull request) { request.api = @"example/blabla"; request.httpMethod = kXMHTTPMethodGET; } ]; 複製程式碼
2/3 回撥再呼叫
上文提及委託模式也是典型的回撥方式之一,在iOS
應用程式的入口就採用了委託模式,即整個應用程式UIApplication
單例的及其委託物件AppDelegate
,UIApplicationDelegate
協議中聲明瞭諸多可選optional
方法,將程式的執行情況相關事件/狀態回撥給委託者AppDelegate
。與本文關聯的是,自iOS 7
後系統升級了遠端推送策略而新增了一系列API
,其中就包括UIApplicationDelegate
協議中一個使用block
的協議方法如下(含典型實現):
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 當收到推送後非同步載入一些資料 // 然後告知回去資料的結果情況 completionHandler(UIBackgroundFetchResultNewData); }); } 複製程式碼
此協議方法是告知AppDelegate
程式收到了遠端推送,AppDelegate
可以做一些獲取資料的處理,並要求在獲取資料完成後呼叫completionHandler
告知UIApplication
獲取資料的結果情況,從而讓UIApplication
來估算電量和資料消耗情況,作為系統進行資源管理的一部分,要求completionHander
必須儘快呼叫(30 s以內),這個場景就是回撥再呼叫
,其中:
-
這是
UIApplication
委託的協議方法,而不是其例項方法,是UIApplication
呼叫AppDelegate
的方法,即回撥 。 -
在回撥的協議方法中,攜帶一個
block
型別的引數,將一段程式碼傳遞給AppDelegate
,並要求AppDelegate
完成業務邏輯後執行此block
程式碼,以達到呼叫UIApplication
的目的,即回撥再呼叫 。
這類將block
作為引數或者返回值使用通常稱為高階函式
。
這種設計方式,有一種變換的實現方式:由UIApplication
單獨再提供一個API
讓AppDelgate
來主動呼叫,寫一個虛擬碼方法如下:
// UIApplication 類的虛擬碼 // 處理 delegate 後臺獲取資料後的結果 - (void)handleBackgroundFetchResult:(UIBackgroundFetchResult)result; 複製程式碼
則,上述協議方法及其典型實現,可以替換為如下虛擬碼:
// 注意移除了 回撥的 block,改為直接呼叫虛擬碼 API - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo { dispatch_async(dispatch_get_global_queue(0, 0), ^{ // 非同步獲取一些資料 // 然後告知獲取資料的結果情況 // completionHandler(UIBackgroundFetchResultNewData); // 改為 直接呼叫 [application handleBackgroundFetchResult:UIBackgroundFetchResultNewData]; }); } 複製程式碼
對比兩種方式,後者顯然不如在回撥delegate
時直接帶入需要執行的邏輯來得直觀。這種“回撥再呼叫”用法,後來在iOS 10
釋出的系統重構的通知管理框架UserNotification
中頻繁使用,比如上述方法在UNUserNotificaionCenter
的UNUserNotificationCenterDelegate
中的宣告。
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)())completionHandler; 複製程式碼
總結可見,在如下場景中:物件A
回撥給被回撥者B
完成後,仍需要被回撥者B
去呼叫A
並傳遞一些引數(或無引數)執行延續邏輯。採用類似回撥一個 block 引數實現回撥再呼叫
是很不錯的方案。
3/3 專案實踐
一個使用block
做回撥處理,並在回撥中返回block
引數用於延續邏輯在實際專案中的應用案例:
3個專案中,均需要通過網路介面請求的方式來獲取客服聯絡電話後彈窗提示可撥打,三個網路介面各不相同。將該業務邏輯封裝為一個 API ,方便多處業務入口的呼叫,具體是在通訊管理的單例[ContactHelper sharedInstance]
:
// 提示撥打客服電話,例項方法 - (void)callCustomerServerInVC:(UIViewController *)VC; 複製程式碼
由於不同專案中網路介面不一致,且介面可能會變動,因此不在ContactHelper
寫網路請求邏輯,而是通過block
的形式回撥給具體專案進行實現,同時將彈窗的邏輯和樣式封裝在ContactHelper
內部進行統一。
-
第一步,設定獲取客服資訊的邏輯,通過
ContactHelper
的宣告為fetcher
的block
屬性儲存,當需要時進行呼叫
typedef void(^ContactCompletion)(NSDictionary *userInfo, NSString *errorMsg); // - (void)configCustomerPhoneFetcher:(void (^)(ContactCompletion completion, UIViewController *vc))fetcher; /// 儲存獲取聯絡方式的邏輯 - (void)configCustomerPhoneFetcher:(void (^)(ContactCompletion))fetcher { _fetcher = [fetcher copy]; } /// 專案中具體配置的呼叫示例 ContactHelper *helper = [ContactHelper sharedInstance]; [helper configCustomerPhoneFetcher:^(ContactCompletion completion, UIViewController *vc) { // 通過網路請求非同步獲取電話號碼 NSString *tel = @"400xxxxxxx"; /// 執行 回撥再呼叫,實現電話號碼撥叫 completion(@{kContactPhoneKey:tel,nil); }]; 複製程式碼
-
第二步,當業務方呼叫
ContactHelper
以彈窗撥打客服電話時,ContactHelper
呼叫第一步 配置好的獲取方式fetcher
屬性。 -
第三步,利用
fetcher
獲取到並再呼叫的資訊進行彈窗撥號提示,因此ContactHelper
內部實現呼叫客服電話後再彈窗提醒如下:
//獲取客服電話 - (void)callCustomerServiceInVC:(UIViewController *)controller{ if (!_fetcher)return; // 配置獲取到客服電話後的操作 ContactCompletion completion = ^(NSDictionary *dic, NSString *errorMsg){ if (errorMsg) { // 提示獲取號碼出錯 } else { NSString *phone = dic[kContactPhoneKey]; // 彈窗提示撥號 }; // 執行儲存的回撥,並將下一步的操作傳遞過去 _fetcher(completion, controller); } 複製程式碼
綜上,利用回撥再呼叫
這個思路,可以將3個專案的不同介面的請求客服電話的請求隔離在3個專案中設定,而彈窗提示的邏輯則在ContactHelper
中統一處理,而在其他的一些需要外部獲取資料後再返回到呼叫者延續執行的情況都可以使用該方案。
參考文獻
iOS程式犭袁:ofollow,noindex">有一種 Block 叫 Callback,有一種 Callback 叫 CompletionHandler
其中,在第三方雲服務LeanCloud
的一些SDK
中有類似的高階函式應用。