本篇先看看AFURLConnectionOperation,AFURLConnectionOperation继承自NSOperation,是一个封装好的任务单元,在这里构建了NSURLConnection,作为NSURLConnection的delegate处理请求回调,做好状态切换,线程管理,可以说是AFNetworking最核心的类,下面分几部分说下看源码时注意的点,最后放上代码的注释。
 
0.Tricks
AFNetworking代码中有一些常用技巧,先说明一下。
 
A.clang warning
  1. #pragma clang diagnostic push
  2. #pragma clang diagnostic ignored "-Wgnu"
  3. //code
  4. #pragma clang diagnostic pop
表示在这个区间里忽略一些特定的clang的编译警告,因为AFNetworking作为一个库被其他项目引用,所以不能全局忽略clang的一些警告,只能在有需要的时候局部这样做,作者喜欢用?:符号,所以经常见忽略-Wgnu警告的写法,详见这里
 
B.dispatch_once
为保证线程安全,所有单例都用dispatch_once生成,保证只执行一次,这也是iOS开发常用的技巧。例如:
  1. static dispatch_queue_t url_request_operation_completion_queue() {
  2. static dispatch_queue_t af_url_request_operation_completion_queue;
  3. static dispatch_once_t onceToken;
  4. dispatch_once(&onceToken, ^{
  5. af_url_request_operation_completion_queue = dispatch_queue_create("com.alamofire.networking.operation.queue",   DISPATCH_QUEUE_CONCURRENT );
  6. });
  7. return af_url_request_operation_completion_queue;
  8. }
C.weak & strong self
常看到一个 block 要使用 self,会处理成在外部声明一个 weak 变量指向 self,在 block 里又声明一个 strong 变量指向 weakSelf:
  1. __weak __typeof(self)weakSelf = self;
  2. self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
  3. __strong __typeof(weakSelf)strongSelf = weakSelf;
  4. }];
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 处于什么状态,都执行这个任务。
  1. NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
  2. [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
  3. [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里是这样:
  1. dispatch_group_async(group, queue, ^{
  2. block();
  3. });
这个写法等价于
  1. dispatch_async(queue, ^{
  2. dispatch_group_enter(group);
  3. block()
  4. dispatch_group_leave(group);
  5. });
如果要把一个异步任务加入group,这样就行不通了:
  1. dispatch_group_async(group, queue, ^{
  2. [self performBlock:^(){
  3. block();
  4. }];
  5. //未执行到block() group任务就已经完成了
  6. });
这时需要这样写:
  1. dispatch_group_enter(group);
  2. [self performBlock:^(){
  3. block();
  4. dispatch_group_leave(group);
  5. }];
异步任务回调后才算这个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 是 iOS 6 以上才有的。详见这里
 
这里在序列化时保存了当前任务状态,接收的数据等,但回调block是保存不了的,需要在取出来发送时重新设置。可以像下面这样持久化保存和取出任务:
  1. AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
  2. NSData *data = [NSKeyedArchiver archivedDataWithRootObject:operation];
  3. AFHTTPRequestOperation *operationFromDB = [NSKeyedUnarchiver unarchiveObjectWithData:data];
  4. [operationFromDB start];
C.backgroundTask
这里提供了setShouldExecuteAsBackgroundTaskWithExpirationHandler 接口,决定APP进入后台后是否继续发送接收请求,并在后台执行时间超时后取消所有请求。在 dealloc 里需要调用 [application endBackgroundTask:] ,告诉系统这个后台任务已经完成,不然系统会一直让你的APP运行在后台,直到超时。
 
相关代码:-setShouldExecuteAsBackgroundTaskWithExpirationHandler:, -dealloc:
 
7.AFHTTPRequestOperation
AFHTTPRequestOperation 继承了 AFURLConnectionOperation,把它放一起说是因为它没做多少事情,主要多了responseSerializer,暂停下载断点续传,以及提供接口请求成功失败的回调接口 -setCompletionBlockWithSuccess:failure:。详见源码注释。
 
8.源码注释
  1. AFURLConnectionOperation.m
 
  1. AFHTTPRequestOperation.m

AFNetworking2.0源码解析<一>的更多相关文章

  1. AFNetworking2.0源码解析<四>

    结构 AFURLResponseSerialization负责解析网络返回数据,检查数据是否合法,把NSData数据转成相应的对象,内置的转换器有json,xml,plist,image,用户可以很方 ...

  2. AFNetworking2.0源码解析<三>

    本篇说说安全相关的AFSecurityPolicy模块,AFSecurityPolicy用于验证HTTPS请求的证书,先来看看HTTPS的原理和证书相关的几个问题. HTTPS HTTPS连接建立过程 ...

  3. AFNetworking2.0源码解析<二>

    本篇我们继续来看看AFNetworking的下一个模块 — AFURLRequestSerialization.   AFURLRequestSerialization用于帮助构建NSURLReque ...

  4. AFNetworking (3.1.0) 源码解析 <四>

    这次主要看一下文件夹Security中的类AFSecurityPolicy----安全策略类. AFSecurityPolicy主要的作用是验证HTTPS请求证书的有效性,在iOS9之后,默认不能发送 ...

  5. solr&lucene3.6.0源码解析(四)

    本文要描述的是solr的查询插件,该查询插件目的用于生成Lucene的查询Query,类似于查询条件表达式,与solr查询插件相关UML类图如下: 如果我们强行将上面的类图纳入某种设计模式语言的话,本 ...

  6. Sentinel源码解析四(流控策略和流控效果)

    引言 在分析Sentinel的上一篇文章中,我们知道了它是基于滑动窗口做的流量统计,那么在当我们能够根据流量统计算法拿到流量的实时数据后,下一步要做的事情自然就是基于这些数据做流控.在介绍Sentin ...

  7. solr&lucene3.6.0源码解析(三)

    solr索引操作(包括新增 更新 删除 提交 合并等)相关UML图如下 从上面的类图我们可以发现,其中体现了工厂方法模式及责任链模式的运用 UpdateRequestProcessor相当于责任链模式 ...

  8. Heritrix 3.1.0 源码解析(三十七)

    今天有兴趣重新看了一下heritrix3.1.0系统里面的线程池源码,heritrix系统没有采用java的cocurrency包里面的并发框架,而是采用了线程组ThreadGroup类来实现线程池的 ...

  9. Android事件总线(二)EventBus3.0源码解析

    1.构造函数 当我们要调用EventBus的功能时,比如注册或者发送事件,总会调用EventBus.getDefault()来获取EventBus实例: public static EventBus ...

随机推荐

  1. (55)Linux驱动开发之一驱动概述

                                                                                                      驱动 ...

  2. NLP大赛冠军总结:300万知乎多标签文本分类任务(附深度学习源码)

    NLP大赛冠军总结:300万知乎多标签文本分类任务(附深度学习源码)       七月,酷暑难耐,认识的几位同学参加知乎看山杯,均取得不错的排名.当时天池AI医疗大赛初赛结束,官方正在为复赛进行平台调 ...

  3. 微信小程序 API 界面 (2)

    由于每个 API 参数:对象的属性都有 success,fail,complete,所以在这个提前介绍,就不再每个API 上写了 success:类型 函数 接口调用成功的回调函数 fail:类型 函 ...

  4. kurento搭建以及运行kurento-hello-world

    搭建环境的系统是ubuntu 1.kurento服务器搭建 运行如下脚本即可完成安装 #!/bin/bash echo "deb http://ubuntu.kurento.org trus ...

  5. Windows Server系统定时任务备份ORACLE数据库

    Windows Server系统定时任务备份ORACLE数据库 一.编辑备份脚本 RMAN备份数据库 1.在备份脚本目录下,创建bat文件db_rman.bat set ORACLE_SID=orcl ...

  6. 大数据时代下EDM邮件营销的变革

    根据研究,今年的EDM邮件营销的邮件发送量比去年增长了63%,许多方法可以为你收集用户数据,这些数据可以帮助企业改善自己在营销中的精准度,相关性和执行力. 最近的一项研究表明,中国800强企业当中超过 ...

  7. 服务器控件调用JS函数

    是服务器端控件,不能在JS里直接调用,但可以在aspx.cs 里写方法可以调用JS函数,比如JS方法名称是check(), function check() {   alert(document.ge ...

  8. Oracle.DataAccess.Client.OracleCommand”的类型初始值设定项引发异常

    Oracle.ManagedDataAccess.dll 连接Oracle数据库不需要安装客户端 最开始,连接Oracle 数据是需要安装客户端的,ado.net 后来由于微软未来不再支持 Syste ...

  9. Unity ZTest 深度测试 & ZWrite 深度写入

    初学Shader,一开始对于渲染队列,ZTest 和 ZWrite一头雾水,经过多方查阅和实验,有了一些自己的理解.发此文与初学Shader的朋友分享,也算是为自己做个笔记.不对或不足之处欢迎指正. ...

  10. Java面试题集(86-115)

    Java程序员面试题集(86-115) 摘要:下面的内容包括Struts 2和Hibernate的常见面试题,虽然Struts 2在2013年6月曝出高危漏洞后已经显得江河日下,而Spring MVC ...