前言
對於iOS開發中的網絡請求模塊,AFNet的使用應該是最熟悉不過了,但你是否把握了網絡請求正確的完成時機?本篇文章涉及線程同步、線程依賴、線程組等專用名詞的含義,若對上述名詞認識模糊,可先進行查閱理解後閱讀本文。如果你也糾結於文中所述問題,可進行閱讀希望對你有所幫助。大神無視勿噴。
在真實開發中,我們通常會遇到如下問題:
一、某界面存在多個請求,希望所有請求均結束才進行某操作。
對於這一問題的解決方案很容易想到通過線程組進行實現。代碼如下:
dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //請求1 NSLog(@"Request_1"); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //請求2 NSLog(@"Request_2"); }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //請求3 NSLog(@"Request_3"); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ //界面刷新 NSLog(@"任務均完成,刷新界面"); });
打印如下
Request_2 Request_1 Request_3 任務均完成,刷新界面
根據打印結果觀察可能並沒有什麼問題,但需要注意的是Request_1、Request_2等在真實開發中通常對應為某個網絡請求。而網絡請求通常為異步,那這時是否還會有同樣結果呢?
口說無憑,我們將NSLog(@"Request");部分替換為真正的網絡請求。
對於App請求數據大部分人都會選擇AFNetworking。使用AFN異步請求,請求的數據返回後,就刷新相關UI。如果某一個頁面有多個網絡請求,我們假設有三個請求,A、B、C,而且UI裡的數據必須等到A、B、C全部完成後刷新後才顯示。
這裡我們書寫一個網絡請求通用方法,假設同時請求某新聞列表的3頁數據,每頁均為一個獨立的網絡請求。使用我們最常用的AFNet請求,方法如下(真實開發中可能為banner數據請求、主體網絡請求、廣告網絡請求等):
- (void)request_A { AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; NSDictionary *parameter = @{@"token":@"63104AB32427EBF89B957BBD1A5C5C11", @"page":@"1", @"upTime":@"desc"}; [manager POST:URL parameters:parameter progress:^(NSProgress * _Nonnull uploadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:nil]; for (NSDictionary *rowsDict in dict[@"rows"]) { NSLog(@"A___%@",rowsDict[@"title"]); } } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { }]; }
request_B、request_C分別為請求第二頁與第三頁數據,這裡不重復書寫。為了顯示更加明顯,在請求中打印了對應新聞的標題內容。
運行打印如下:
任務均完成,刷新界面 C___搞笑,不是認真的 C___攝影 | 街拍 C___生活小竅門 C___傳統中國 C___想吃的美食系列 B___時間的見證者 B___沒事 來吐槽吧…… B___觸動心靈的攝影 B___攝影 | 黑白印記 B___每日插畫推薦 A___左愛情,右面包 A___潮我看 A___世界各國的人們怎麼過情人節 A___一點創意點亮生活 A___攝影 | 隨手拍
運行後馬上接收到了線程組完成的提示,之後數據才依次請求下來,很明顯三個單純的AFNet請求已經不能滿足我們的需求了。線程組完成時並沒有在我們希望的時候給予通知。在真實開發中會造成的問題為多個請求均加載完成,但界面已在未得到數據前提前刷新導致界面空白。
因此對於這種問題需要另辟蹊徑,這裡我們就要借助GCD中的信號量dispatch_semaphore進行實現,即營造線程同步情況。
dispatch_semaphore信號量為基於計數器的一種多線程同步機制。用於解決在多個線程訪問共有資源時候,會因為多線程的特性而引發數據出錯的問題。
如果semaphore計數大於等於1,計數-1,返回,程序繼續運行。如果計數為0,則等待。
dispatch_semaphore_signal(semaphore)為計數+1操作。dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER)為設置等待時間,這裡設置的等待時間是一直等待。我們可以通俗的理解為單櫃台排隊點餐,計數默認為0,每當有顧客點餐,計數+1,點餐結束-1歸零繼續等待下一位顧客。比較類似於NSLock。
我們將網絡請求通用方法進行修改如下:
- (void)request_A { //創建信號量並設置計數默認為0 dispatch_semaphore_t sema = dispatch_semaphore_create(0); AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; NSDictionary *parameter = @{@"token":@"63104AB32427EBF89B957BBD1A5C5C11", @"page":@"1", @"upTime":@"desc"}; [manager POST:URL parameters:parameter progress:^(NSProgress * _Nonnull uploadProgress) { } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { //計數+1操作 dispatch_semaphore_signal(sema); NSDictionary * dict = [NSJSONSerialization JSONObjectWithData:responseObject options:NSJSONReadingMutableContainers error:nil]; for (NSDictionary *rowsDict in dict[@"rows"]) { NSLog(@"A___%@",rowsDict[@"title"]); } } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { ////計數+1操作 dispatch_semaphore_signal(sema); }]; //若計數為0則一直等待 dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); }
為方便閱讀,偽代碼如下:
dispatch_semaphore_t sema = dispatch_semaphore_create(0); [網絡請求:{ 成功:dispatch_semaphore_signal(sema); 失敗:dispatch_semaphore_signal(sema); }]; dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
這時我們再運行程序,打印如下:
C___聆聽的耳朵 C___家居 | 你的家裡還缺點什麼? C___logo設計 C___時裝 C___搞笑,不是認真的 A___左愛情,右面包 A___潮我看 A___世界各國的人們怎麼過情人節 A___一點創意點亮生活 A___攝影 | 隨手拍 B___時間的見證者 B___沒事 來吐槽吧…… B___觸動心靈的攝影 B___攝影 | 黑白印記 B___每日插畫推薦 任務均完成,刷新界面
運行打印可見,通過信號量dispatch_semaphore完美的解決了此問題,並且網絡請求仍為異步,不會堵塞當前主線程。
二、某界面存在多個請求,希望請求依次執行。
對於這個問題通常會通過線程依賴進行解決,因使用GCD設置線程依賴比較繁瑣,這裡通過NSOperationQueue進行實現,這裡采用比較經典的例子,三個任務分別為下載圖片,打水印和上傳圖片,三個任務需異步執行但需要順序性。代碼如下,下載圖片、打水印、上傳圖片仍模擬為分別請求新聞列表3頁數據。
//1.任務一:下載圖片 NSBlockOperation *operation1 = [NSBlockOperation blockOperationWithBlock:^{ [self request_A]; }]; //2.任務二:打水印 NSBlockOperation *operation2 = [NSBlockOperation blockOperationWithBlock:^{ [self request_B]; }]; //3.任務三:上傳圖片 NSBlockOperation *operation3 = [NSBlockOperation blockOperationWithBlock:^{ [self request_C]; }]; //4.設置依賴 [operation2 addDependency:operation1]; //任務二依賴任務一 [operation3 addDependency:operation2]; //任務三依賴任務二 //5.創建隊列並加入任務 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperations:@[operation3, operation2, operation1] waitUntilFinished:NO];
首先我們使用未添加信號量dispatch_semaphore時運行,打印如下
B___時間的見證者 B___沒事 來吐槽吧…… B___觸動心靈的攝影 B___攝影 | 黑白印記 B___每日插畫推薦 A___潮我看 A___左愛情,右面包 A___世界各國的人們怎麼過情人節 A___一點創意點亮生活 A___攝影 | 隨手拍 C___盤 C___聆聽的耳朵 C___家居 | 你的家裡還缺點什麼? C___logo設計 C___時裝
根據打印結果可見,若不對請求方法做處理,其運行結果並不是我們想要的,聯系實際需求,A、B、C請求分別對應下載圖片、打水印、上傳圖片,而此時運行順序則為B->A->C,在未獲得圖片時即執行打水印操作明顯是錯誤的。重復運行亦會出現不同結果,即請求不做處理,其結果不可控無法預測。線程依賴設置並未起到作用。
解決此問題的方法仍可通過信號量dispatch_semaphore進行解決。我們將請求方法替換為添加dispatch_semaphore限制的形式。即
dispatch_semaphore_t sema = dispatch_semaphore_create(0); [網絡請求:{ 成功:dispatch_semaphore_signal(sema); 失敗:dispatch_semaphore_signal(sema); }] dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
再次重復運行,我們會發現每次運行結果均一致,A、B、C三任務異步順序執行(A->B->C)
A___潮我看 A___左愛情,右面包 A___世界各國的人們怎麼過情人節 A___一點創意點亮生活 A___攝影 | 隨手拍 B___時間的見證者 B___沒事 來吐槽吧…… B___觸動心靈的攝影 B___攝影 | 黑白印記 B___每日插畫推薦 C___盤 C___聆聽的耳朵 C___家居 | 你的家裡還缺點什麼? C___logo設計 C___時裝
通過重復運行打印結果可證實確實實現了我們想要的效果。這樣即解決了所提出的問題二。
後續
在開發中我們會遇到很多需要進行線程同步或請求同步的情況。比如彈出視圖設置某功能,在點擊確認按鈕時發生請求,在請求成功同時銷毀彈出視圖等,均需要保證在請求真正完成時進行下一步操作。因此把握網絡請求完成的正確時機還是很有必要的。
暫時寫到這裡,若有更好實例會繼續在本文更新。