場景
在開發過程中,有時候會遇到這樣一些問題,比如:
- 在某些業務要求下,需發送同步請求。
- 在某些界面需請求多個接口,且各個接口返回的數據之間或者整體存在依賴關系。
- ···
那么在上述的這些場景下應如何發送網絡請求?發同步請求 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)
同步請求相對異步請求而言存在一些缺陷,如:
- 請求發出后,就無法取消
- 返回的數據只能放到請求結束后進行處理
- ···
很遺憾,NSURLConnection 目前已被蘋果全面棄用,并且 AFNetworking 在 3.x 中已經移除此類 API,因此同步請求不建議采用此種方式。
Dispatch_semaphore(信號量)
信號量機制,我們可以簡單理解為資源管理分配的一種抽象方式。在 GCD 中,提供了以下這么幾個函數,可用于請求同步等處理,模擬同步請求:
- dispatch_semaphore_t semaphore = dispatch_semaphore_create(value);
- dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
- 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。主要使用如下兩個函數:
- dispatch_group_enter(group);
- 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];
});
對于熟悉 dispatch_group 的同學來說,可能會想,為何不用 dispatch_group_async?對于網絡請求而言,請求發出時它就已經執行完畢,也就是 block 中還有個 completeHandler 的情況下,dispatch_group_async 并不會等待網絡請求的回調,所以不符合我們要求。
總結
通過本文簡單探究,展示了如何采用信號量機制模擬同步請求,在開發過程中,我們應盡量避免發送同步請求;并且在某個操作依賴于其他幾個任務的完成時,采用 dispatch_group_async or dispatch_group_enter/dispatch_group_leave 來實現同步等處理。如果是進行網絡請求同步,應采用后者。當然,如果感興趣,我們可以在第三方網絡庫的基礎上封裝一層自己網絡庫。(相關源碼)