iOS多個網絡請求同步執行

這裏所說的同步執行是指多個網絡請求按順序執行,但這些請求還是可以在異步線程處理的不會阻塞主線程;首先我們看一個實際的應用場景:
登錄時發起請求A,請求成功後得到返回的用戶信息;根據得到的信息作爲參數再發送網絡請求B;這裏就要求網絡請求B必須是在網絡請求A回調後再調用:A--->B;
類似的需求,在實際開發中或多或少會碰到;現在我們就來處理這種需求

NSURLConnection

NSURLConnection封裝了類似的同步請求功能:

@interface NSURLConnection (NSURLConnectionSynchronousLoading)

/*! 
    @discussion
                 A synchronous load for the given request is built on
                 top of the asynchronous loading code made available
                 by the class.  The calling thread is blocked while
                 the asynchronous loading system performs the URL load
                 on a thread spawned specifically for this load
                 request. No special threading or run loop
                 configuration is necessary in the calling thread in
                 order to perform a synchronous load. For instance,
                 the calling thread need not be running its run loop.
….
*/
+ (nullable NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse * _Nullable * _Nullable)response error:(NSError **)error API_DEPRECATED("Use [NSURLSession dataTaskWithRequest:completionHandler:] (see NSURLSession.h", macos(10.3,10.11), ios(2.0,9.0), tvos(9.0,9.0)) API_UNAVAILABLE(watchos);

@end

以上A、B請求同步執行的簡單示例:

    NSURLRequest *rqs = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://github.com"]];
    NSData *githubData = [NSURLConnection sendSynchronousRequest:rqs returningResponse:nil error:nil];
    NSLog(@"A業務,%@",githubData);
    rqs = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.jianshu.com"]];
    NSData *jianshuData = [NSURLConnection sendSynchronousRequest:rqs returningResponse:nil error:nil];
    NSLog(@"B業務,%@",jianshuData);
NSURLSession

使用NSURLConnection很方便的實現了同步請求,但這套API老早就已經DEPRECATED,需要使用NSURLSession代替;
但是NSURLSession並沒有提供類似sendSynchronousRequest的接口;那麼,使用NSURLSession實現同步請求的一個簡單方式就是在A請求成功回調裏再調用B請求;類似代碼如下:

NSURLSession *session = [NSURLSession sharedSession];
[[session dataTaskWithURL:[NSURL URLWithString:@"https://github.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    NSLog(@"A業務,%@",data);
    [[session dataTaskWithURL:[NSURL URLWithString:@"https://www.jianshu.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        NSLog(@"B業務,%@",data);
    }] resume];
}] resume];

如果我們的業務不只是A、B同步,而是A--->B--->C---D,那就需嵌套更多層,形成了Callback Hell;這樣的代碼是很醜陋的,可讀性、維護性都比較差;
接下來,我們就自己動手來實現類似NSURLConnection的同步請求的方法;
iOS同步方案有很多種,我之前的文章都有詳細介紹:
細數iOS中的線程同步方案(一)
細數iOS中的線程同步方案(二)
這裏我們選用性能較高的GCD信號量semaphore;

@implementation NSURLSession (Sync)

+ (nullable NSData *)sendSynchronousURL:(NSURL *)url returningResponse:(NSURLResponse * _Nullable __strong * _Nullable)response error:(NSError * __strong *)error {
    __block NSData *data;
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    NSURLSession *session = [NSURLSession sharedSession];
    [[session dataTaskWithURL:url completionHandler:^(NSData * _Nullable tData, NSURLResponse * _Nullable tResponse, NSError * _Nullable tError) {
        // 子線程
        *response = tResponse;
        *error = tError;
        data = tData;
        dispatch_semaphore_signal(sem);
    }]resume];
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    return data;
}

@end

這裏給NSURLSession添加了分類,封裝了和NSURLConnection中同步請求一樣的方法;需要注意的是,方法參數中response和error前都加了__strong內存修飾符,這是因爲如果沒有明確指定內存的修飾符(strong, weak, autoreleasing),類似NSURLResponse **reponse,NSError **error的臨時變量,在ARC下編譯器會默認添加__autoreleasing內存修飾符,而在block中捕獲一個__autoreleasing的out-parameter很容易造成內存問題;Xcode會提示警告信息Block captures an autoreleasing out-parameter, which may result in use-after-free bugs
這篇博客詳細解釋了原因:https://www.cnblogs.com/tiantianbobo/p/11653843.html

封裝好之後,A、B同步請求的代碼就可以這樣寫了:

NSData *githubData = [NSURLSession sendSynchronousURL:[NSURL URLWithString:@"https://github.com"] returningResponse:nil error:nil];
NSLog(@"A業務,%@",githubData);
NSData *jianshuData = [NSURLSession sendSynchronousURL:[NSURL URLWithString:@"https://www.jianshu.com"] returningResponse:nil error:nil];
NSLog(@"B業務,%@",jianshuData);
AFNetworking

實際開發中,大部分人應該都是使用AFNetworking處理網絡請求的;AFNetworking3.x其實也是對NSURLSession的封裝;
AFNetworking中貌似也沒有提供類似sendSynchronousURL同步請求的方法;按照之前NSURLSession的思路,現在同樣使用GCD信號量實現AFNetworking的同步請求:

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
[manager GET:@"https://github.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    NSLog(@"A業務,%@",responseObject);
    dispatch_semaphore_signal(sem);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    NSLog(@"%@", error.localizedDescription);
    dispatch_semaphore_signal(sem);
}];

dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
[manager GET:@"https://www.jianshu.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    NSLog(@"B業務,%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    NSLog(@"%@", error.localizedDescription);
}];

如果你在主線程運行以上代碼,會發現什麼都不會輸出;這是因爲AFNetworking回調默認是主線程,這樣dispatch_semaphore_wait和dispatch_semaphore_signal在同一個線程,這樣就死鎖了;所以以上代碼需要放在異步線程執行,類似:

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        ......
    });
AFNetworking和線程有關的屬性

在AFURLSessionManager.h中,可以看到以下3個和線程有關的屬性

/**
 The operation queue on which delegate callbacks are run.
 */
@property (readonly, nonatomic, strong) NSOperationQueue *operationQueue;

/**
 The dispatch queue for `completionBlock`. If `NULL` (default), the main queue is used.
 */
@property (nonatomic, strong, nullable) dispatch_queue_t completionQueue;

/**
 The dispatch group for `completionBlock`. If `NULL` (default), a private dispatch group is used.
 */
@property (nonatomic, strong, nullable) dispatch_group_t completionGroup;
  • operationQueue
    NSURLSession回調的隊列,創建session時使用;AFN源碼:
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;

self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

NSURLSession的delegateQueue設置爲[NSOperationQueue mainQueue]則session回調就是主線程,[[NSOperationQueue alloc] init]則會是子線程

[[session dataTaskWithURL:[NSURL URLWithString:@"https://github.com"] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
    // 主線程 or 子線程
}]resume];

([NSURLSession sharedSession]創建的session的delegateQueue爲主隊列)

  • completionQueue
    AFN回調的隊列,默認主隊列;前面GCD信號量同步執行的方法,可以指定completionQueue爲異步隊列則可以在主線程調用:
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
// 指定回調隊列
manager.completionQueue = dispatch_queue_create("sync_request", DISPATCH_QUEUE_SERIAL);
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
[manager GET:@"https://github.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    NSLog(@"A業務,%@",responseObject);
    dispatch_semaphore_signal(sem);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    NSLog(@"%@", error.localizedDescription);
    dispatch_semaphore_signal(sem);
}];

dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
[manager GET:@"https://www.jianshu.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    NSLog(@"B業務,%@",responseObject);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    NSLog(@"%@", error.localizedDescription);
}];
  • completionGroup
    直接看源碼:
- (void)URLSession:(__unused NSURLSession *)session
              task:(NSURLSessionTask *)task
didCompleteWithError:(NSError *)error
{
    ...
    dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
        if (self.completionHandler) {
            self.completionHandler(task.response, responseObject, serializationError);
        }

        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];
        });
    });
    ...
}

NSURLSession請求回調裏,使用了GCD group處理AFN自己的回調;使用group的意圖,應該是爲了處理批量網絡請求;提供的completionGroup就是供開發者方便調用dispatch_group_wait或dispatch_group_notify實現所有請求完成後之後的操作;
如C請求需要使用A、B返回的數據作爲參數請求,那麼C需要等A、B都完成後才執行,且A、B都是異步;即 A & B ---> C;
代碼實現:

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
dispatch_group_t group = dispatch_group_create();
manager.completionGroup = group;
[manager GET:..]; // A請求
[manager GET:...]; // B請求

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"AB業務完成");
});

但是,實際運行結果並不正確;@"AB業務完成"會立即執行;這是因爲dispatch_group_async是在NSURLSession代理回調中調用的,即需要等到異步請求有結果時才調用,因此在調用dispatch_group_notify時,group內並沒有任務所有無需等待;
這樣的問題github上有類似的issue:https://github.com/AFNetworking/AFNetworking/issues/1926

對於completionGroup暫時還沒理解它的用途是什麼;

如果想要實現以上功能,可以自己開個group,通過dispatch_group_enter、dispatch_group_leave來控制同步;

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.responseSerializer = [AFHTTPResponseSerializer serializer];
dispatch_group_t group = dispatch_group_create();
[manager GET:@"https://github.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    NSLog(@"A業務,%@",responseObject);
    dispatch_group_leave(group);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    NSLog(@"%@", error.localizedDescription);
    dispatch_group_leave(group);
}];
dispatch_group_enter(group);

[manager GET:@"https://www.jianshu.com" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
    NSLog(@"B業務,%@",responseObject);
    dispatch_group_leave(group);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
    NSLog(@"%@", error.localizedDescription);
    dispatch_group_leave(group);
}];
dispatch_group_enter(group);

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    NSLog(@"AB業務完成");
});

輸出結果:

B任務,{length = 39986, bytes = 0x3c21444f 43545950 45206874 6d6c3e0a ... 3c2f6874 6d6c3e0a }
A任務,{length = 120516, bytes = 0x0a0a0a0a 0a0a3c21 444f4354 59504520 ... 2f68746d 6c3e0a0a }
AB業務完成
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章