理解AFNetworking中的Runloop
最近大家都开始用AFNetworking,今天看了下它网络请求的代码,采用的也是NSOperation+NSURLConnetion并发模型。一般使用这种模型都要解决一个问题: NSURLConnection对象在下载完前,所在线程就退出了,NSOperation对象也就接收不到回调。
这个问题在stackoverflow上已经讨论了N多次了,其原因也在apple的guide line上写的很清楚了:NSURLConnection的delegate方法需要在connection发起的线程的runloop中调用。因此,当发起connection的线程exit了,delegate自然不会被调用,请求也就回不来了。
针对这个问题,通常有这么两种解法:
- 所有connection在主线程的runloop中发起,回调也都由主线程的runloop分发:
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
[_connection scheduleInRunLoop:runLoop forMode:NSRunLoopCommonModes];
[_connection start];
- 让发起请求的线程不退出,通过内置一个runloop来实现
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSPort port] forMode:NSRunLoopCommonModes];
[_connection scheduleInRunLoop:runLoop forMode:NSRunLoopCommonModes];
[_connection start];
[runLoop run];
这两种方法都不可取!
第一种方法是出于性能考虑,当并发的请求很多时,需要大量占用main runloop,会影响GUI性能。
第二种方法问题更大,connection虽然可以顺利完成,但由于线程一直被runloop占据,导致线程永远无法停止,线程池直接失去了对线程的控制,而由于线程无法退出,它的stackframe中引用的NSOperation对象也无法释放,并发数量上去后,无论是CPU的资源还是内存上,都会有问题。
AFNetworking解决这个问题采用了另一种方法:单独起一个global thread,内置一个runloop,所有的connection都由这个runloop发起,回调也都由它接收。这是个不错的想法,既不占用主线程,又不耗CPU资源:
[self performSelector:@selector(operationDidStart)
onThread:[[self class] networkRequestThread] withObject:nil
waitUntilDone:NO
modes:[self.runLoopModes allObjects]]
线程代码:
+ (void) __attribute__((noreturn)) networkRequestThreadEntryPoint:(id)__unused object {
do {
@autoreleasepool {
[[NSRunLoop currentRunLoop] run];
}
} while (YES);
}
+ (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最早想出来的,是apple的一个demo:MVCNetworking,AFNetworking简单粗暴的借鉴了这个demo。
不论是AFNetworking还是MVCNetworking,保持runloop的代码均为:
do {
@autoreleasepool
{
[[NSRunLoop currentRunLoop] run];
}
} while (YES);
另一种优雅的方式是,直接塞一个input source:
@autoreleasepool
{
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
更多关于Runloop的基础知识清参考前一篇文章