問題來源: 最近遇到了一個多網絡異步回調的問題,其實也就是我們請求的數據是異步的,我們使用了帶有返回值的方法,結果我們先獲取的結果都是空的,這個其實對新手來說,可能不知道為什么會有這個結果,這個其實稍微百度一下就能找到答案,不過還是寫一下,為大家處理一下盲區
我們主要介紹3中方法,來獲取異步方法中的回調結果
一、 使用信號量 dispatch_semaphore_t 控制請求
- 我們先看一下實際應用中的一些列子,我們盡量仿真模擬真實的網絡請求我們看下代碼如何工作的:
/*!
* @author Raybon.Lee, 16-04-07 10:04:02
*
* @brief 從服務器請求數據,異步返回數據,并更新UI,我們這里盡量高仿真實的網絡請求環境
*
* @return 返回一個數組
*
* @since <#1.0#>
*/
- (__kindof NSArray *)fetchDataFromServe{
//假如下面這個數組是用來存放數據的
NSMutableArray * array = [NSMutableArray arrayWithCapacity:0];
//下面這個來代替我們平時常用的異步網絡請求
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i=0; i<10; i++) {
[array addObject:[NSNumber numberWithInt:i]];
}
NSLog(@"array = %@",array);
});
return array;
}
正常情況下,我們可能會有這種寫法,因為沒注意到返回數據是異步的
我們看下調用返回:
NSArray * resultArray = [self fetchDataFromServe];
NSLog(@"resultArrsy = %@",resultArray);
*控制臺輸出結果:
2016-04-07 10:30:45.868 ABNumDemo[4129:1394388] resultArrsy = (
)
2016-04-07 10:30:45.875 ABNumDemo[4129:1394441] array = (
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
)
我們第一眼看到的感覺一般都是很明確,這個值肯定存在,那為什么最終獲取的值就是不對呢,有時候考慮問題很容易凌亂,上面的結果一定反饋給我們信息了,結果是空的
- 現在我們修改一下方法,使用信號量控制,順便說一下信號量是如何工作
- (__kindof NSArray *)fetchDataFromServe{
//修改下面的代碼,使用信號量來進行一個同步數據
//我們傳入一個參數0 ,表示沒有資源,非0 表示是有資源,這一點需要搞清楚
//補充:這里的整形參數如果是非0 就是總資源
dispatch_semaphore_t semaoh = dispatch_semaphore_create(0);
//假如下面這個數組是用來存放數據的
NSMutableArray * array = [NSMutableArray arrayWithCapacity:0];
//下面這個來代替我們平時常用的異步網絡請求
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i=0; i<10; i++) {
[array addObject:[NSNumber numberWithInt:i]];
}
NSLog(@"array = %@",array);
//謝謝葉神的糾正 修正:發送信號,信號量 管理資源數+1 車輛如果遇到綠色信號燈,等待的車輛就會減少,也就是資源數減少,一直減少到 dispatch_semaphore_wait 這個函數返回 0 才會繼續執行,
//之前注釋有點問題,這個通過相當于 放行操作 執行信號數量+1,這個地方一定觸發信號,就會通知wait 函數進行 -1 操作,也就是后面可以繼續通行
dispatch_semaphore_signal(semaoh);
});
//信號等待 時,資源數 -1 阻塞當前線程 這個難理解的可以理解成等待紅綠燈的車輛,紅燈等待車輛
//車輛自然是累加的排隊等候,沒有資源,會一直觸發信號控制
dispatch_semaphore_wait(semaoh, DISPATCH_TIME_FOREVER);
return array;
}
- 信號量的理解:
我們初始化的時候會先設置一個信號總量,如果信號總量的整形參數是0 ,那么就是沒有資源需要等待,我們如果下面執行wait 操作,那么相當于線程擁堵,執行信號-1 操作,如果現在是綠燈通行狀態,我們會設置 sigle 信號,執行一個信號+1 操作,告訴當前線程,有一個信號可以釋放了,大概可以這么來理解
修改之后我們再來看一下控制臺的輸出:
2016-04-07 10:36:17.361 ABNumDemo[4136:1395451] array = (
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
)
2016-04-07 10:36:17.364 ABNumDemo[4136:1395425] resultArrsy = (
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
)
是不是看到我們想要的效果了,其實我們就是要異步回調有結果之后再釋放資源數
二、 下面我們再看下 dispatch_group_t
的使用方法
- 同樣我們還是寫一個仿真請求網絡的地方
- (NSArray *)fetchTheNetUserData{
dispatch_group_t group = dispatch_group_create();
NSMutableArray * array = [NSMutableArray array];
for (int i=0; i<5; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[array addObject:[NSNumber numberWithInt:i]];
});
}
return array;
}
我們現在調用一下函數看下輸出:
NSArray * groupArray = [self fetchTheNetUserData];
NSLog(@"grouparray = %@",groupArray);
2016-04-07 11:17:53.508 ABNumDemo[4143:1400362] grouparray = (
)
這個結果其實不是想要的結果,我們稍微修改一下
- (NSArray *)fetchTheNetUserData{
//使用GCD創建一個group 組,這個其實不是很好理解,我們可以理解成,創建一個組,然后給這個組添加任務,等到所有組的任務都結束之后在進行一個頒獎典禮,這個就是我們要的效果
dispatch_group_t group = dispatch_group_create();
NSMutableArray * array = [NSMutableArray array];
for (int i=0; i<5; i++) {
//group 進入一個組 和dispatch_group_leave 這兩個必須是成對出現,缺一不可,如果對應不上則會出現線程阻塞,
dispatch_group_enter(group);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[array addObject:[NSNumber numberWithInt:i]];
//dispatch_group_leave 離開一個組 ,這個和排毒其實有點類似
dispatch_group_leave(group);
});
}
//dispatch_group_wait 這個函數返回 0則會繼續執行,否則一直等待 group 組內的所有成員任務完畢,這個其實也是資源數增加的一個函數,等待當前線程結束
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"array = %@",array);
return array;
}
- 增加 ,一下兩種形式等價,只不過使用方式不同,看自己需求
這兩個還是稍微有點差別,一個是設置資源線程等待
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
下面這個是通知形式的,可以做一些沒有返回值方法的更新UI操作
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//[tableView reloadData];
});
修改之后的輸出環境:
2016-04-07 11:22:25.251 ABNumDemo[4149:1401314] array = (
0,
1,
2,
3,
4
)
2016-04-07 11:22:25.258 ABNumDemo[4149:1401314] grouparray = (
0,
1,
2,
3,
4
)
通過以上兩種方法,或許我們已經看到想要的結果。
- 有個需要注意的地方,在我們使用以上兩者中的時候,如果使用了AF嵌套,AF缺省的時候是主線程,而我們的wait 函數也是主線程,此時會造成死鎖,平時使用的時候要注意這一點。這兩天在調試的時候發現了這個,表明一下
三、 使用block 獲取異步回調的結果
- 我們還是看代碼,直觀明了:
- (void)queryTheDataRequestWithCompletion:(void (^)(NSString * string))block{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
block(@"test-block");
});
}
- (void)requestDataFormServe:(CallBlock)block{
block(@{@"name":@"raybon"});
}
調用這兩個方法:
[self requestDataFormServe:^(NSDictionary *dict) {
NSString * name = dict[@"name"];
NSLog(@"name = %@",name);
}];
[self queryTheDataRequestWithCompletion:^(NSString *string) {
NSLog(@"string = ^%@",string);
}];
接著我們看下輸出控制臺的結果:
2016-04-07 11:33:25.153 ABNumDemo[4155:1402837] name = raybon
2016-04-07 11:33:25.160 ABNumDemo[4155:1402877] string = ^test-block
這種采用block的方法,我覺得是不是更好理解呢,還好用,但是這個要針對不同的情況采取不同的方法,并不是固定的,這里只是提供幾種思路而已,有更好的想法都可以留言增加上去,技術么,當然是越優化越好。
補充一、dispatch_semaphore_t 控制請求
- 增加一個通過源控制的請求,也是一個線程安全的,這個用的還是GCD的API,我們用代碼分析
```
//創建一個源 DISPATCH_SOURCE_TYPE_DATA_ADD 源類型是增加數據
dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));
//設置事件回調
dispatch_source_set_event_handler(source, ^{
//數據執行完畢,通知我更新UI操作
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"## 我已經收到數據,開始更新UI操作 ##");
dispatch_source_cancel(source);
});
});
//source 默認處于suspend 暫停 狀態 ,我們要執行 dispatch_resume 繼續執行
dispatch_resume(source);
dispatch_async(dispatch_get_main_queue(), ^{
//此處是網絡請求,請求到數據之后,我們更新一個源
NSLog(@"## 通知數據合并更新 ##");
dispatch_source_merge_data(source, 1);
});
unsigned long num = dispatch_source_get_data(source);
NSLog(@"data = %ld",num);
//得到dispatch 源 ,獲取的是dispatch_source_create 的第三個參數,
unsigned long num1 = dispatch_source_get_mask(source);
//獲取dispatch 源 獲取的是dispatch_create 的第二個參數位置的信息
unsigned long num2 = dispatch_source_get_handle(source);
// 源取消時的回調,用于關閉流處理
dispatch_source_set_cancel_handler(source, ^{
NSLog(@"## 如果source 被取消,則執行這個函數 ##");
});
//檢測源是否取消,如果是非0 則表示取消
long cancelNum = dispatch_source_testcancel(source);
//可用于源啟動時調用block,調用完成后釋放這個block,也可以在源執行中調用這個block
dispatch_source_set_registration_handler(source, ^{
NSLog(@"我是注冊收到的回調");
});
```
* 這里我們看一下API的作用
該API主要是監聽數據更新完畢的block回調,最簡單的方法就是網絡請求完畢需要更新UI的時候,我們會在這里面執行block
```
void
dispatch_source_set_event_handler(dispatch_source_t source,
dispatch_block_t handler);
* 合并數據源的API
dispatch_source_merge_data(source, 1);
```
該API主要提供數據更新完畢時候調用一次,第二個參數不能設置為0, 否則block無反應
* 其他API就是取消源的API,我們不用的時候可以取消掉。
總結:
以上幾種方法適合我們在做一些簡單的遍歷操作的時候挺適合的,針對請求的一些異步數據,還是提醒一點,使用這個一定要注意是否有嵌套主線程,造成線程阻塞,只要思路清晰,處理這些問題還是沒問題的
- 最后我們擴充一個資源共享的問題,同樣我們還是使用信號量控制
這個是線程安全的,每次只允許一個線程進行讀寫操作,遇到讀寫問題可以參考這里哦
//這里我們指定一個資源 wait 返回值就不會為0 執行發出信號操作
dispatch_semaphore_t semat = dispatch_semaphore_create(1);
NSMutableArray * array = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i=0; i<1000; i++) {
dispatch_semaphore_wait(semat, DISPATCH_TIME_FOREVER);
[array addObject:[NSNumber numberWithInt:i]];
dispatch_semaphore_signal(semat);
}
});
- 補充:
使用信號量控制AF請求,會遇到阻塞,我們這個時候除了使用group 信號控制,這里推薦葉神提供的 runloopObserve ,簡單理解就是通過runloop 的進入,到runloop 的結束,釋放信號,這個暫且未研究完畢,完畢研究會繼續追加此文。也希望有使用到這個的大神,直接留言,會在此文追加。