[iOS] [OC] 關於block回撥、高階函式“回撥再呼叫”及專案實踐

席萍萍BO發表於2018-09-19

1/3 回撥

使用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,其模態展示方式,同樣是展示結束後呼叫completionblock程式碼。
- (BOOL)presentAnimated:(BOOL)animated 
      completionHandler:(nullable UIPrintInteractionCompletionHandler)completion;
複製程式碼

此外,在 WKWebView 中分析JavaScript程式碼時也有類似應用,實際開發中存在 completioncompletionHandler 或者callBack等不同的命名方式,歸根結底目的都是實現一個事件完成後的“回撥作用”,與使用“委託模式”的 delegate + protocol 有異曲同工之妙,這也是很多二級頁面控制器或者檢視的回撥流行使用一個 block 屬性來做回撥處理的原因。

而類似 UIView 動畫的 animationsblock動畫引數,以及自動佈局框架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單例的及其委託物件AppDelegateUIApplicationDelegate協議中宣告瞭諸多可選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以內),這個場景就是回撥再呼叫,其中:

  1. 這是UIApplication委託的協議方法,而不是其例項方法,是 UIApplication呼叫 AppDelegate 的方法,即回撥
  2. 在回撥的協議方法中,攜帶一個block型別的引數,將一段程式碼傳遞給 AppDelegate ,並要求 AppDelegate完成業務邏輯後執行此block程式碼,以達到呼叫UIApplication的目的,即回撥再呼叫

這類將block作為引數或者返回值使用通常稱為高階函式

這種設計方式,有一種變換的實現方式:由UIApplication單獨再提供一個 APIAppDelgate來主動呼叫,寫一個虛擬碼方法如下:

// 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 中頻繁使用,比如上述方法在 UNUserNotificaionCenterUNUserNotificationCenterDelegate 中的宣告。

- (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的宣告為 fetcherblock屬性儲存,當需要時進行呼叫
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程式犭袁: 有一種 Block 叫 Callback,有一種 Callback 叫 CompletionHandler 其中,在第三方雲服務 LeanCloud 的一些SDK中有類似的高階函式應用。

相關文章