基于AFNetworking 3.0的NSURLSessionDataTask Batch Request

本文要实现的功能

AFNetworking 3.0之前,AFNetworking提供了这样一个方法:

1
2
3
+ (NSArray *)batchOfRequestOperations:(NSArray *)operations
progressBlock:(void (^)(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations))progressBlock
completionBlock:(void (^)(NSArray *operations))completionBlock;

该方法可以并行执行一组操作请求,注意是并行哦,等所有的请求都执行结束后,执行completionBlock。但是在3.0以后AFNetworking还没有给提供一个这样的方法,所以本文就是要在3.0的基础上,来封装一个具有上面这个方法的功能的方法。也就是对一组NSURLSessionDataTask的Batch Request的封装。

实现过程

主要是利用dispatch_group_tdispatch_group_notify来实现。先来看实现的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
- (void)concurrentBatchOfRequestOperations:(NSArray *)operations progressBlock:(void (^)(NSUInteger, NSUInteger))progressBlock completionBlock:(void (^)(NSArray *))completionBlock {

if (!operations || operations.count == 0) {
dispatch_async(dispatch_get_main_queue(), ^{
if (completionBlock) {
completionBlock(@[]);
}
});
return;
}

NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
AFURLSessionManager *manager = [[AFURLSessionManager alloc]initWithSessionConfiguration:configuration];
AFHTTPResponseSerializer *responseSerializer = [AFHTTPResponseSerializer serializer];
responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html", @"application/json", nil];
manager.responseSerializer = responseSerializer;

dispatch_group_t group = dispatch_group_create();

NSMutableArray *tasks = @[].mutableCopy;

for (HTTPRequestOperation *operation in operations) {
dispatch_group_enter(group);
NSURLSessionDataTask *task = [manager dataTaskWithRequest:operation.request uploadProgress:nil downloadProgress:nil completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) {

if (operation.completionBlock) {
operation.completionBlock(response, responseObject, error);
}

NSUInteger numberOfFinishedTasks = [[tasks indexesOfObjectsPassingTest:^BOOL(NSURLSessionDataTask * obj, NSUInteger idx, BOOL * _Nonnull stop) {

return (obj.state == NSURLSessionTaskStateCompleted);

}] count];

if (progressBlock) {
progressBlock(numberOfFinishedTasks, [tasks count]);
}

dispatch_group_leave(group);
}];

[tasks addObject:task];
}

for (NSURLSessionDataTask *task in tasks) {
[task resume];
}

dispatch_group_notify(group, dispatch_get_main_queue(), ^{
if (completionBlock) {
completionBlock(tasks);
}
});
}

这里面传进来的operation,只是我自定义的一个简单的封装requestcompletionBlock的类,如下:

1
2
3
4
5
6
7
8
9
10
//简单封装request和completionBlock
@interface HTTPRequestOperation : NSObject

@property (nonatomic, strong, readonly) NSURLRequest *request;

@property (nonatomic, copy) void (^completionBlock)(NSURLResponse *response, id responseObject, NSError * error);

- (instancetype)initWithRequest:(NSURLRequest *)request;

@end

原本想直接让调用者传dataTask进来,但是我查了文档,dataTask无法存储completionBlock,所以干脆自己弄一个类来封装需要的这些东西,然后自己来创建dataTask

使用说明

创建一组上面自定义的HTTPRequestOperation

1
2
3
4
5
6
7
8
9
10
11
		
//创建单个operation
NSMutableURLRequest *request = [[AFHTTPRequestSerializer serializer] requestWithMethod:@"GET" URLString:urlStr parameters:nil error:nil];
HTTPRequestOperation *operation = [[HTTPRequestOperation alloc]initWithRequest:request];
[operation setCompletionBlock:^(NSURLResponse *response, id responseObject, NSError *error) {
if (error) {
//该请求失败
} else {
//该请求成功
}
}];

假如已经创建好了个一组mutableOperations,则调用如下:

1
2
3
4
5
[[HTTPClient defaultClient] concurrentBatchOfRequestOperations:mutableOperations progressBlock:^(NSUInteger numberOfFinishedTasks, NSUInteger totalNumberOfTasks) {

} completionBlock:^(NSArray *dataTasks) {

}];

进阶:让请求串行执行

要控制异步请求能串行的去执行,那就要用到dispatch_semaphore_t了,此处不粘贴全部代码了,只需要对上面的concurrentBatchOfRequestOperations稍加修改即可。

1) 在for循环的上面创建信号量,在最开始时先将信号量设置为0。

1
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

2) 在任务执行后,调用dispatch_semaphore_signal,让信号量加1。这样刚被阻塞的线程就会变得活跃,可以继续执行后面的任务。所以在dataTaskWithRequestcompletionHandler中最下面一句。

1
2
3
	
dispatch_group_leave(group);
dispatch_semaphore_signal(semaphore);

3) 在开启一个任务后,执行dispatch_semaphore_wait,若此时信号量为0,所以再来访问的线程会被阻塞,等待当前任务执行结束。若此时信号量不为0,会将信号量减1,就又变为了0,再有线程过来访问时就又会被阻塞等待。

1
2
3
4
for (NSURLSessionDataTask *task in tasks) {
[task resume];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}

于是,这一组放到并行队列中的任务就串行执行了。

知识延伸

由于也是第一次来使用GCD中的groupsemaphore,刚开始也比较迷惑,翻了官方的文档,看了好多博客文章,现在想来自己总结一下他们的用途和用法。

dispatch_group_t

如果你想在一组block执行完之后,再去干其他的事情,那么GCD的group可以帮到你了。这一组中的block可以同步执行,也可以异步执行,它们可以运行在不同的队列中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//创建group
dispatch_group_t group = dispatch_group_create();

//创建用来调度任务的queue
dispatch_queue_t q = dispatch_get_global_queue(0, 0);

//添加任务
dispatch_group_async(group, q, ^{
//任务1
});
dispatch_group_async(group, q, ^{
//任务2
});

//等到所有的任务完成后,切换到`main queue`进行操作
dispatch_group_notify(group, dispatch_get_main_queue(), ^{

});

也可以用dispatch_group_enter(group);dispatch_group_leave(group);给group中添加任务。手动指定一个任务什么时候添加进组里,什么时候离开组。在本例中用的就是这一对好基友,因为NSURLSessionDataTask的执行本身就是异步的了,所以不用再指定一个队列来调度他们了。

dispatch_semaphore_t

如果你想让一组异步执行的任务,一个完成后再开始下一个任务,那么GCD中的semaphore可以帮到你了。如果要保证某一数据在多线程的情况下是数据安全的,那么应该是这家伙的最大的用武之地了。

1
2
3
4
5
6
7
8
//创建semaphore
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

//发出一个semaphore,将semaphore的数量加1
dispatch_semaphore_signal(semaphore);

//等待semaphore,将semaphore的数量减1
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

GCD 串行队列、并行队列、同步调度、异步调度

  • 串行队列中的任务是按照FIFO的规则执行的,如果其中的任务是异步调度的,则当前线程不会等待任务执行结束,会马上执行后面的语句。如果是同步调度的,则当前线程要等待任务执行完后,再执行后面的语句,也就是当前线程被阻塞。

下面想说说既然串行队列已经可以保证任务是按照顺序执行的了,那为啥还要用dispatch_semaphore_t呢?因为NSURLSession本身就是并行的!下面的代码简单演示了在串行队列中添加的任务被异步调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
NSLog(@"current thread:%@", [NSThread currentThread]);

dispatch_queue_t queue = dispatch_queue_create("串行队列", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"任务1 In Thread:%@", [NSThread currentThread]);
});

dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"任务2 In Thread:%@", [NSThread currentThread]);
});

dispatch_async使得添加到该queue中的block块是异步执行的,又由于是一个串行的queue,所以block块是在新开的线程上是顺序执行的。但是如果block中又接着添加任务到一个并行队列中,那该操作的执行时机就不受该queue的影响啦~如下:

1
2
3
4
5
6
7
8
9
dispatch_async(queue, ^{
[NSThread sleepForTimeInterval:3];
NSLog(@"任务1 In Thread:%@", [NSThread currentThread]);
// 全局队列是并行的
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSThread sleepForTimeInterval:5];
NSLog(@"block块任务 In Thread:%@", [NSThread currentThread]);
});
});

NSURLSessionDataTask就相当于block块里的添加到全局队列中的任务,所以它的执行顺序只用串行队列还是没办法保障的。

以上。