iOS 中網絡請求同步

場景

在開發過程中,有時候會遇到這樣一些問題,比如:

  • 在某些業務要求下,需發送同步請求。
  • 在某些界面需請求多個接口,且各個接口返回的數據之間或者整體存在依賴關系。
  • ···

那么在上述的這些場景下應如何發送網絡請求?發同步請求 or 異步請求?請求嵌套?······

本文將簡單探究開發過程中網絡請求同步的問題以及相關注意點。

NSURLConnection 中的同步請求

我們都知道 NSURLConnection 中有一個同步請求的 API :


+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request
returningResponse:(NSURLResponse **)response
error:(NSError **)error

針對上述的第一種情況,該 API 可滿足要求。如果同步請求阻塞主線程的時間過長,存在被 watchdog kill 的可能。想避免這種情況,建議在子線程中調用此 API。(感興趣的同學可以看看,關于 watchdog timeout crashes/Understanding and Analyzing Application Crash Reports)

同步請求相對異步請求而言存在一些缺陷,如:

  1. 請求發出后,就無法取消
  2. 返回的數據只能放到請求結束后進行處理
  3. ···

很遺憾,NSURLConnection 目前已被蘋果全面棄用,并且 AFNetworking 在 3.x 中已經移除此類 API,因此同步請求不建議采用此種方式。

Dispatch_semaphore(信號量)

信號量機制,我們可以簡單理解為資源管理分配的一種抽象方式。在 GCD 中,提供了以下這么幾個函數,可用于請求同步等處理,模擬同步請求:

  1. dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
  2. dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
  3. dispatch_semaphore_signal(semaphore);

value 可以理解為資源數量,以 value = 0 為例,調用 dispatch_semaphore_wait 操作成功后,當資源數量 value 等于 0 時,就會阻塞當前線程(反之,value 就會減 1),直到有 dispatch_semaphore_signal 通知信號發出,當 value 大于 0 時,當前線程就會被喚醒繼續執行其他操作。

下面我們展示一段代碼來模擬同步請求:

Objective-C:

    // 1.創建信號量
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
    NSLog(@"0");
    // 開始異步請求操作(部分代碼略)
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"1");
        // This function returns non-zero if a thread is woken. Otherwise, zero is returned.
        // 2.在網絡請求結束后發送通知信號
        dispatch_semaphore_signal(semaphore);
    });
    // Returns zero on success, or non-zero if the timeout occurred.
    // 3.發送等待信號
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"2");

    // print 0、1、2
    

Swift:

    func sendSynchronousDataTask(with url: URL) -> (Data?, URLResponse?, Error?) {
        var data: Data?
        var response: URLResponse?
        var error: Error?
        // 1.創建信號量
        let semaphore = DispatchSemaphore(value: 0)
        // 開始異步請求操作
        let dataTask = URLSession.shared.dataTask(with: url) {
            data = $0
            response = $1
            error = $2
            // 2.在網絡請求結束后發送通知信號
            semaphore.signal()
        }
        dataTask.resume()
        // 3.發送等待信號
        _ = semaphore.wait(timeout: .distantFuture)
        
        return (data, response, error)
    }

在 iOS 系統中,如果應用不能及時的響應用戶界面交互事件(如啟動、暫停、恢復和終止),watchdog 就會殺死程序并生成一個 watchdog 超時崩潰報告,據官方說法,watchdog timeout 時間并沒有明文規定,但一般會少于網絡請求超時時間。

In order to keep the user interface responsive, iOS includes a watchdog mechanism. If your application fails to respond to certain user interface events (launch, suspend, resume, terminate) in time, the watchdog will kill your application and generate a watchdog timeout crash report. The amount of time the watchdog gives you is not formally documented, but it's always less than a network timeout.

這里有一個奇怪的現象,經測試,筆者采用信號量機制一直阻塞主線程時并沒有被 watchdog kill,但 NSURLConnection 中的同步請求方法 + sendSynchronousRequest:returningResponse:error: 在慢速網絡下與其說 crash 了,不如說被 watchdog kill 了。不扯遠了,開始下一個話題 —— dispatch_group_t

Dispatch_group(組)

繼續本文話題,回顧文章開頭提到的問題,如果針對單個請求進行同步處理,那么使用同步請求即可,上述兩種方式都可以。如果在某些界面需請求多個接口,且各個接口返回的數據之間或者整體存在依賴關系,那怎么辦呢?雖然采用嵌套請求的方式能解決此問題,但存在很多問題,如:其中一個請求失敗會導致后續請求無法正常進行、多個請求在時間上沒有復用,即無并發性。

A dispatch group is a mechanism for monitoring a set of blocks. Your application can monitor the blocks in the group synchronously or asynchronously depending on your needs. By extension, a group can be useful for synchronizing for code that depends on the completion of other tasks.

針對這種情形,即某個操作依賴于其他幾個任務的完成時,我們可采用 dispatch_group。主要使用如下兩個函數:

  1. dispatch_group_enter(group);
  2. dispatch_group_leave(group);

以上這兩個函數必須配對使用,否則 dispatch_group_notify 不會觸發。貼一段代碼(源碼):

    // 創建 dispatch 組
    dispatch_group_t group = dispatch_group_create();
    
    // 第一個請求:
    dispatch_group_enter(group);
    [self sendGetAddressByPinWithURLs:REQUEST(@"getAddressByPin.json") completionHandler:^(NSDictionary * _Nullable data, NSError * _Nullable error) {
        NSArray *addressList = [TXAddressModel mj_objectArrayWithKeyValuesArray:data[@"addressList"]];
        self.addressList = addressList;
        dispatch_group_leave(group);
    }];
    
    // 第二個請求
    dispatch_group_enter(group);
    [self sendCurrentOrderWithURLs:REQUEST(@"currentOrder.json") completionHandler:^(NSDictionary * _Nullable data, NSError * _Nullable error) {
        TXCurrentOrderModel *currentOrderModel = [TXCurrentOrderModel mj_objectWithKeyValues:data];
        self.currentOrderModel = currentOrderModel;
        dispatch_group_leave(group);
    }];
    
    // 當上面兩個請求都結束后,回調此 Block
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"OVER:%@", [NSThread currentThread]);
        [self setupOrderDataSource];
    });

browser-jd.png

對于熟悉 dispatch_group 的同學來說,可能會想,為何不用 dispatch_group_async?對于網絡請求而言,請求發出時它就已經執行完畢,也就是 block 中還有個 completeHandler 的情況下,dispatch_group_async 并不會等待網絡請求的回調,所以不符合我們要求。

總結

通過本文簡單探究,展示了如何采用信號量機制模擬同步請求,在開發過程中,我們應盡量避免發送同步請求;并且在某個操作依賴于其他幾個任務的完成時,采用 dispatch_group_async or dispatch_group_enter/dispatch_group_leave 來實現同步等處理。如果是進行網絡請求同步,應采用后者。當然,如果感興趣,我們可以在第三方網絡庫的基礎上封裝一層自己網絡庫。(相關源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容