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业务完成
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265