AFNetworking多线程分析

AFNetworking多线程分析

AFNetworking是目前最常用的iOS的网络开发框架。它是对Apple系统提供的网络框架(NSURLConnection和NSURLSession)的上层封装。本文针对AFNetworking的2x版本的NSURLConnection的网络请求的工作流程做一个简单的梳理。

NSOperation介绍

在介绍AFNetworking的多线程流程前需要先了解一下NSOperation的基本知识。
提到并发相关的概念,可能会想到一些关键字,比如线程,锁等等。iOS开发中也有对应的支持,比如 NSThreadNSLock 等。但是直接使用这些组件来做并发会很复杂,也极易引入并发相关的bug并且难以调试,尤其是死锁相关的问题。因此Apple自己封装了这些低层次的组件,提供了更加易于的并发模型,即GCD和NSOperation。

GCD和NSOperation都是基于任务队列的并发模型。使用者不必要关系线程和锁,只需要将要执行的代码block放入到任务队列即可。GCD是一个轻量级的框架,提供的功能相对简单但是高效。NSOperation是在GCD基础之上封装的一个功能更加强大的框架。提供ObjectC风格的API,相比于GCD,它提供如下额外的功能:

  1. 设置任务队列中任务的依赖关系,比如任务A在任务B执行后执行
  2. 可以cancel或者suspend一个任务
  3. NSOperation支持KVO

相比于GCD,NSOperation在性能开销上有一些增加。但是如果要用到一些高级的功能,这些开销也是值得的。
例如,我们需要创建一个任务来输出‘Hello world’,只需要实现如下几步:

  1. 继承系统的NSOperation
  2. 覆盖main方法
  3. 在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中网络请求的基本框架如下:

afnetwork
afnetwork

从上图可以看到,系统维护了一个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就继承自AFURLConnectionOperationAFURLConnectionOperationNSOperation的子类,它实现了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的详细知识,请参考如下博客:

iOS中的Run Loop机制

在执行完上述代码后,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。

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

推荐阅读更多精彩内容

  • 简述 在iOS开发中,与直接使用苹果框架中提供的NSURLConnection或NSURLSession进行网络请...
    zongmumask阅读 9,389评论 9 49
  • Object C中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么? 1...
    AlanGe阅读 1,716评论 0 17
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,125评论 29 470
  • 写在开头: 大概回忆下,之前我们讲了AFNetworking整个网络请求的流程,包括request的拼接,sess...
    涂耀辉阅读 19,909评论 53 314
  • 本文是我对AFNetworking代码模块的总结,想做到有问题知道找谁(找AFNetworking的哪个模块),而...
    破弓阅读 446评论 0 0