AFNetworking多线程分析
AFNetworking是目前最常用的iOS的网络开发框架。它是对Apple系统提供的网络框架(NSURLConnection和NSURLSession)的上层封装。本文针对AFNetworking的2x版本的NSURLConnection的网络请求的工作流程做一个简单的梳理。
NSOperation介绍
在介绍AFNetworking的多线程流程前需要先了解一下NSOperation的基本知识。
提到并发相关的概念,可能会想到一些关键字,比如线程,锁等等。iOS开发中也有对应的支持,比如 NSThread,NSLock 等。但是直接使用这些组件来做并发会很复杂,也极易引入并发相关的bug并且难以调试,尤其是死锁相关的问题。因此Apple自己封装了这些低层次的组件,提供了更加易于的并发模型,即GCD和NSOperation。
GCD和NSOperation都是基于任务队列的并发模型。使用者不必要关系线程和锁,只需要将要执行的代码block放入到任务队列即可。GCD是一个轻量级的框架,提供的功能相对简单但是高效。NSOperation是在GCD基础之上封装的一个功能更加强大的框架。提供ObjectC风格的API,相比于GCD,它提供如下额外的功能:
- 设置任务队列中任务的依赖关系,比如任务A在任务B执行后执行
- 可以cancel或者suspend一个任务
- NSOperation支持KVO
相比于GCD,NSOperation在性能开销上有一些增加。但是如果要用到一些高级的功能,这些开销也是值得的。
例如,我们需要创建一个任务来输出‘Hello world’,只需要实现如下几步:
- 继承系统的NSOperation
- 覆盖main方法
- 在main方法中实现代码逻辑
代码如下:
#import <Foundation/Foundation.h>
@interface MyOperation: NSOperation
@end
@implementation MyOperation
- (void)main {
@autoreleasepool {
NSLog(@"Hello world");
}
}
@end
具体的API可以参考Concurrency Programming Guide
NSURLConnection
NSURLConnection是Apple提供的网络框架。调用者不需要关心底层的socket实现,只需要调用上层API即可。
NSURLConnection提供两种类型的网络请求API,一种是同步API,一种是异步API。签名如下:
// 同步请求
sendAsynchronousRequest:queue:completionHandler:
// 异步请求
sendSynchronousRequest:returningResponse:error:
@end
同步请求会挂起当前线程,而异步请求会在调用完成后离开返回,当网络请求完成后会执行相应的回调。AFNetworking中使用的是异步请求的API来实现网络请求。
系统框架
AFNetworking中网络请求的基本框架如下:
从上图可以看到,系统维护了一个NSOperationQueue。每一次请求都会构建一个对应的NSOperation,然后加入到该queue中。当该operation开始执行时(调用start方法),它在一个名为AFNetworking的线程中创建了一个NSURLConnection的实例,开始进行实际的网络请求。
流程分析
外层API
下面是一个使用AFNetworking的常用例子:
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
[manager GET:@"http://example.com/resources.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(@"JSON: %@", responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(@"Error: %@", error);
}];
其中 manager 的GET方法实现如下:
- (AFHTTPRequestOperation *)GET:(NSString *)URLString
parameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure
{
NSMutableURLRequest *request = [self.requestSerializer requestWithMethod:@"GET" URLString:[[NSURL URLWithString:URLString relativeToURL:self.baseURL] absoluteString] parameters:parameters error:nil];
AFHTTPRequestOperation *operation = [self HTTPRequestOperationWithRequest:request success:success failure:failure];
[self.operationQueue addOperation:operation];
return operation;
}
这段代码创建了一个AFHTTPRequestOperation的实例,然后放到了self.operationQueue中。其中AFHTTPRequestOperationManager就继承自AFURLConnectionOperation,AFURLConnectionOperation是NSOperation的子类,它实现了NSURLConnection的大部分逻辑,包括Request的发送和Response的处理以及所有的NSURLConnection的Delegate。
窥探AFURLConnectionOperation
在AFHTTPRequestOperation加入到了operationQueue后系统内部到底发生了什么呢?这个时候,其实执行了NSOperation的start方法:
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
上述代码在首先检查了内部的状态,如果处于Ready状态,则在某个线程中执行方法operationDidStart。该线程的创建代码如下:
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
这是个单例方法,创建了一个全局唯一的线程,线程名字为AFNetworking。该线程用于实际的网络请求的处理。而方法operationDidStart的内容如下:
- (void)operationDidStart {
[self.lock lock];
if (![self isCancelled]) {
// 创建NSURLConnection的实例
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
for (NSString *runLoopMode in self.runLoopModes) {
[self.connection scheduleInRunLoop:runLoop forMode:runLoopMode];
[self.outputStream scheduleInRunLoop:runLoop forMode:runLoopMode];
}
[self.connection start];
}
[self.lock unlock];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingOperationDidStartNotification object:self];
});
}
可以看出,在上述方法中系统实际的创建了NSURLConnection的实例,然后执行其start方法,开始实际的网络请求。
注意这里创建NSURLConnection的代码和我们平常使用的不同。它首先初始化一个非立即执行的实例,然后设置其运行的runLoop,最后才调用start方法开始执行。每一个NSThread都有一个NSRunLoop,可以通过方法currentRunLoop来获取。默认情况下NSURLConnection会在RunloopMode为NSDefaultRunLoopMode时执行。这样当用户滑动ScrollView等操作时RunLoopMode为NSEventTrackingRunLoopMode,这样NSURLConnection相关的回调不会立即执行。因此上述代码手动的设置了RunLoopMode。
如果需要了解RunLoop的详细知识,请参考如下博客:
在执行完上述代码后,AFNetworking就交给Apple的NSURLFoundation来处理实际的请求。NSURLFoundation每个版本的实现可能不一样,内部也会创建自己的线程。不过对于使用者而言,只需要关心发送请求和处理响应的回调即可。接下来发生的就是NSURLConnection的一系列Delegate的回调。
Completion Block的踪迹
在创建AFHTTPRequestOperation时会设置success和failure的回调。这些回调会在网络请求完成后触发,使用者大多只需要关心这些回调就行。那些这些回调又是在什么时候执行的呢?
构建AFURLRequestOperation时创建的回调block通过如下方法传入:
- (void)setCompletionBlockWithSuccess:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure {
self.completionBlock = ^{
// 执行success和failure block
}
}
上述方法是设置了self.completionBlock,该block中会根据执行的网络请求的结果来执行传入的success或者failure的回调。设置self.completionBlock会调用如下方法:
- (void)setCompletionBlock:(void (^)(void))block {
[self.lock lock];
if (!block) {
[super setCompletionBlock:nil];
} else {
__weak __typeof(self)weakSelf = self;
// 调用NSOperation的setCompletionBlock
[super setCompletionBlock:^ {
__strong __typeof(weakSelf)strongSelf = weakSelf;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
dispatch_group_t group = strongSelf.completionGroup ?: url_request_operation_completion_group();
dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
#pragma clang diagnostic pop
dispatch_group_async(group, queue, ^{
block();
});
dispatch_group_notify(group, url_request_operation_completion_queue(), ^{
[strongSelf setCompletionBlock:nil];
});
}];
}
[self.lock unlock];
}
上述的代码其实是调用了NSOperation的setCompletionBlcock方法。传入的block会在什么时候执行呢?其实会在NSOperation执行完finish之后。调用finish方法的代码如下:
- (void)connectionDidFinishLoading:(NSURLConnection __unused *)connection {
self.responseData = [self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey];
[self.outputStream close];
if (self.responseData) {
self.outputStream = nil;
}
self.connection = nil;
// 调用NSOperation的finish方法
[self finish];
}
它是在NSURLConnection的Delegate方法中调用。当网络请求完成后,connectionDidFinishLoading的Delegate就会执行,该方法中会调用NSOperation的finish方法,从而开始执行self.completionBlock。该block中就有外层传入的success和failure的回调。代码如下
dispatch_queue_t queue = strongSelf.completionQueue ?: dispatch_get_main_queue();
该block会优先放到self.completionQueue,如果该值为空,则会放到main queue中。这也解释了Stack Overflow中的如下问题:
Are AFNetworking success/failure blocks invoked on the main thread?
默认情况下,self.completionQueue是空,因此success和failure的回调是在主线程中执行。所有尽量不要在这些回调中执行过多占用CPU的代码。如果这些回调确实需要占用CPU,则建议创建一个单独的任务队列,并赋值给afnetworking的completionQueue属性。
总结
- 模型分析
AFNetworking并没有为每一个请求创建一个线程,而是将每个请求封装成一个NSOperation放到一个queue中。但是每当该operation执行时,它都会在一个单独的线程(AFNetworking)中创建NSURLConnection对象,并监听所有的回调。由于网络请求都是采用NSURLConnection或者NSURLSession的异步API,因此一个单一的处理线程已经可以满足需要。
总而言之,AFNetworking其实是采用了NSOperationQueue+NSURLFoundation的异步API来完成高效的网络请求。在具体的实现细节上,有很多地方值得学习和借鉴。
- 并发粒度
AFNetworking所有的网络请求都是放到了NSOperationQueue中,而该queue会有多个并发的线程来执行。默认情况下系统会根据硬件的条件,比如CPU的核心数等来设置并发的线程数,我们也可以手动的设置该变量。
[[self.requestOperationManager operationQueue] setMaxConcurrentOperationCount:2];
上述代码手动的设置了并发的线程数为2。
因此,如果我们需要所有的网络请求按照创建的顺序序列的执行则可以设置setMaxConcurrentOperationCount为1。
- completion block优化
如果某个operation的success和failure的回调占用较多的CPU,那么可以创建一个任务队列并赋值给该operation的completionQueue。