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业务完成
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章