iOS 多個異步網絡請求全部返回后再執行具體邏輯的方法


提起ios中多個異步函數后的同步問題,自然會想到 dispatch group 這個概念,那么它能夠解決異步網絡請求的問題嗎?

對於dispatch多個異步操作后的同步方法,以前只看過dispatch_group_async,看看這個方法的說明:

 * @discussion
 * Submits a block to a dispatch queue and associates the block with the given
 * dispatch group. The dispatch group may be used to wait for the completion
 * of the blocks it references.

可以看出,dispatch_group_async,是用於同步工作的,但是,它的判斷標准是放入的block是否執行完畢,如果我們放入block中包含異步的網絡請求,這個方法無法在網絡數據返回后再進行同步。

看一段使用dispatch_group_async處理網絡問題的代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    NSURLSession *session = [NSURLSession sharedSession];
    
    dispatch_queue_t dispatchQueue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t dispatchGroup = dispatch_group_create();
    dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
        
        NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"got data from internet1");
        }];
        [task resume];
        
        
        
    });
    dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
        NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"got data from internet2");
        }];
        [task resume];
    });
    
    dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
        NSLog(@"end");
    });
}

看看log的輸出

2016-07-13 17:42:55.170 aaaa[4797:295528] end
2016-07-13 17:42:55.322 aaaa[4797:295574] got data from internet2
2016-07-13 17:42:55.375 aaaa[4797:295574] got data from internet1

完全沒有達到效果。這是因為這里的網絡請求是個異步的方法,沒有等待具體的數據返回,放入的dispatch queue的 block就執行完畢了。所以沒收到2個網絡數據,就提前調用了dispatch_group_notify指定的結束方法。

看完了錯誤的方法,再看看正確的方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    NSURLSession *session = [NSURLSession sharedSession];
    
    dispatch_queue_t dispatchQueue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_group_t dispatchGroup = dispatch_group_create();
    // dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
    
    dispatch_group_enter(dispatchGroup);
    
    NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"got data from internet1");
        
        dispatch_group_leave(dispatchGroup);
    }];
    [task resume];
    
    //  });
    //  dispatch_group_async(dispatchGroup, dispatchQueue, ^(){
    dispatch_group_enter(dispatchGroup);
    
    NSURLSessionDataTask *task2 = [session dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"got data from internet2");
        
        dispatch_group_leave(dispatchGroup);
    }];
    [task2 resume];
    // });
    
    dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(), ^(){
        NSLog(@"end");
    });
}

看正確是輸出結果:

2016-07-13 17:46:10.282 aaaa[4847:300370] got data from internet1
2016-07-13 17:46:10.501 aaaa[4847:300370] got data from internet2
2016-07-13 17:46:10.502 aaaa[4847:300341] end

 相對於簡單的dispatch_group_async,dispatch_group_enter 和 dispatch_group_leave 可以對group進行更細致的處理。

我們看看關於dispatch_group_enter 的具體說明

Calling this function increments the current count of outstanding tasks in the group. Using this function (with dispatch_group_leave) allows your application to properly manage the task reference count if it explicitly adds and removes tasks from the group by a means other than using the dispatch_group_async function. A call to this function must be balanced with a call to dispatch_group_leave. You can use this function to associate a block with more than one group at the same time.

簡單的說,就是dispatch_group_enter會對group的內部計數加一,dispatch_group_leave會對group的內部計數減一,就類似以前的retain和release方法。說白了也是維護了一個計數器。

以前我的做法就是自己維護計數器。在發送網絡請求前,記下發送總數,數據返回后,在同一個thread中(或者在一個DISPATCH_QUEUE_SERIAL類型的dispatch_queue中),對計數器進行+1操作,當計數器和網絡請求數相等時,調用最后的處理。

相比自己的處理的計數器,dispatch_group_enter 處理方法可能顯得更正規一些,代碼更規范了,但執行效果是一樣的。。。

 

今天再改其他的工程的時候,又遇到了這個問題,有一個值,需要2個異步操作查詢回2個值進行計算,因此必須再2個異步操作結束后才能進行計算操作。開始試着使用了OperationQueue,想用addDependency方法,但是這個方法無法靈活地控制,只適合block內容已經確定的情況。對於我遇到的這種異步操作,block的內容是不定的,需要依賴異步的返回,用operation queue會遇到各種問題,無法解決問題,十分復雜!

 

 


 

今天看到了dispatch_barrier_async函數,說明如下

Calls to this function always return immediately after the block has been submitted and never wait for the block to be invoked. When the barrier block reaches the front of a private concurrent queue, it is not executed immediately. Instead, the queue waits until its currently executing blocks finish executing. At that point, the barrier block executes by itself. Any blocks submitted after the barrier block are not executed until the barrier block completes.

The queue you specify should be a concurrent queue that you create yourself using the dispatch_queue_create function. If the queue you pass to this function is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.

簡單地說,就是在這個函數之前被提交到quque里的block一定會被先執行,之后執行dispatch_barrier_async設定的block,最后執行調用dispatch_barrier_async之后才提交到queue里的block。

我開始以為這個函數能夠處理我們的問題,結果是不行的,先看看測試用的代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    
    NSURLSession *session = [NSURLSession sharedSession];
    
    dispatch_queue_t dispatchQueue = dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(dispatchQueue, ^{
        NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"got data from internet1");
            
        }];
        
        [task resume];
    });

    dispatch_async(dispatchQueue, ^{
        NSURLSessionDataTask *task2 = [session dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"got data from internet2");
            
        }];
        [task2 resume];
    });
   


    dispatch_barrier_async(dispatchQueue, ^{
        NSLog(@"================== barrier block is called");
    });
    
    
    dispatch_async(dispatchQueue, ^{
        NSLog(@"=========the last block is called");
    });

   
}

這段代碼的輸出結果是:

2016-07-17 14:34:46.620 aaaaa[5000:91010] ================== barrier block is called
2016-07-17 14:34:46.621 aaaaa[5000:91010] =========the last block is called
2016-07-17 14:34:46.815 aaaaa[5000:91010] got data from internet1
2016-07-17 14:34:46.866 aaaaa[5000:91014] got data from internet2

完全沒有達到2個網絡請求都返回后,再執行the last block的效果。

原因和 dispatch_group_async無法達到目的的原因是一樣的:它認為一個block返回后就是邏輯結束了,就會繼續執行其他代碼,對於block中異步返回的網絡數據,沒有對應的處理手段。

 


 

 

今天,突然想,NSUrlSession 不是用的 NSOperation Queue 嗎,能不能直接利用Operation Queue 而不是dispatch_queue  來解決這個問題呢?

我們知道NSOperation中有addDependency這個方法,我們能不能把幾個網絡請求分別封裝一下:

 [NSBlockOperation blockOperationWithBlock:^{
        NSURLSessionDataTask *task = [session dataTaskWithURL:[NSURL URLWithString:@"https://www.baidu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            NSLog(@"got data from internet1");
            
        }];
        
        [task resume];
    }];

這樣,再添加依賴,來達到效果呢?

結論是:僅僅使用NSBlockOperation來構建operation是不可以的。 這里的錯誤原因和使用dispatch_group_async是一樣的。

但是,如果把NSUrlConnection的請求封裝成NSOperation子類,使這個子類有這個效果:"當網絡數據返回時,才算這個operation的結束",就可以利用這個子類和nsoperationqueue 達到我們的目的!

(題外話:NSOperationQueue 就不同於dispatch_queue了,它沒有dispatch_queue中的並行,串行類型,但是,有個類似功能的屬性maxConcurrentOperationCount,當maxConcurrentOperationCount = 1時,自然就是串行的了。)

 

------------------------------------------------------------------------------------------------------------------

今天去面試,靈光一顯,總算總結出了使用urlsession 進行下載的通用方法,這個方法加入了對最大並發數的限制,也加入了全部完成后的回調,基本可以應對任何情況的下載了!

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(3);
    
    dispatch_group_t dispatchGroup = dispatch_group_create();
    
    for (int i = 0; i < 10; i++) {
        
        NSLog(@"i is %d",i);
        NSURLSession *session = [NSURLSession sharedSession];
        
        NSURL *url = [NSURL URLWithString:@"https://codeload.github.com/EricForGithub/SalesforceReactDemo/zip/master"];
        
        NSURLSessionDownloadTask *sessionDownloadTask =[session downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
            
            sleep(5.0);
            
            NSLog(@"release a signal");
            
           
            dispatch_semaphore_signal(semaphore);
        
dispatch_group_leave(dispatchGroup);//調用后,有可能觸發完成函數,所以應該在這之前先釋放信號量,保證在完成函數里,所有的資源都釋放了 }]; dispatch_group_enter(dispatchGroup);//為了所有下載完成后能調用函數,引入 dispatch group。如果信號量是1的話,可以不使用這個機制,也能達到效果。 dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); //為了最大並發數,加入信號量機制 [sessionDownloadTask resume]; } dispatch_group_notify(dispatchGroup, dispatch_get_main_queue(),
^(){ NSLog(@"end"); }); }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM