AFNetworking2.0源码解析<一>

最近看AFNetworking2的源码,学习这个知名网络框架的实现,顺便梳理写下文章。AFNetworking2的大体架构和思路在这篇文章已经说得挺清楚了,就不再赘述了,只说说实现的细节。AFNetworking的代码还在不断更新中,我看的是AFNetworking2.3.1
本篇先看看AFURLConnectionOperation,AFURLConnectionOperation继承自NSOperation,是一个封装好的任务单元,在这里构建了NSURLConnection,作为NSURLConnection的delegate处理请求回调,做好状态切换,线程管理,可以说是AFNetworking最核心的类

0.Tricks
AFNetworking代码中有一些常用技巧,先说明一下。
A.clang warning

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu"
//code
#pragma clang diagnostic pop

表示在这个区间里忽略一些特定的clang的编译警告,因为AFNetworking作为一个库被其他项目引用,所以不能全局忽略clang的一些警告,只能在有需要的时候局部这样做,作者喜欢用?:符号,所以经常见忽略-Wgnu警告的写法,详见这里

B.dispatch_once
为保证线程安全,所有单例都用dispatch_once生成,保证只执行一次,这也是iOS开发常用的技巧。例如:

static dispatch_queue_t url_request_operation_completion_queue() {
    static dispatch_queue_t af_url_request_operation_completion_queue;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        af_url_request_operation_completion_queue = dispatch_queue_create("com.alamofire.networking.operation.queue",   DISPATCH_QUEUE_CONCURRENT );
    });
    return af_url_request_operation_completion_queue;
}

C.weak & strong self
常看到一个block要使用self,会处理成在外部声明一个weak变量指向self,在block里又声明一个strong变量指向weakSelf:

__weak __typeof(self)weakSelf = self;
self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
    __strong __typeof(weakSelf)strongSelf = weakSelf;
}];

weakSelf是为了block不持有self,避免循环引用,而再声明一个strongSelf是因为一旦进入block执行,就不允许self在这个执行过程中释放。block执行完后这个strongSelf会自动释放,没有循环引用问题。

1.线程
先来看看NSURLConnection发送请求时的线程情况,NSURLConnection是被设计成异步发送的,调用了start方法后,NSURLConnection会新建一些线程用底层的CFSocket去发送和接收请求,在发送和接收的一些事件发生后通知原来线程的Runloop去回调事件。
NSURLConnection的同步方法sendSynchronousRequest方法也是基于异步的,同样要在其他线程去处理请求的发送和接收,只是同步方法会手动block住线程,发送状态的通知也不是通过RunLoop进行。
使用NSURLConnection有几种选择:

A.在主线程调异步接口
若直接在主线程调用异步接口,会有个Runloop相关的问题:
当在主线程调用[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]时,请求发出,侦听任务会加入到主线程的Runloop下,RunloopMode会默认为NSDefaultRunLoopMode。这表明只有当前线程的Runloop处于NSDefaultRunLoopMode时,这个任务才会被执行。但当用户滚动tableview或scrollview时,主线程的Runloop是处于NSEventTrackingRunLoopMode模式下的,不会执行NSDefaultRunLoopMode的任务,所以会出现一个问题,请求发出后,如果用户一直在操作UI上下滑动屏幕,那在滑动结束前是不会执行回调函数的,只有在滑动结束,RunloopMode切回NSDefaultRunLoopMode,才会执行回调函数。苹果一直把动画效果性能放在第一位,估计这也是苹果提升UI动画性能的手段之一。
所以若要在主线程使用NSURLConnection异步接口,需要手动把RunloopMode设为NSRunLoopCommonModes。这个mode意思是无论当前Runloop处于什么状态,都执行这个任务。

NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];

B.在子线程调同步接口
若在子线程调用同步接口,一条线程只能处理一个请求,因为请求一发出去线程就阻塞住等待回调,需要给每个请求新建一个线程,这是很浪费的,这种方式唯一的好处应该是易于控制请求并发的数量。

C.在子线程调异步接口
子线程调用异步接口,子线程需要有Runloop去接收异步回调事件,这里也可以每个请求都新建一条带有Runloop的线程去侦听回调,但这一点好处都没有,既然是异步回调,除了处理回调内容,其他时间线程都是空闲可利用的,所有请求共用一个响应的线程就够了。
AFNetworking用的就是第三种方式,创建了一条常驻线程专门处理所有请求的回调事件,这个模型跟nodejs有点类似。网络请求回调处理完,组装好数据后再给上层调用者回调,这时候回调是抛回主线程的,因为主线程是最安全的,使用者可能会在回调中更新UI,在子线程更新UI会导致各种问题,一般使用者也可以不需要关心线程问题。
以下是相关线程大致的关系,实际上多个NSURLConnection会共用一个NSURLConnectionLoader线程,这里就不细化了,除了处理socket的CFSocket线程,还有一些Javascript:Core的线程,目前不清楚作用,归为NSURLConnection里的其他线程。因为NSURLConnection是系统控件,每个iOS版本可能都有不一样,可以先把NSURLConnection当成一个黑盒,只管它的start和callback就行了。如果使用AFHttpRequestOperationManager的接口发送请求,这些请求会统一在一个NSOperationQueue里去发,所以多了上面NSOperationQueue的一个线程。


相关代码:-networkRequestThread:, -start:, -operationDidStart:。

2.状态机
继承NSOperation有个很麻烦的东西要处理,就是改变状态时需要发KVO通知,否则这个类加入NSOperationQueue不可用了。NSOperationQueue是用KVO方式侦听NSOperation状态的改变,以判断这个任务当前是否已完成,完成的任务需要在队列中除去并释放。
AFURLConnectionOperation对此做了个状态机,统一搞定状态切换以及发KVO通知的问题,内部要改变状态时,就只需要类似self.state = AFOperationReadyState的调用而不需要做其他了,状态改变的KVO通知在setState里发出。
总的来说状态管理相关代码就三部分,一是限制一个状态可以切换到其他哪些状态,避免状态切换混乱,二是状态Enum值与NSOperation四个状态方法的对应,三是在setState时统一发KVO通知。详见代码注释。
相关代码:AFKeyPathFromOperationState, AFStateTransitionIsValid, -setState:, -isPaused:, -isReady:, -isExecuting:, -isFinished:.

3.NSURLConnectionDelegate
处理NSURLConnection Delegate的内容不多,代码也是按请求回调的顺序排列下去,十分易读,主要流程就是接收到响应的时候打开outputStream,接着有数据过来就往outputStream写,在上传/接收数据过程中会回调上层传进来的相应的callback,在请求完成回调到connectionDidFinishLoading时,关闭outputStream,用outputStream组装responseData作为接收到的数据,把NSOperation状态设为finished,表示任务完成,NSOperation会自动调用completeBlock,再回调到上层。

4.setCompleteBlock
NSOperation在iOS4.0以后提供了个接口setCompletionBlock,可以传入一个block作为任务执行完成时(state状态机变为finished时)的回调,AFNetworking直接用了这个接口,并通过重写加了几个功能:
A.消除循环引用
在NSOperation的实现里,completionBlock是NSOperation对象的一个成员,NSOperation对象持有着completionBlock,若传进来的block用到了NSOperation对象,或者block用到的对象持有了这个NSOperation对象,就会造成循环引用。这里执行完block后调用[strongSelf setCompletionBlock:nil]把completionBlock设成nil,手动释放self(NSOperation对象)持有的completionBlock对象,打破循环引用。
可以理解成对外保证传进来的block一定会被释放,解决外部使用使很容易出现的因对象关系复杂导致循环引用的问题,让使用者不知道循环引用这个概念都能正确使用。
B.dispatch_group
这里允许用户让所有operation的completionBlock在一个group里执行,但我没看出这样做的作用,若想组装一组请求(见下面的batchOfRequestOperations)也不需要再让completionBlock在group里执行,求解。
C.”The Deallocation Problem”
作者在注释里说这里重写的setCompletionBlock方法解决了”The Deallocation Problem”,实际上并没有。”The Deallocation Problem”简单来说就是不要让UIKit的东西在子线程释放。
这里如果传进来的block持有了外部的UIViewController或其他UIKit对象(下面暂时称为A对象),并且在请求完成之前其他所有对这个A对象的引用都已经释放了,那么这个completionBlock就是最后一个持有这个A对象的,这个block释放时A对象也会释放。这个block在什么线程释放,A对象就会在什么线程释放。我们看到block释放的地方是url_request_operation_completion_queue(),这是AFNetworking特意生成的子线程,所以按理说A对象是会在子线程释放的,会导致UIKit对象在子线程释放,会有问题。
但AFNetworking实际用起来却没问题,想了很久不得其解,后来做了实验,发现iOS5以后苹果对UIKit对象的释放做了特殊处理,只要发现在子线程释放这些对象,就自动转到主线程去释放,断点出来是由一个叫_objc_deallocOnMainThreadHelper的方法做的。如果不是UIKit对象就不会跳到主线程释放。AFNetworking2.0只支持iOS6+,所以没问题。

5.batchOfRequestOperations
这里额外提供了一个便捷接口,可以传入一组请求,在所有请求完成后回调complionBlock,在每一个请求完成时回调progressBlock通知外面有多少个请求已完成。详情参见代码注释,这里需要说明下dispatch_group_enter和dispatch_group_leave的使用,这两个方法用于把一个异步任务加入group里。
一般我们要把一个任务加入一个group里是这样:

dispatch_group_async(group, queue, ^{
    block();
});

这个写法等价于

dispatch_async(queue, ^{
    dispatch_group_enter(group);
    block()
    dispatch_group_leave(group);
});

如果要把一个异步任务加入group,这样就行不通了:

dispatch_group_async(group, queue, ^{
    [self performBlock:^(){
        block();
    }];
    //未执行到block() group任务就已经完成了
});

这时需要这样写:

dispatch_group_enter(group);
[self performBlock:^(){
    block();
    dispatch_group_leave(group);
}];

异步任务回调后才算这个group任务完成。对batchOfRequest的实现来说就是请求完成并回调后,才算这个任务完成。
其实这跟retain/release差不多,都是计数,dispatch_group_enter时任务数+1,dispatch_group_leave时任务数-1,任务数为0时执行dispatch_group_notify的内容。
相关代码:-batchOfRequestOperations:progressBlock:completionBlock:

6.其他

A.锁

AFURLConnectionOperation有一把递归锁,在所有会访问/修改成员变量的对外接口都加了锁,因为这些对外的接口用户是可以在任意线程调用的,对于访问和修改成员变量的接口,必须用锁保证线程安全。

B.序列化

AFNetworking的多数类都支持序列化,但实现的是NSSecureCoding的接口,而不是NSCoding,区别在于解数据时要指定Class,用-decodeObjectOfClass:forKey:方法代替了-decodeObjectForKey:。这样做更安全,因为序列化后的数据有可能被篡改,若不指定Class,-decode出来的对象可能不是原来的对象,有潜在风险。另外,NSSecureCoding是iOS6以上才有的。详见这里
这里在序列化时保存了当前任务状态,接收的数据等,但回调block是保存不了的,需要在取出来发送时重新设置。可以像下面这样持久化保存和取出任务:

AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
NSData *data = [NSKeyedArchiver archivedDataWithRootObject:operation];
 
AFHTTPRequestOperation *operationFromDB = [NSKeyedUnarchiver unarchiveObjectWithData:data];
[operationFromDB start];

C.backgroundTask

这里提供了setShouldExecuteAsBackgroundTaskWithExpirationHandler接口,决定APP进入后台后是否继续发送接收请求,并在后台执行时间超时后取消所有请求。在dealloc里需要调用[application endBackgroundTask:],告诉系统这个后台任务已经完成,不然系统会一直让你的APP运行在后台,直到超时。
相关代码:-setShouldExecuteAsBackgroundTaskWithExpirationHandler:, -dealloc:

7.AFHTTPRequestOperation

AFHTTPRequestOperation继承了AFURLConnectionOperation,把它放一起说是因为它没做多少事情,主要多了responseSerializer,暂停下载断点续传,以及提供接口请求成功失败的回调接口-setCompletionBlockWithSuccess:failure:。

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

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,121评论 29 470
  • ———————————————回答好下面的足够了---------------------------------...
    恒爱DE问候阅读 1,710评论 0 4
  • __block和__weak修饰符的区别其实是挺明显的:1.__block不管是ARC还是MRC模式下都可以使用,...
    LZM轮回阅读 3,278评论 0 6
  • OC的理解与特性 OC作为一门面向对象的语言,自然具有面向对象的语言特性:封装、继承、多态。它既具有静态语言的特性...
    克鲁德李阅读 445评论 0 0
  • 序言 目前形势,参加到iOS队伍的人是越来越多,甚至已经到供过于求了。今年,找过工作人可能会更深刻地体会到今年的就...
    Jack_lin阅读 78,321评论 110 1,945