AFNetworking源码分析

Stella981
• 阅读 738

不用网络框架进行网络请求

NSURLConnection的简单使用(下面的代码均只为了演示,更详细的使用方法请自行谷歌)

NSURLConnection提供了两个类方法用于发起同步或异步请求,对于异步请求来说必然是在子线程中发起,若在主线程中发起异步网络请求会造成主线程阻塞,界面无响应,这就涉及到多线程编程。但多线程编程是一门非常细致的活,要考虑很多的问题,比如线程的生命周期,多线程资源竞争,加锁,避免死锁,稍不留意就会踩到坑里。好在苹果的接口使用极其方便,你甚至不需要理解多线程就能轻松使用上多线程了,发起异步请求的接口内部已经实现了多线程相关的操作。越是高级的接口,其隐藏的细节就越多,对于开发者来说当然是很方便,但若还能对其实现有所理解那就更完美了。当子线程发起了异步请求后会阻塞以等待网络响应,那应该由谁来处理网络响应呢?苹果提供了两种方式,一种是block,提供一个处理响应的block回调。一种是代理,使用代理的话就必须实现NSURLConnectionDelegate这个协议。你只需要在有网络请求的UIViewController中调用NSURLConnection提供的类方法就可以了。但如果你的项目中有不止一个UIViewController或者有的UIViewController中都不止一个请求的话,你就需要在每一个有网络请求的UIViewController中这样写。这样写会有什么问题呢?首先会造成软件结构不清晰,没有剥离出网络层,其次没有实现网络请求统一管理,无法实现取消所有网络请求等功能,再有会出现很多重复的代码,没有让公用功能形成模块,进行复用。当然为了解决这些问题你也可以自己实现自己的网络框架。

使用NSURLConnection版本的AFNetworking

使用网络框架的好处在于可以将分散在各个视图控制器中的网络请求统一起来模块化形成网络层,降低与数据层和表现层的耦合。AFNetworking就做了这样的工作。网络上已经有很多分析基于NSURLConnection实现的AFNetworking 2.x的源代码,这里只简单说一下其实现整个流程,想要深入了解的请谷歌或查看其源代码。首先AFNetworking要解决实现接口统一和所有网络请求统一管理的问题。NSURLConnection发起有两种方式发起请求,分别是设置响应block和代理,那采用哪种方式能够实现网络请求统一管理呢?block肯定不行,因为传入响应回调block的[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]方法会立即执行。那么只能使用设置响应代理这种方式了。具体实现是将网络请求继承于NSOperation,生产网络请求,然后再将这个网络请求加入操作队列里,剩下的所有与线程相关的操作都由这个操作队列去实现。越高级易容的接口就隐藏越多的实现细节,NSOperation隐藏了线程相关的所有细节,使得开发者只需关心构建什么样的操作。AFNetworking甚至隐藏了操作,操作队列的概念,使得开发者只需关心如何设置网络相关的请求参数,响应回调等,而不用再关心当多个网络请求如何进行统一管理,这些都已经由AFNetworking内部的操作队列属性完成了。那需要为每个请求创建一个线程来发起请求吗?基于NSURLConnection的AFNetworking是只创建了一条线程来发起所有请求并阻塞以等待响应。

重构推出的NSURLSession解决了NSURLConnection哪些问题

从2013年苹果发布重构后的加载系统NSURLSession,AFNetworking这个最受欢迎的网络框架也随之发布了基于NSURLSession的实现版本。这篇文章从 NSURLConnection 到 NSURLSession对NSURLSession的使用做了介绍。既然是对NSURLConnection进行的重构,那一定是解决了NSURLConnection存在的一些问题。苹果的接口以及其简单的形式呈现给开发者,尽量把复杂,容易出错的地方以简单的接口暴露出来,这使得用户能够参考文档很快上手,但这也有一些弊端,就是开发者对其中的原理不是很了解。越高级的接口越容易使用,但其隐蔽的实现细节就越多。相信在进行多线程编程的时候,很多开发者遇到过各种各样的坑,但在iOS平台上,苹果推出了GCD,NSperation等一系列接口可以让开发者完全可以只关注业务的实现,这些接口内部已经替开发者管理好了线程的创建,销毁,多线程资源竞争等需要开发者费很多精力的事情,甚至开发者不用对多线程理解透彻都能把多线程用得得心应手。NSURLConnection已经隐藏了线程相关的操作,已经给开发者减轻了很多负担。但NSURLConnectoin只隐藏了单个网络请求的线程的相关操作,并没有提供接口来解决多个网络请求时多个线程的管理问题,譬如当有多个网络请求时是否应该使用线程池来避免不停创建与销毁线程(这个可以有NSOperationQueue很好的解决)。并且NSURLConnection不是基于HTTP/2协议的,若使用NSURLConnection发起请求则每次请求都需要经过三次握手过程,可见NSURLConnection确实有很多可以优化的地方(我只发现这些)。NSURLConnection存在的无法将多个请求关联起来的问题已经很好的由AFNetworking解决了,所以推出的NSURLSession可以说是借鉴了AFNetworking的思想,并且可以从AFNetworking的源代码中很容易的看出来。基于NSURLConnection的AFNetworking需要让网络请求继承与NSOperation,然后再将该生成的网络请求加入操作队列中,但基于NSURLSessioin的AFNetworking只需要创建一个网络请求任务就可以了,原因在于,NSURLSession内部已经维护了两个操作队列,一个是处理session的相关回调,一个是处理响应相关的回调,所以说NSURLSession是借鉴了AFNetworking的继承于NSOperation和用单线程发起并等待响应的思想(这个会在后面给出证明)。

基于NSURLSession的AFNetworking的源码分析

下图是基于NSURLSession的AFNetworking的UML图(只为展示类之间的关联关系,并没有给出每个类的所有属性和方法):

AFNetworking源码分析

AFNetworking.png

从该类图已经能够明白AFNetworking整个的工作流程。
下面进行代码分析,使用AFHTTPSessionManager进行网络请求的示例代码(具体的使用请参考AFNetworking的README文档):

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:URL parameters:nil progress:nil success:^(NSURLSessionDataTask *_Nonnulltask, id _NullableresponseObject) {
  dispatch_async(dispatch_get_main_queue(), ^{
  //将子线程从网络拉取的数据用于主线程刷新视图
    });
  }failure:^(NSURLSessionDataTask*_Nullabletask,NSError*_Nonnullerror) {

}];

通过查看manager方法代码可以看到其实现最终是调用了父类AFURLSessionManager的initWithSessionConfiguration:方法,该方法代码片段如下:

self.sessionConfiguration = configuration;
self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];
self.responseSerializer = [AFJSONResponseSerializer serializer];

可以看到AFNetworking对数据的解析方式默认是json解析。
这里设置的代理操作队列最大的并发操作数为1是让所有请求的发起和等待网络响应均在同一条线程中执行,而不用为每一个请求都新建一条线程,这样节约了很多资源。
在响应到达后会执行AFURLSessionManager的NSURLSessionDataDelegate协议的方法,[AFURLSessioinManager URLSession:dataTask:didReceiveData:]用于查找对应的响应代理,并将后续的数据处理如数据拼接转交给该代理。该方法的实现代码如下:

AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:dataTask];
[delegate URLSession:session dataTask:dataTask didReceiveData:data];

if (self.dataTaskDidReceiveData) {
    self.dataTaskDidReceiveData(session, dataTask, data);
}

在数据比较大时,改方法可能会多次执行。
当数据传输完成后会调用[AFURLSessioinManager URLSession:task:didCompleteWithError],该方法用于让对应的代理执行NSURLSessionTaskDelegate协议中的方法,并将该代理对象从字典中移除,源代码如下:

AFURLSessionManagerTaskDelegate *delegate = [self delegateForTask:task];

// delegate may be nil when completing a task in the background
if (delegate) {
    [delegate URLSession:session task:task didCompleteWithError:error];

    [self removeDelegateForTask:task];
}

if (self.taskDidComplete) {
    self.taskDidComplete(session, task, error);
}

随后代理执行URLSession:task:didCompleteWithError:,该方法把数据放到另一个由静态方法生成的url_session_manager_processing_queue操作队列中做数据解析,如json解析,并将解析后的数据回传到主线程或者你自己生成的操作队列里,通过通知中心将请求完成的消息传递到主线程去(后面会写一篇文章介绍通知中心的实现原理,并写一个类似的通知中心)。该方法源码片段如下:

dispatch_async(url_session_manager_processing_queue(), ^{
            NSError *serializationError = nil;
            responseObject = [manager.responseSerializer responseObjectForResponse:task.response data:[NSData dataWithData:self.mutableData] error:&serializationError];

            dispatch_group_async(manager.completionGroup ?: url_session_manager_completion_group(), manager.completionQueue ?: dispatch_get_main_queue(), ^{
                if (self.completionHandler) {
                    self.completionHandler(task.response, responseObject, serializationError);
                }

            dispatch_async(dispatch_get_main_queue(), ^{
                [[NSNotificationCenter defaultCenter] postNotificationName:AFNetworkingTaskDidCompleteNotification object:task userInfo:userInfo];
                });
            });
        });

整个流程可由下图表示:

AFNetworking源码分析

流程图.png

所以NSURLSession的实现完全是借鉴了之前的AFNetworking,NSURLSession中维护了两个操作队列,一个用于处理发起请求,等待响应,一个用于处理响应到达后需要执行的回调,对数据进行操作。如果是使用AFHTTPSessionManager的manager方法,初始化session的操作队列的最大并发数为1,则与基于NSURLConnection的AFNetworking完全一样,所有请求和等待响应都在一条线程中执行,然后数据的解析在一个异步并发的操作队列中执行,当然你可以设置该操作队列的最大并发数。

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Wesley13 Wesley13
2年前
java将前端的json数组字符串转换为列表
记录下在前端通过ajax提交了一个json数组的字符串,在后端如何转换为列表。前端数据转化与请求varcontracts{id:'1',name:'yanggb合同1'},{id:'2',name:'yanggb合同2'},{id:'3',name:'yang
Jacquelyn38 Jacquelyn38
2年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Stella981 Stella981
2年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
3个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这