AFNetworking 到底做了什么?(2)

112702646-2d036ecb6746df01
接着上一篇的内容往下讲,如果没看过上一篇内容可以点这:

AFNetworking到底做了什么?

之前我们讲到NSUrlSession代理这一块:

代理8:

这个代理就是task完成了的回调,方法内做了下面这几件事:

  • 在这里我们拿到了之前和这个task对应绑定的AF的delegate:
  • 去转发了调用了AF代理的方法。这个等我们下面讲完NSUrlSession的代理之后会详细说。
  • 然后把这个AF的代理和task的绑定解除了,并且移除了相关的progress和通知:
  • 调用了自定义的Blcok:self.taskDidComplete(session, task, error);
    代码还是很简单的,至于这个通知,我们等会再来补充吧。
NSURLSessionDataDelegate:
代理9:

官方文档翻译如下:

函数作用:
告诉代理,该data task获取到了服务器端传回的最初始回复(response)。注意其中的completionHandler这个block,通过传入一个类型为NSURLSessionResponseDisposition的变量来决定该传输任务接下来该做什么:
NSURLSessionResponseAllow 该task正常进行
NSURLSessionResponseCancel 该task会被取消
NSURLSessionResponseBecomeDownload 会调用URLSession:dataTask:didBecomeDownloadTask:方法来新建一个download task以代替当前的data task
NSURLSessionResponseBecomeStream 转成一个StreamTask

函数讨论:
该方法是可选的,除非你必须支持“multipart/x-mixed-replace”类型的content-type。因为如果你的request中包含了这种类型的content-type,服务器会将数据分片传回来,而且每次传回来的数据会覆盖之前的数据。每次返回新的数据时,session都会调用该函数,你应该在这个函数中合理地处理先前的数据,否则会被新数据覆盖。如果你没有提供该方法的实现,那么session将会继续任务,也就是说会覆盖之前的数据。

总结一下:

  • 当你把添加content-type的类型为multipart/x-mixed-replace那么服务器的数据会分片的传回来。然后这个方法是每次接受到对应片响应的时候会调被调用。你可以去设置上述4种对这个task的处理。
  • 如果我们实现了自定义Block,则调用一下,不然就用默认的NSURLSessionResponseAllow方式。
代理10:

  • 这个代理方法是被上面的代理方法触发的,作用就是新建一个downloadTask,替换掉当前的dataTask。所以我们在这里做了AF自定义代理的重新绑定操作。
  • 调用自定义Block。

按照顺序来,其实还有个AF没有去实现的代理:

这个也是之前的那个代理,设置为NSURLSessionResponseBecomeStream则会调用到这个代理里来。会新生成一个NSURLSessionStreamTask来替换掉之前的dataTask。

代理11:

  • 这个方法和上面didCompleteWithError算是NSUrlSession的代理中最重要的两个方法了。
  • 我们转发了这个方法到AF的代理中去,所以数据的拼接都是在AF的代理中进行的。这也是情理中的,毕竟每个响应数据都是对应各个task,各个AF代理的。在AFURLSessionManager都只是做一些公共的处理。
代理12:

官方文档翻译如下:

函数作用:
询问data task或上传任务(upload task)是否缓存response。

函数讨论:
当task接收到所有期望的数据后,session会调用此代理方法。如果你没有实现该方法,那么就会使用创建session时使用的configuration对象决定缓存策略。这个代理方法最初的目的是为了阻止缓存特定的URLs或者修改NSCacheURLResponse对象相关的userInfo字典。
该方法只会当request决定缓存response时候调用。作为准则,responses只会当以下条件都成立的时候返回缓存:
该request是HTTP或HTTPS URL的请求(或者你自定义的网络协议,并且确保该协议支持缓存)
确保request请求是成功的(返回的status code为200-299)
返回的response是来自服务器端的,而非缓存中本身就有的
提供的NSURLRequest对象的缓存策略要允许进行缓存
服务器返回的response中与缓存相关的header要允许缓存
该response的大小不能比提供的缓存空间大太多(比如你提供了一个磁盘缓存,那么response大小一定不能比磁盘缓存空间还要大5%)

  • 总结一下就是一个用来缓存response的方法,方法中调用了我们自定义的Block,自定义一个response用来缓存。
NSURLSessionDownloadDelegate
代理13:

  • 这个方法和之前的两个方法:

    总共就这3个方法,被转调到AF自定义delegate中。
  • 方法做了什么看注释应该很简单,就不赘述了。
代理14:

简单说一下这几个参数:
bytesWritten 表示自上次调用该方法后,接收到的数据字节数
totalBytesWritten表示目前已经接收到的数据字节数
totalBytesExpectedToWrite 表示期望收到的文件总字节数,是由Content-Length header提供。如果没有提供,默认是NSURLSessionTransferSizeUnknown。

代理15:

官方文档翻译:

函数作用:
告诉代理,下载任务重新开始下载了。

函数讨论:
如果一个正在下载任务被取消或者失败了,你可以请求一个resumeData对象(比如在userInfo字典中通过NSURLSessionDownloadTaskResumeData这个键来获取到resumeData)并使用它来提供足够的信息以重新开始下载任务。
随后,你可以使用resumeData作为downloadTaskWithResumeData:或downloadTaskWithResumeData:completionHandler:的参数。当你调用这些方法时,你将开始一个新的下载任务。一旦你继续下载任务,session会调用它的代理方法URLSession:downloadTask:didResumeAtOffset:expectedTotalBytes:其中的downloadTask参数表示的就是新的下载任务,这也意味着下载重新开始了。

总结一下:

  • 其实这个就是用来做断点续传的代理方法。可以在下载失败的时候,拿到我们失败的拼接的部分resumeData,然后用去调用downloadTaskWithResumeData:就会调用到这个代理方法来了。
  • 其中注意:fileOffset这个参数,如果文件缓存策略或者最后文件更新日期阻止重用已经存在的文件内容,那么该值为0。否则,该值表示当前已经下载data的偏移量。
  • 方法中仅仅调用了downloadTaskDidResume自定义Block。

至此NSUrlSesssion的delegate讲完了。大概总结下:

  • 每个代理方法对应一个我们自定义的Block,如果Block被赋值了,那么就调用它。
  • 在这些代理方法里,我们做的处理都是相对于这个sessionManager所有的request的。是公用的处理。
  • 转发了3个代理方法到AF的deleagate中去了,AF中的deleagate是需要对应每个task去私有化处理的

152702646-bc2238dee34f8acd

分割图.png

接下来我们来看转发到AF的deleagate,一共3个方法:

AF代理1:

这个方法是NSUrlSession任务完成的代理方法中,主动调用过来的。配合注释,应该代码很容易读,这个方法大概做了以下几件事:

  1. 生成了一个存储这个task相关信息的字典:userInfo,这个字典是用来作为发送任务完成的通知的参数。
  2. 判断了参数error的值,来区分请求成功还是失败。
  3. 如果成功则在一个AF的并行queue中,去做数据解析等后续操作:

    注意AF的优化的点,虽然代理回调是串行的(不明白可以见本文最后)。但是数据解析这种费时操作,确是用并行线程来做的。
  4. 然后根据我们一开始设置的responseSerializer来解析data。如果解析成功,调用成功的回调,否则调用失败的回调。
    我们重点来看看返回数据解析这行:

    我们点进去看看:

    原来就是这么一个协议方法,各种类型的responseSerializer类,都是遵守这个协议方法,实现了一个把我们请求到的data转换为我们需要的类型的数据的方法。至于各种类型的responseSerializer如何解析数据,我们到代理讲完再来补充。
  5. 这边还做了一个判断,如果自定义了GCD完成组completionGroup和完成队列的话completionQueue,会在加入这个组和在队列中回调Block。否则默认的是AF的创建的组:

    和主队列回调。AF没有用这个GCD组做任何处理,只是提供这个接口,让我们有需求的自行调用处理。如果有对多个任务完成度的监听,可以自行处理。
    而队列的话,如果你不需要回调主线程,可以自己设置一个回调队列。
  6. 回到主线程,发送了任务完成的通知:

    这个通知这回AF有用到了,在我们对UIKit的扩展中,用到了这个通知。
AF代理2:

同样被NSUrlSession代理转发到这里,拼接了需要回调的数据。

AF代理3:

下载成功了被NSUrlSession代理转发到这里,这里有个地方需要注意下:

  • 之前的NSUrlSession代理和这里都移动了文件到下载路径,而NSUrlSession代理的下载路径是所有request公用的下载路径,一旦设置,所有的request都会下载到之前那个路径。
  • 而这个是对应的每个task的,每个task可以设置各自下载路径,还记得AFHttpManager的download方法么

    这个地方return的path就是对应的这个代理方法里的path,我们调用最终会走到这么一个方法:

    清楚的可以看到地址被赋值给AF的Block了。

至此AF的代理也讲完了,数据或错误信息随着AF代理成功失败回调,回到了用户的手中。

152702646-bc2238dee34f8acd

分割图.png

接下来我们来补充之前AFURLResponseSerialization这一块是如何解析数据的:

112702646-0cef773849aea454

AFURLResponseSerialization.png

如图所示,AF用来解析数据的一共上述这些方法。第一个实际是一个协议方法,协议方法如下:

而后面6个类都是遵守这个协议方法,去做数据解析。这地方可以再次感受一下AF的设计模式…接下来我们就来主要看看这些类对这个协议方法的实现:

AFHTTPResponseSerializer:

  • 方法调用了一个另外的方法之后,就把data返回来了,我们继续往里看这个方法:

  • 看注释应该很容易明白这个方法有什么作用。简单来说,这个方法就是来判断返回数据与咱们使用的解析器是否匹配,需要解析的状态码是否匹配。如果错误,则填充错误信息,并且返回NO,否则返回YES,错误信息为nil。
  • 其中里面出现了两个属性值,一个acceptableContentTypes,一个acceptableStatusCodes,两者在初始化的时候有给默认值,我们也可以去自定义,但是如果给acceptableContentTypes定义了不匹配的类型,那么数据仍旧会解析错误。
  • 而AFHTTPResponseSerializer仅仅是调用验证方法,然后就返回了data。
AFJSONResponseSerializer:

注释写的很清楚,大概需要讲一下的是以下几个函数:

之前注释已经写清楚了这些函数的作用,首先来看第1个:

这里可以注意,我们这里传过去的code和domain两个参数分别为NSURLErrorCannotDecodeContentDataAFURLResponseSerializationErrorDomain,这两个参数是我们之前判断response可接受类型和code时候自己去生成错误的时候填写的。

第二个:

方法主要还是通过递归的形式实现。比较简单。

第三个:

方法主要是把json解析的错误,赋值给我们需要返回给用户的error上。比较简单,小伙伴们自己看看就好。

至此,AFJSONResponseSerializer就讲完了。
而我们ResponseSerialize还有一些其他的类型解析,大家可以自行去阅读,代码还是很容易读的,在这里就不浪费篇幅去讲了。

分割图.png

在AFURLSessionManager中,有这么一个类:_AFURLSessionTaskSwizzling。这个类大概的作用就是替换掉NSUrlSession中的resumesuspend方法。正常处理原有逻辑的同时,多发送一个通知,以下是我们需要替换的新方法:

这块知识是关于OC的Runtime:method swizzling的,如果有不清楚的地方,可以看看这里method swizzling–by冰霜或者自行查阅。

原方法中有大量的英文注释,我把它翻译过来如下:

iOS 7和iOS 8在NSURLSessionTask实现上有些许不同,这使得下面的代码实现略显trick
关于这个问题,大家做了很多Unit Test,足以证明这个方法是可行的
目前我们所知的:

  • NSURLSessionTasks是一组class的统称,如果你仅仅使用提供的API来获取NSURLSessionTask的class,并不一定返回的是你想要的那个(获取NSURLSessionTask的class目的是为了获取其resume方法)
  • 简单地使用[NSURLSessionTask class]并不起作用。你需要新建一个NSURLSession,并根据创建的session再构建出一个NSURLSessionTask对象才行。
  • iOS 7上,localDataTask(下面代码构造出的NSURLSessionDataTask类型的变量,为了获取对应Class)的类型是 NSCFLocalDataTask,NSCFLocalDataTask继承自NSCFLocalSessionTask,NSCFLocalSessionTask继承自__NSCFURLSessionTask。
  • iOS 8上,localDataTask的类型为NSCFLocalDataTask,NSCFLocalDataTask继承自NSCFLocalSessionTask,NSCFLocalSessionTask继承自NSURLSessionTask
  • iOS 7上,NSCFLocalSessionTask和NSCFURLSessionTask是仅有的两个实现了resume和suspend方法的类,另外NSCFLocalSessionTask中的resume和suspend并没有调用其父类(即NSCFURLSessionTask)方法,这也意味着两个类的方法都需要进行method swizzling。
  • iOS 8上,NSURLSessionTask是唯一实现了resume和suspend方法的类。这也意味着其是唯一需要进行method swizzling的类
  • 因为NSURLSessionTask并不是在每个iOS版本中都存在,所以把这些放在此处(即load函数中),比如给一个dummy class添加swizzled方法都会变得很方便,管理起来也方便。

一些假设前提:

  • 目前iOS中resume和suspend的方法实现中并没有调用对应的父类方法。如果日后iOS改变了这种做法,我们还需要重新处理。
  • 没有哪个后台task会重写resume和suspend函数

其余的一部分翻译在注释中,对应那一行代码。大概总结下这个注释:

  • 其实这是被社区大量讨论的一个bug,之前AF因为这个替换方法,会导致偶发性的crash,如果不要这个swizzle则问题不会再出现,但是这样会导致AF中很多UIKit的扩展都不能正常使用。
  • 原来这是因为iOS7和iOS8的NSURLSessionTask的继承链不同导致的,而且在iOS7继承链中会有两个类都实现了resumesuspend方法。而且子类没有调用父类的方法,我们则需要对着两个类都进行方法替换。而iOS8只需要对一个类进行替换。
  • 对着注释看,上述方法代码不难理解,用一个while循环,一级一级去获取父类,如果实现了resume方法,则进行替换。

但是有几个点大家可能会觉得疑惑的,我们先把这个方法调用的替换的函数一块贴出来。

因为有小伙伴问到过,所以我们来分析分析大家可能会觉得疑惑的地方:

  1. 首先可以注意class_getInstanceMethod这个方法,它会获取到当前类继承链逐级往上,第一个实现的该方法。所以说它获取到的方法不能确定是当前类还是父类的。而且这里也没有用dispatch_once_t来保证一个方法只交换一次,那万一这是父类的方法,当前类换一次,父类又换一次,不是等于没交换么?…请注意这行判断:

    这个条件就杜绝了这种情况的发生,只有当前类实现了这个方法,才可能进入这个if块。

2.那iOS7两个类都交换了af_resume,那岂不是父类换到子类方法了?…只能说又是没仔细看代码的…注意AF是去向当前类添加af_resume方法,然后去交换当前类的af_resume。所以说根本不会出现这种情况…

AFUrlSessionManager 基本上就这么多内容了。

152702646-bc2238dee34f8acd

分割图.png

现在我们回到一开始初始化的这行代码上:

1)首先我们要明确一个概念,这里的并发数仅仅是回调代理的线程并发数。而不是请求网络的线程并发数。请求网络是由NSUrlSession来做的,它内部维护了一个线程池,这个用来做网络请求,它在这些线程用底层的CFSocket去发送和接收请求。它是并发的。

2)明确了这个概念之后,我们来梳理一下AF3.x的整个流程和线程的关系:

  • 我们一开始初始化sessionManager的时候,一般都是在主线程,(当然不排除有些人喜欢在分线程初始化…)
  • 然后我们调用get或者post等去请求数据,接着会进行request拼接,AF代理的字典映射,progressKVO添加等等,到NSUrlSessionresume之前这些准备工作,仍旧是在主线程中的。
  • 然后我们调用NSUrlSessionresume,接着就跑到NSUrlSession内部去对网络进行数据请求了,在它内部是多线程并发的去请求数据的。
  • 紧接着数据请求完成后,回调回来在我们一开始生成的并发数为1的NSOperationQueue中,这个时候会是多线程串行的回调回来的。(注:不明白的朋友可以看看雷纯峰大神这篇iOS 并发编程之 Operation Queues
  • 然后我们到返回数据解析那一块,我们自己又创建了并发的多线程,去对这些数据进行了各种类型的解析。
  • 最后我们如果有自定义的completionQueue,则在自定义的queue中回调回来,也就是分线程回调回来,否则就是主队列,主线程中回调结束。

3)最后我们来解释解释为什么回调Queue要设置并发数为1:

  • 我认为AF这么做有以下两点原因:
    1)众所周知,AF2.x所有的回调是在一条线程,这条线程是AF的常驻线程,而这一条线程正是AF调度request的思想精髓所在,所以第一个目的就是为了和之前版本保持一致。
    2)因为跟代理相关的一些操作AF都使用了NSLock。所以就算Queue的并发数设置为n,因为多线程回调,锁的等待,导致所提升的程序速度也并不明显。反而多task回调导致的多线程并发,平白浪费了部分性能。
    而设置Queue的并发数为1,(注:这里虽然回调Queue的并发数为1,仍然会有不止一条线程,但是因为是串行回调,所以同一时间,只会有一条线程在操作AFUrlSessionManager的那些方法。)至少回调的事件,是不需要多线程并发的。回调没有了NSLock的等待时间,所以对时间并没有多大的影响。(注:但是还是会有多线程的操作的,因为设置刚开始调起请求的时候,是在主线程的,而回调则是串行分线程。)

当然这仅仅是我个人的看法,如果有不同意见的欢迎交流~

至此我们AF3.X业务层的逻辑,基本上结束了。小伙伴们,看到这你明白了AF做了什么了吗?可能很多朋友要扔鸡蛋了…可能你还是没觉得AF到底有什么用,我用NSUrlSession不也一样,我干嘛要用AF,在这里,我暂时卖个关子,等我们下篇讲完AFSecurityPolicy和部分UIKit的扩展,以及AF2.x的核心类源码实现之后,我们再好好总结。

未完待续…
1 1 收藏 评论

相关文章

可能感兴趣的话题



直接登录
跳到底部
返回顶部