足球盘口软件
当前位置: 足球盘口软件 > 前端 >
那些年我们踩过的坑,电商项目

那些年我们踩过的坑-NSTimer,坑-NSTimer

  昨天下午工作的时候遇见一个这样的需求,网络请求失败后把请求数据保存到本地,并自动重发3次,时间间隔是10秒,如果3次后还失败的话,下一次启动这个接口的时候,把新数据和保存在本地的数据都要发送,刚开始以为没多少难度,不就是网络请求发送数据嘛,首先脑子里的第一反应就是用定时器,初始化定时器,然后触发相应的方法,设置请求的次数标志,超过3次停止定时器。事实却证明我还没有理解定时器......

  由于是老接口,不能修改,因为产品已经上线,修改会涉及到太多业务,所以只能客户端想办法处理。这样导致的问题就是新数据不能和旧数据一起整合在一起发送,得分两次发送。好吧,那就上吧,我就信心满满的上了。

     初始化定时器,遍历本地的数据,分别对应创建一个定时器使用下面的方法,加载到定时器数组

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

然后fire执行。OK,搞定。

bi..bi...bi...bi....bi.....bi......

  擦,定时器全乱了,10s内定时器没啥问题,10s后所以定时器都交替进行。。。这不是坑爹么。。。。

  吸了口气,喝了一杯水,扫了一眼定时器的代码,灵光一闪,会不会是fire用错了,初始化的时候不要立即执行,等初始化完毕的时候在从数组里面拿出定时器,请求成功或者失败三次后再拿出第二个定时器请求。哈哈哈哈哈哈,应该不会错了,就这么办。

bi....bi.....bi.....bi....bi........

  我了个去,稍微好一点了,20秒内的数据是正常的,后面的定时器又交替进行。。。。泥煤呀,甘都得。。。不过已经有进步了,至少20秒是正确的吧,再改改代码应该就可以了,所以立马想一下定时器的执行流程,后来发现会不会是多个定时器和一个定时器的运行是有区别的?因为自己之前基本上都是创建一个定时器就可以了,fire、invalidate使用。没办法,上SOF看看吧。后来才知道原来这两个方法初始化的定时器即使不用fire也会对应的NSTimeInterval后执行,fire只是让他们立即执行,把启动的时间提前到当前,就像一个演唱会本来打算10分钟后开始的,现在因为主唱提前10分钟到了会场,看见粉丝这么热情,提前开始了。

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

  可是问题又来了,那既然这样没办法控制定时器的执行,我这个功能岂不是没法做了,有没有什么办法可以控制定时器么,想执行的时候就执行,不想执行的时候就丢掉它。。。。

  查找资料的过程中还发现了几个初始化定时器的方法:两个类方法,一个实例方法。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

- (id)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep;

  这和上面的初始化方法有什么区别么,接着发现者两个类方法和实例方法是要手动添加到NSRunLoop代码执行的:

[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];

哈哈哈,这不就是我想要的东东嘛(ˇˍˇ),yo yo check now!

修改定时器的方法,手动添加NSRunLoop执行,然后网络请求不变。。。。OK,搞定。。

bi..bi.....bi....bi......

无压力了。。。。测试一个for循环1000次,没发生什么错误。。。好吧,来个总结。

一直都习惯用最上面的两个方法初始化定时器,然后fire,并且fire的作用只是把定时器的时间提前了,这个是之前使用的时候没有去考虑的。。这种东东在一个定时器下面不会有什么问题,但是多个定时器的话基本上就悲剧。。不过在同一个地方使用多个定时器这样的设计方法我暂时也不知道合理不合理,可能也会有意想不到的的错误,比如内存暴涨,性能受影响之类的,这个暂时没有去考虑,如果你有更好的解决方法,可以交流交流。

 

对于iOS开发者来说,就算是没有用过YTKNetwork框架,应该也见过,听过了。它是猿题库技术团队开源的一个网络请求框架,内部封装了AFNetworking。它把每个请求实例化,管理它的生命周期,也可以管理多个请求。

图片 1

下面通过理论讲解,实例分析,效果展示的方式给大家分享下PHP定时执行任务实现方法。

在正式讲解源码之前,我会先讲一下该框架所用的架构和设计模式。我总觉得对架构和设计有一定的了解的话,会有助于对源码的理解。

MVVM架构

定时器任务,在WEB应用比较常见,如何使用PHP实现定时器任务,大致有两种方案:

昨天下午工作的时候遇见一个这样的需求,网络请求失败后把请求数据保存到本地,并自动重发3次,...

先上图:

自然是按照ViewModel、ViewController、View、Model的顺序依次构建。

1)使用Crontab命令,写一个shell脚本,在脚本中调用PHP文件,然后定期执行该脚本;

图片 2YTKRequest架构图

RACObserve循环引用

2)配合使用ignore_user_abort,使脚本脱离浏览器运行。

在这里简单说明一下:

self.rac_willDeallocSignal信号—>block—->self—->rac_willDeallocSignal信号

前者是利用Linux的特性,和PHP本身没有多大关系,后者使用场景有限,且只能由一次HTTP请求触发该脚本,执行完后退出。那么我们如何使用纯PHP实现纯粹的定时器任务,且能适应认识任务业务需求?

  1. YTKNetwork框架将每一个请求实例化,YTKBaseRequest是所有请求类的基类,YTKRequest是它的子类。所以如果我们想要发送一个请求,则需要创建并实例化一个继承于YTKRequest的自定义的请求类(CustomRequest)并发送请求。
  2. YTKNetworkAgent是一个单例,负责管理所有的请求类(例如CustomRequest)。当CustomRequest发送请求以后,会把自己放在YTKNetworkAgent持有的一个字典里,让其管理自己。
  3. 我们说YTKNetwork封装了AFNetworking,实际上是YTKNetworkAgent封装了AFNetworking,由它负责AFNetworking请求的发送和AFNetworking的回调处理。所以如果我们想更换一个第三方网络请求库,就可以在这里更换一下。而YTKRequest更多的是只是负责缓存的处理。
  4. YTKNetworkConfig与YTKPriviate的具体职能现在不做介绍,会在后文给出。
MVVM构造更加轻量化ViewController

此程序在Linux下开发,以cli模式运行,一下是基本知识的简要介绍。

OK,现在我们知道了YTKNetwork中类与类之间的关系以及关键类的大致职能,接下来我会告诉你YTKNetwork为什么会采用这种关系来架构,以及采用这种架构会有什么好处。

  • 控制器里面所有属于事件处理的逻辑和计算通通搬到ViewModel里面去,换句话说,控制器就干两件事情,第一、响应用户的交互;第二,呈现给用户交互结果。

  • 问题的复杂性就在于此,典型的一个购物车,你选中一个商品,这是一个交互,而交互的结果却是多个的,包括总价格要变化,按钮的使能要变化,商品的总数要发生变化,交给ViewModel来处理,好了你至少要计算出三个交互结果,然后三次使用Block或是委托来实现逆向传值,才能把结果ViewModel的处理结果传递到控制器去,如果不是三个而是5个6个,那很有可能带来的结果是,本来是想精简控制器的代码所以设置ViewModel,最后因为事件处理的结果和多,使用了一大堆的Block和委托来传值,反而把控制器给弄乱了,因此很多开发者如果不会使用RAC却使用MVVM的框架是很考验耐心的,还不如直接把事件处理写在控制器来的直接呢,虽然控制器冗余了点,但是还谈不上乱呀。

  • 如果使用RAC来处理控制器和ViewModel之间的协作,所有的问题迎刃而解。原因就是用户交互终究只会触发一个信号,无论最后这个用户交互事件需要呈现多少个连锁反应,所有与这个操作有关的UI元素订阅这个信号不就行了么,一个信号对应多个连锁反应,就是这么神奇。

  • 那么问题来了,都有哪些方法把控制器的事件传递到ViewModel里面去呢 ?现在最常用到的就是第一:RACObserve观察者主要传递控制器的输入内容变化;第二:RACCommand主要用于耗时事件;第三:RACSubject用于立即响应按钮事件,例如简单地跳转个控制器啥的。

CLI:PHP的命令行模式,常见的WEB应用使用的是fpm;进程:进程是程序运行的基本单元,进程之间是独立运行且互不干扰的,有独立的运行空间,每个进程都有一个进程控制块;进程间通信:既然进程是独立运行,我们需要一种机制保证不同进程信息的交换,进程间通信主要包括:管道,IPC,套接字;PCNTL扩展:PHP的一个进程扩展,主要用到pcntl_alarm()函数,详细介绍请查阅官网.实现原理

YTKNetwork框架采用的设计模式是命令模式(Command Pattern)

ViewModel和ViewController的交互

用一个三维数组保存所有需要执行的任务,一级索引为时间戳,值为执行任务的方法、回调参数等,具体数组形式如下:

首先看一下命令模式的定义:

  • ViewModel处理网络请求,网络请求的最终结果通过ViewModelRACComand信号传递到控制器。

  • 控制器持有ViewModel对象,意味着控制器持有ViewModel对象的所有属性,ViewModel对象的属性可是实时通过RACObserver记录控制器的值,对ViewModel的属性做更新监听,一旦属性变化一次,就释放一次信号,进行判断后给控制器一个反馈信号。

  • 完整的逻辑链条就是输入框输入内容,释放以内容为值的信号,ViewModel的属性绑定了这个信号,将输入框值自动绑定了自己的属性上、ViewModel的另一个信号属性又订阅了这个属性的值的变化,一旦变化,则发送一个信号出去,然后控制器的输入框和登录按钮的使能状态又订阅了这个信号、再然后就是一个完整的响应链条,实现控制器最初发送信号,ViewModel处理信号,然后再发出信号,控制器再响应信号的闭环生态。

  • 简单地说,控制器发A信号同时订阅ViewModel处理完A信号发送的B信号。

  • 如果需要及时反馈,那么直接发送RACSingle信号,如果对于A信号的处理是一个异步耗时操作,那么RACCommand命令内嵌RACSingle的形式返回B信号。RACComand里面的信号执行异步耗时操作,返回成功或失败,成功就发送值为字典的信号,失败就发送错误信号。

复制代码 代码如下:array( '1438156396' => array( array(1,array, array

命令模式将请求封装成对象,以便使用不同的请求,队列或者日志来参数化其他对象。命令模式也支持可撤销的操作。摘自:《Head First 设计模式》

BaseViewModel

说明:1438156396 时间戳复制代码 代码如下:array(1,array, array 参数依次表示: 执行时间间隔,回调函数,传递给回调函数的参数,是否持久化(ture则一直保存在数据中,否则执行一次后删除)

看一下命令模式的类图:

  • BaseViewModel的属性包括title、jumpTool、paramsDict。初始化BaseViewModel时就需要传入paramsDict,意义在于self.title = paramsDict[@"title"]给控制器的导航栏标题赋值,除此之外,paramsDict还可以传递其它的参数,当然是以字典键值对的形式进行传递。jumpTool作为一个继承于NSObject的对象,持有navigationController属性值,封装一系列控制器跳转的方法,意义在于处理navigationController或是待跳转viewController不存在时的异常。

  • baseViewModel作为初始化baseViewController的形参,意味着初始化baseViewcontroller之前必须先实例化一个baseViewModel,因为baseViewModel里面就持有baseViewController所需要的所有数据,包括最重要的导航栏标题,这就是相对于属性正向传值的优点,可以在初始化viewController的时候就把数据赋值给控制器。当然为了全局的使用和对传进来的viewModel进一步操作,就需要将初始化控制器传进来的viewModel赋值给self.baseViewModel

  • MVC都是在viewController创建控制器,然后self.navigationController跳转控制器。为了在viewModel里面跳转控制器,这里则是通过ViewModel.jumpTool来调用方法跳转控制器。 一个tabarItem对应着一个navigationController,创建navigationController导航控制器的时候,需要初始化一个继承于BaseViewController的普通控制器作为根控制器,在BaseViewControllerviewDidLoad方法里将navigationController存在baseViewModel.jumpTool.navigation属性里。如此一来,JumpTool控制器跳转帮助类就可以在持有navigationController的基础上随意封装任何形参的控制器跳转方法。只要谁持有了控制器跳转帮助类对象,谁就可以在任意地方执行控制器跳转。

这些任务可以是任意类的方法。既然是定时任务,我们需要一个类似计时的东东,此方案采用信号量去做,每一秒向当前进程发送SIGALRM信号,并捕获该信号,触发信号处理函数,循环遍历数据,判断是否有当前时间需要执行的任务。如果有则采用回调方式触发,并把参数传递给该方法。

图片 3命令模式类图.png

 $arr) { $current = time(); foreach {//遍历每一个任务 $func = $job['func']; /*回调函数*/ $argv = $job['argv']; /*回调函数参数*/ $interval = $job['interval']; /*时间间隔*/ $persist = $job['persist']; /*持久化*/ if {//当前时间有执行任务 //调用回调函数,并传递参数 call_user_func_array; //删除该任务 unset(self::$task[$time][$k]); } if {//如果做持久化,则写入数组,等待下次唤醒 self::$task[$current+$interval][] = $job; } } if(empty { unset; } } } /** *添加任务 */ public static function add($interval, $func, $argv = array { if { return; } $time = time()+$interval; //写入定时任务 self::$task[$time][] = array('func'=>$func, 'argv'=>$argv, 'interval'=>$interval, 'persist'=>$persist); } /** *删除所有定时器任务 */ public function dellAll() { self::$task = array(); } }

图中英文的含义:

  • 继承于BaseViewModel基类的viewmodel都会重写基类的init方法,意义是子类ViewModel相比父类BaseViewModel拥有更多的属性需要初始化,而且必须是在初始化ViewModel对象的同时就来实例子类里面的这些新增属性。扩展这些属性都挺简单的的,能想到的唯一稍微麻烦的地方就是初始化VeiwModel的新增RACComand属性了。

  • UI控件持有ViewModel目的是利用viewModel响应控制器的用户交互事件或者说是为了将触发事件传递到viewModel里,比如按钮点击命令啥的。

这是定时器类核心部分,有一个静态变量保存有所有需要执行的任务,这里为什么是静态的呢?大家自行思考.当进程接受到 SIGALRM 信号后,触发 signalHandler 函数,随后循序遍历数组查看是否有当前时间需要执行的任务,有则回调,并传递参数,删除当前job,随后检查是否要做持久化任务,是则继续将当前job写入事件数组等待下次触发,最后再为当前进程设置一个闹钟信号.可以看出这个定时器,只要触发一次就会从内部再次触发,得到自循环目的.

英文 中文
Command 抽象命令类
ConcreteCommand 命令类的实现类
Invoker 调用者
Receiver 命令接收者
Client 客户端
BaseViewcontroller
 这是回调类及函数,为方便说明,加入不少调试信息.Timer类及回调都有了,我们看看使用场景是怎么样的. 代码非常短,这里注册了两个job,随后运行定时器,在一个无限循环里捕捉信号触发动作,如果不捕获将无法触发事先注册的处理函数.这样一个自循环的定时器开发完成.运行结果如下: 如我们场景类添加的任务一样,在90的时候执行了两个任务,一个为持久化的不带参数的job,一个为非持久化带参数的job,随后非持久化job不再执行.在收到信号前,当前进程不能退出.这里我使用了条件永远为真的循环.在我们实际生产环境中,需要创造这么一个先决条件,比如说,我们有一组服务,这些服务都是一直运行的,不管是IO访问,等待socket链接等等,当前服务都不会终止,即使进程阻塞也不会有问题,这种场景,也就是有一个一直运行的服务中使用.目前PHP只支持以秒为单位的触发,不支持更小时间单位,对位定时任务而言基本足够以上内容就是本文的全部介绍,希望大家喜欢。

详细介绍一下:

  • 首页控制器是通过StoryBoard初始化的,意味着没有按照BaseViewController基类里面的初始化方法进行初始化。添加了右滑退出控制器的手势,这对于控制器的退出是十分重要且必要的。

  • BaseViewController父类里的初始化方法写了重要的一步,传入navigationController参数初始化BaseViewModel属性。自然ViewMode属性值为空,自然在调用父类viewDidLoad方法给BaseViewModel.jumpTool属性赋值无法实现,因此难以跳转。

  • BaseViewController设置ViewModel属性,因为每个子类控制器都会设置这个ViewModel属性,干脆在基类里面设置ViewModel属性,带来的问题就是子类的ViewModel属性本质上与BaseViewController里面的viewModel属性是子类父类的关系,意味着子类控制器去调用BaseViewControllerViewModle属性的方法,会造成水土不服。

  • 解决方法就是在继承于BaseViewController的子类控制器里面声明@dynamic viewModel,如此一来,ViewModel所属Class不再是BaseViewModel,而是属于子类Class。这样来看,并没有起到父类帮子类声明统一属性的方法,如果子类用的属性跟父类用的属性声明的方法相同,自然没问题,可是子类的属性与父类的属性是子类父类的关系,这就需要子类改写父类的成员变量的类型,变量名称不变,Xcode提示你父类和子类声明了重复的成员变量,使用@dynamic告诉Xcode成员变量所属Class的关系就可以了。

  • BaseViewController本来不该写这个属性的,但是不得已而为之,十分需要BaseViewModel属性来保存实例控制器时传进来的ViewModel,必须依赖baseViewController.baseViewModel.jumpTool保存navigationController才能实现控制器的跳转。否则,每一个继承于BaseViewController基类控制器的子类控制器都需要重写一些基类的init方法,不然根本就没法保存初始化控制器时传进来的viewModel。其实后来的通过子类Class再次声明viewModel和@dynamic本质就是为了扩展基类BaseViewControllerviewModel属性的方法和属性。

  1. 命令模式的本质是对命令的封装,将发出命令的责任和执行命令的责任分割开。
  2. 命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求是怎么被接收,以及操作是否被执行、何时被执行,以及是怎么被执行的。

可能还是觉得有点抽象,在这里举一个《Head First 设计模式》里的例子,一个客人在餐厅点餐的过程:

  • 由类方法初始化控制器变成了由类名字符串[[NSClassFromString(ClassName) alloc]来初始化控制器。封装控制器跳转逻辑的唯一原因就是统一规避所有可能会遇到的控制器跳转异常,异常主要有两点:一是导航控制器不存在,二是待跳转的控制器构建不出来。
  1. 你将点的菜写在订单里,交给了服务员。
  2. 服务员将订单交给厨师。
  3. 厨师做好菜之后将做好的菜交给服务员。
  4. 最后服务员把菜递给你。
商品Cell

在这里,命令就好比是订单,而你是命令的发起者。你的命令通过服务员交给了命令的执行者。所以至于这道菜具体是谁做,怎么做,你是不知道的,你做的只是发出命令和接受结果。而且对于餐厅来说,厨师是可以随便换的,而你可能对此一无所知。反过来,厨师只需要好好把菜做好,至于是谁点的菜也不需要他考虑。

  • Cell下方存在加入购物车的逻辑,扯出了购物车管理器、购物车管理器又扯出了用户管理器、用户管理器有扯出了地址管理器、现在迷失在地址管理器,难以自拔!

结合上面命令模式的类图以及餐厅点餐的例子,我们来理清一下YTKNetwork内部的职能

RACComand
场景 Command ConcreteCommand Invoker Receiver Client
餐厅 空白订单 填入菜名的订单 服务员 厨师 客人
YTKNetwork YTKBaseRequest CustomRequest YTKNetworkAgent AFNetworking ViewController/ViewModel
  • UIButton扩展了rac_command的属性,将buttonenable状态与command命令的执行来状态绑定,用户点击按钮时,command命令会自动执行,同时按钮enable置为NO。如果手动执行command命令,则可以发送参数。

  • 使用RACComand命令的时候的传入的参数,这个参数到底是什么地方传进来的,有什么用,就像属性传值是的,只要有RACComand这个对象,就意味着可以把对象传递到RACComand里面去。谁调用excue这个消息,谁就有资格传递信息进行正向传值。

  • 使用RACComand的过程中,如果只是创建了一个信号,那么直接返回这个信号就可以了,同时这个信号在异步操作执行完成之后发送执行结果给那些订阅此信号的人。信号在把消息发送出去的同时,也销毁了信号本身,一旦RACComand检测到内部持有的信号已经销毁,必然改变自己的执行状态,表示信号里面的耗时任务执行完毕。于是RACComand的状态改成了执行完毕。那么现在是一个RACConmand里面三个信号,信号3是一个刷新UI的信号,依赖信号1和信号2去请求数据,信号1和信号2同时成功后再去触发信号3。

可以看到,YTKNetwork对命令模式的实现是很符合其设计标准的,它将请求的发起者和接收者分离开来,可以让我们随时更换接受者。

另外,因为封装了请求,我们既可以管理单个请求,也可以同时管理多个请求,甚至实现琏式请求的发送。关于多个请求的发送,我们也可以想象在餐厅里,你可以在吃的过程中还想起来要吃别的东西,例如点心,饮料之类的,你就可以填多个订单(当然也可以写在一起)交给服务员。

  • RACCommand的初始化必须实现Block参数代码块,而且这个Block参数代码块很特别,既有输入值id类型数据,又有输出值RACSignal,输入值来自于ViewModel操作RACCommand属性执行Excute命令时,可以带一个参数过来,这个参数可以是任何形式,用来区分这个命令操作到底是属于谁。返回值必须是一个信号,就算你什么都不做,也必须在Block参数代码块里面返回一个[RACSignal empty]空信号,这就是一个典型的有输入值有返回值的Block函数形参了。

  • 如果是要在RACCommand事件里面进行一个异步操作,就不能返回空信号了,不能返回空信号,必须返回一个冷信号,冷信号作用就是先把信号创建出来,暂且不发送任何的内容,直接在创建信号后的Block形参代码块里面写入将要执行的异步操作,然后根据异步操作的执行结果通过订阅者subscriber发送不同的信号值,当然我们创建的冷信号RACsignal是需要以RACCommandBlock形参返回值返回出去的。冷信号RACsignalBlock形参的返回值则是一个RACDisposable对象,唯一的意义就是在订阅者subscriber发布错误信号error或是结束信号complete之后会销毁我们创建的RACsignal冷信号,销毁这个信号的同时会进入RACDisposableBlock参数代码块里面。如果需要在冷信号被销毁之后执行某些代码,那么RACDisposable显得特别暖心,平时直接return一下RACDisposable的实例就可以了。

  • 接下来就是逻辑处理异步请求的结果,方式一直接在异步耗时操作的Block回调进行处理;方式二是订阅这个RACSignal冷信号,然后根据不同的信号值,做出处理。

相信到这里,大家应该对YTKNetwork的设计与架构有了足够的认识了,下面进入到真正的源码解析,我们结合一下它的代码来看一下YTKNetwork是如何实现和管理网络请求的。

在真正讲解源码之前,我先详细说一下各个类的职责:

  • 登录注册首先实例RACCommand,后面的Block代码块里面直接就return创建RACSignal冷信号。RACSignalBlock代码块里,就是异步网络请求,同时类方法Block回调网络请求结果,请求成功则subscriber发送YES,反之失败则subscriber发送NO

  • 创建RACsignal冷信号带有一个Block代码块。RACDisposable作为与异步网络请求并行的关系必须作为Block代码块的返回值进行return

  • 订阅RACComandBlock代码块里面创建的RACSignal。方式一subscriber订阅RACCommand.executionSignals.switchToLatest信号;方式二直接在Blcok代码块里subscriber订阅前面创建的RACSignal信号。

  • 获取RACCommand执行状态。方式一subscriber订阅[RACCommand.executing skip:1]信号来判断RACCommand执行状态。方式二直接在创建RACCommand时默认RACCommand执行状态开始,在订阅到RACSignal发出值信号或RACSignal被销毁时默认执行状态结束。

  • subscriber发送的值信号格式。如果网络请求数据成功,就发送@{@"code":@100,@"data":responseObject}这个字典,如果请求数据失败,就发送@{@"code":@400,@"data":@"请求失败"}

类名 职责
YTKBaseRequest 所有请求类的基类。持有NSURLSessionTask实例,responseData,responseObject,error等重要数据,提供一些需要子类实现的与网络请求相关的方法,处理回调的代理和block,命令YTKNetworkAgent发起网络请求。
YTKRequest YTKBaseRequest的子类。负责缓存的处理:请求前查询缓存;请求后写入缓存。
YTKNetworkConfig 被YTKRequest和YTKNetworkAgent访问。负责所有请求的全局配置,例如baseUrl和CDNUrl等等。
YTKNetworkPrivate 提供JSON验证,appVersion等辅助性的方法;给YTKBaseRequest增加一些分类。
YTKNetworkAgent 真正发起请求的类。负责发起请求,结束请求,并持有一个字典来存储正在执行的请求。
YTKBatchRequest 可以发起批量请求,持有一个数组来保存所有的请求类。在请求执行后遍历这个数组来发起请求,如果其中有一个请求返回失败,则认定本组请求失败。
YTKBatchRequestAgent 负责管理多个YTKBatchRequest实例,持有一个数组来保存YTKBatchRequest。支持添加和删除YTKBatchRequest实例。
YTKChainRequest 可以发起链式请求,持有一个数组来保存所有的请求类。当某个请求结束后才能发起下一个请求,如果其中有一个请求返回失败,则认定本请求链失败。
YTKChainRequestAgent 负责管理多个YTKChainRequestAgent实例,持有一个数组来保存YTKChainRequest。支持添加和删除YTKChainRequest实例。
RACSubject

OK,现在知道了YTKNetwork内部的责任分配,下面我们先从单个请求的全部流程来看一下YTKNetwork都做了什么。

  • 通过RACSubject信号把View子视图上的按钮事件传递到ViewModel中区,同时传递的参数主要按钮的标识符Tag号。类似于给View子视图添加按钮事件Block回调。

  • TableView子视图持有ViewModel,在Cell点击回调方法里将Cell点击事件转变成RACSubject信号发送到ViewModel

3.21 单个请求的配置

官方的教程建议我们将请求的全局配置是在AppDelegate.m文件里,设定baseUrl以及cdnUrl等参数。

- application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ YTKNetworkConfig *config = [YTKNetworkConfig sharedConfig]; config.baseUrl = @"http://yuantiku.com"; config.cdnUrl = @"http://fen.bi";}

如果我们需要新建一个注册的请求,则需要创建一个继承于YTKRequest的注册接口的类RegisterApi,并将针对该请求参数配置好:

// RegisterApi.h#import "YTKRequest.h"@interface RegisterApi : YTKRequest- initWithUsername:(NSString *)username password:(NSString *)password;@end// RegisterApi.m#import "RegisterApi.h"@implementation RegisterApi { NSString *_username; NSString *_password;}//初始化的时候将两个参数值传入- initWithUsername:(NSString *)username password:(NSString *)password { self = [super init]; if  { _username = username; _password = password; } return self;}//需要和baseUrl拼接的地址- (NSString *)requestUrl { // “ http://www.yuantiku.com ” 在 YTKNetworkConfig 中设置,这里只填除去域名剩余的网址信息 return @"/iphone/register";}//请求方法,某人是GET- (YTKRequestMethod)requestMethod { return YTKRequestMethodPOST;}//请求体- requestArgument { return @{ @"username": _username, @"password": _password };}@end

现在我们知道如何配置全局的参数和针对某个请求的参数了,接下来看一下单个请求是如何发起的。

购物车数据本地化

3.22 单个请求的发起

还是刚才的注册API,在实例化以后,直接调用startWithCompletionBlockWithSuccess:failure方法(或start方法)就可以发起它:

//LoginViewController.m- loginButtonPressed:sender { NSString *username = self.UserNameTextField.text; NSString *password = self.PasswordTextField.text; if (username.length > 0 && password.length > 0) { RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password]; [api startWithCompletionBlockWithSuccess:^(YTKBaseRequest *request) { // 你可以直接在这里使用 self NSLog(@"succeed"); } failure:^(YTKBaseRequest *request) { // 你可以直接在这里使用 self NSLog(@"failed"); }]; }}

上面是以block的形式回调,YTKNetwork也支持代理的回调:

//LoginViewController.m- loginButtonPressed:sender { NSString *username = self.UserNameTextField.text; NSString *password = self.PasswordTextField.text; if (username.length > 0 && password.length > 0) { RegisterApi *api = [[RegisterApi alloc] initWithUsername:username password:password]; api.delegate = self; [api start]; }}- requestFinished:(YTKBaseRequest *)request { NSLog(@"succeed");}- requestFailed:(YTKBaseRequest *)request { NSLog(@"failed");}

有两点需要注意的是:

  1. 必须给自定义请求类(RegisterApi)调用startWithCompletionBlockWithSuccess:failure方法(或start方法),才能真正发起请求。
  2. 在同时设置了回调代理和回调block的情况下,首先回调的是回调代理方法,然后再走回调block。

知道了YTKRequest请求是如何在外部发起的,我们现在从startWithCompletionBlockWithSuccess:failure方法开始,来看一下YTKNetwork都做了什么:

首先来到YTKBaseRequest类(因为最早是由它定义的该方法):

//YTKBaseRequest.m//传入成功和失败的block,并保存起来- startWithCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success failure:(YTKRequestCompletionBlock)failure { //保存成功和失败的回调block,便于将来调用 [self setCompletionBlockWithSuccess:success failure:failure]; //发起请求 [self start];}//保存成功和失败的block- setCompletionBlockWithSuccess:(YTKRequestCompletionBlock)success failure:(YTKRequestCompletionBlock)failure { self.successCompletionBlock = success; self.failureCompletionBlock = failure;}

当保存完成功和失败的block以后,调用start方法,于是来到了YTKRequest类(注意,虽然YTKBaseRequest也实现了start方法,但是由于YTKRequest类是它的子类并也实现了start方法,所以这里最先走的是YTKRequest类的start方法):

//YTKRequest.m- start { //1. 如果忽略缓存 -> 请求 if (self.ignoreCache) { [self startWithoutCache]; return; } //2. 如果存在下载未完成的文件 -> 请求 if (self.resumableDownloadPath) { [self startWithoutCache]; return; } //3. 获取缓存失败 -> 请求 if (![self loadCacheWithError:nil]) { [self startWithoutCache]; return; } //4. 到这里,说明一定能拿到可用的缓存,可以直接回调了(因为一定能拿到可用的缓存,所以一定是调用成功的block和代理) _dataFromCache = YES; dispatch_async(dispatch_get_main_queue(), ^{ //5. 回调之前的操作 //5.1 缓存处理 [self requestCompletePreprocessor]; //5.2 用户可以在这里进行真正回调前的操作 [self requestCompleteFilter]; YTKRequest *strongSelf = self; //6. 执行回调 //6.1 请求完成的代理 [strongSelf.delegate requestFinished:strongSelf]; //6.2 请求成功的block if (strongSelf.successCompletionBlock) { strongSelf.successCompletionBlock(strongSelf); } //7. 把成功和失败的block都设置为nil,避免循环引用 [strongSelf clearCompletionBlock]; });}

我们之前说过YTKRequest负责缓存的相关处理,所以在上面这个start方法里,它做的是请求之前缓存的查询和检查工作:

  • 如果忽略缓存,或者缓存获取失败,调用startWithoutCache方法,发起请求。
  • 如果能成功获取到缓存,则直接回调。

我们来看一下每一步的具体实现:

  1. ignoreCache属性是用户手动设置的,如果用户强制忽略缓存,则无论是否缓存是否存在,直接发送请求。
  2. resumableDownloadPath是断点下载路径,如果该路径不为空,说明有未完成的下载任务,则直接发送请求继续下载。
  3. loadCacheWithError:方法验证了加载缓存是否成功的方法(返回值为YES,说明可以加载缓存;反之亦然),看一下具体实现:
//YTKRequest.m- loadCacheWithError:(NSError * _Nullable __autoreleasing *)error { // 缓存时间小于0,则返回(缓存时间默认为-1,需要用户手动设置,单位是秒) if ([self cacheTimeInSeconds] < 0) { if  { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheTime userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache time"}]; } return NO; } // 是否有缓存的元数据,如果没有,返回错误 if (![self loadCacheMetadata]) { if  { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidMetadata userInfo:@{ NSLocalizedDescriptionKey:@"Invalid metadata. Cache may not exist"}]; } return NO; } // 有缓存,再验证是否有效 if (![self validateCacheWithError:error]) { return NO; } // 有缓存,而且有效,再验证是否能取出来 if (![self loadCacheData]) { if  { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorInvalidCacheData userInfo:@{ NSLocalizedDescriptionKey:@"Invalid cache data"}]; } return NO; } return YES;}

先讲一下什么是元数据:元数据是指数据的数据,在这里描述了缓存数据本身的一些特征:包括版本号,缓存时间,敏感信息等等, 稍后会做详细介绍。

我们来看一下上面关于缓存的元数据的获取方法:loadCacheMetadata方法

//YTKRequest.m- loadCacheMetadata { NSString *path = [self cacheMetadataFilePath]; NSFileManager * fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:path isDirectory:nil]) { @try { //将序列化之后被保存在磁盘里的文件反序列化到当前对象的属性cacheMetadata _cacheMetadata = [NSKeyedUnarchiver unarchiveObjectWithFile:path]; return YES; } @catch (NSException *exception) { YTKLog(@"Load cache metadata failed, reason = %@", exception.reason); return NO; } } return NO;}

cacheMetadata(YTKCacheMetadata) 是当前reqeust类用来保存缓存元数据的属性。YTKCacheMetadata类被定义在YTKRequest.m文件里面:

//YTKRequest.m@interface YTKCacheMetadata : NSObject<NSSecureCoding>@property (nonatomic, assign) long long version;@property (nonatomic, strong) NSString *sensitiveDataString;@property (nonatomic, assign) NSStringEncoding stringEncoding;@property (nonatomic, strong) NSDate *creationDate;@property (nonatomic, strong) NSString *appVersionString;@end

它描述的是缓存的版本号,敏感信息,创建时间,app版本等信息,并支持序列化处理,可以保存在磁盘里。因此,loadCacheMetadata方法的目的是将之前被序列化保存的缓存元数据信息反序列化,赋给自身的cacheMetadata属性上。

现在获取了缓存的元数据并赋给了自身的cacheMetadata属性上,那么接下来就要逐一验证元数据里的各项信息是否符合要求,在下面的validateCacheWithError:里面验证:

//YTKRequest.m- validateCacheWithError:(NSError * _Nullable __autoreleasing *)error { // 是否大于过期时间 NSDate *creationDate = self.cacheMetadata.creationDate; NSTimeInterval duration = -[creationDate timeIntervalSinceNow]; if (duration < 0 || duration > [self cacheTimeInSeconds]) { if  { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorExpired userInfo:@{ NSLocalizedDescriptionKey:@"Cache expired"}]; } return NO; } // 缓存的版本号是否符合 long long cacheVersionFileContent = self.cacheMetadata.version; if (cacheVersionFileContent != [self cacheVersion]) { if  { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache version mismatch"}]; } return NO; } // 敏感信息是否符合 NSString *sensitiveDataString = self.cacheMetadata.sensitiveDataString; NSString *currentSensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description; if (sensitiveDataString || currentSensitiveDataString) { // If one of the strings is nil, short-circuit evaluation will trigger if (sensitiveDataString.length != currentSensitiveDataString.length || ![sensitiveDataString isEqualToString:currentSensitiveDataString]) { if  { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorSensitiveDataMismatch userInfo:@{ NSLocalizedDescriptionKey:@"Cache sensitive data mismatch"}]; } return NO; } } // app的版本是否符合 NSString *appVersionString = self.cacheMetadata.appVersionString; NSString *currentAppVersionString = [YTKNetworkUtils appVersionString]; if (appVersionString || currentAppVersionString) { if (appVersionString.length != currentAppVersionString.length || ![appVersionString isEqualToString:currentAppVersionString]) { if  { *error = [NSError errorWithDomain:YTKRequestCacheErrorDomain code:YTKRequestCacheErrorAppVersionMismatch userInfo:@{ NSLocalizedDescriptionKey:@"App version mismatch"}]; } return NO; } } return YES;}

如果每项元数据信息都能通过,再在loadCacheData方法里面验证缓存是否能被取出来:

//YTKRequest.m- loadCacheData { NSString *path = [self cacheFilePath]; NSFileManager *fileManager = [NSFileManager defaultManager]; NSError *error = nil; if ([fileManager fileExistsAtPath:path isDirectory:nil]) { NSData *data = [NSData dataWithContentsOfFile:path]; _cacheData = data; _cacheString = [[NSString alloc] initWithData:_cacheData encoding:self.cacheMetadata.stringEncoding]; switch (self.responseSerializerType) { case YTKResponseSerializerTypeHTTP: // Do nothing. return YES; case YTKResponseSerializerTypeJSON: _cacheJSON = [NSJSONSerialization JSONObjectWithData:_cacheData options:(NSJSONReadingOptions)0 error:&error]; return error == nil; case YTKResponseSerializerTypeXMLParser: _cacheXML = [[NSXMLParser alloc] initWithData:_cacheData]; return YES; } } return NO;}

如果通过了最终的考验,则说明当前请求对应的缓存是符合各项要求并可以被成功取出,也就是可以直接进行回调了。

当确认缓存可以成功取出后,手动设置dataFromCache属性为 YES,说明当前的请求结果是来自于缓存,而没有通过网络请求。

然后在真正回调之前做了如下处理:

//YTKRequest.m:- start{ .... //5. 回调之前的操作 //5.1 缓存处理 [self requestCompletePreprocessor]; //5.2 用户可以在这里进行真正回调前的操作 [self requestCompleteFilter]; ....}

5.1:requestCompletePreprocessor方法:

//YTKRequest.m:- requestCompletePreprocessor { [super requestCompletePreprocessor]; //是否异步将responseData写入缓存(写入缓存的任务放在专门的队列ytkrequest_cache_writing_queue进行) if (self.writeCacheAsynchronously) { dispatch_async(ytkrequest_cache_writing_queue(), ^{ //保存响应数据到缓存 [self saveResponseDataToCacheFile:[super responseData]]; }); } else { //保存响应数据到缓存 [self saveResponseDataToCacheFile:[super responseData]]; }}

//YTKRequest.m://保存响应数据到缓存- saveResponseDataToCacheFile:data { if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) { if (data != nil) { @try { // New data will always overwrite old data. [data writeToFile:[self cacheFilePath] atomically:YES]; YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init]; metadata.version = [self cacheVersion]; metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description; metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self]; metadata.creationDate = [NSDate date]; metadata.appVersionString = [YTKNetworkUtils appVersionString]; [NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]]; } @catch (NSException *exception) { YTKLog(@"Save cache failed, reason = %@", exception.reason); } } }}

我们可以看到, requestCompletePreprocessor方法的任务是将响应数据保存起来,也就是做缓存。但是,缓存的保存有两个条件,一个是需要cacheTimeInSeconds方法返回正整数(缓存时间,单位是秒,后续会详细说明);另一个条件是isDataFromCache方法返回NO。但是我们知道,如果缓存可用,就会将这个属性设置为YES,所以走到这里的时候,就不做缓存了。

接着看下5.2:requestCompleteFilter方法则是需要用户自己提供具体实现的,专门作为回调成功之前的一些处理:

//YTKBaseRequest.m- requestCompleteFilter {}

到这里,回调之前的处理都结束了,下面来看一下在缓存可用的情况下的回调:

//YTKRequest.m- start{ ... YTKRequest *strongSelf = self; //6. 执行回调 //6.1 请求完成的代理 [strongSelf.delegate requestFinished:strongSelf]; //6.2 请求成功的block if (strongSelf.successCompletionBlock) { strongSelf.successCompletionBlock(strongSelf); } //7. 把成功和失败的block都设置为nil,避免循环引用 [strongSelf clearCompletionBlock];}

我们可以看到 ,这里面同时存在两种回调:代理的回调和block的回调。先执行的是代理的回调,然后执行的是block的回调。而且在回调结束之后,YTKNetwork会帮助我们清空回调的block:

//YTKBaseRequest.m- clearCompletionBlock { // 清空请求结束的block,避免循环引用 self.successCompletionBlock = nil; self.failureCompletionBlock = nil;}

注意,在用户同时实现了代理和block的情况下,二者都会被调用。

到这里,我们了解了YTKNetwork在网络请求之前是如何验证缓存,以及在缓存有效的情况下是如何回调的。

反过来,如果缓存无效时,需要立即请求网络。那么我们现在来看一看在这个时候YTKNetwork都做了什么:

仔细看一下上面的start方法,我们会发现,如果缓存不满足条件时,会直接调用startWithoutCache方法:

//YTKRequest.m- start{ //1. 如果忽略缓存 -> 请求 if (self.ignoreCache) { [self startWithoutCache]; return; } //2. 如果存在下载未完成的文件 -> 请求 if (self.resumableDownloadPath) { [self startWithoutCache]; return; } //3. 获取缓存失败 -> 请求 if (![self loadCacheWithError:nil]) { [self startWithoutCache]; return; } ......}

那么在startWithoutCache方法里都做了什么呢?

//YTKRequest.m- startWithoutCache { //1. 清除缓存 [self clearCacheVariables]; //2. 调用父类的发起请求 [super start];}//清除当前请求对应的所有缓存- clearCacheVariables { _cacheData = nil; _cacheXML = nil; _cacheJSON = nil; _cacheString = nil; _cacheMetadata = nil; _dataFromCache = NO;}

在这里,首先清除了关于缓存的所有数据,然后调用父类的start方法:

//YTKBaseRequest.m:- start { //1. 告诉Accessories即将回调了(其实是即将发起请求) [self toggleAccessoriesWillStartCallBack]; //2. 令agent添加请求并发起请求,在这里并不是组合关系,agent只是一个单例 [[YTKNetworkAgent sharedAgent] addRequest:self];}

第一步里的Accessories是一些遵从<YTKRequestAccessory>代理的对象。这个代理定义了一些用来追踪请求状况的方法。它被定义在了YTKBaseRequest.h文件里:

//用来跟踪请求的状态的代理。@protocol YTKRequestAccessory <NSObject>@optional/// Inform the accessory that the request is about to start.////// @param request The corresponding request.- requestWillStart:request;/// Inform the accessory that the request is about to stop. This method is called/// before executing `requestFinished` and `successCompletionBlock`.////// @param request The corresponding request.- requestWillStop:request;/// Inform the accessory that the request has already stoped. This method is called/// after executing `requestFinished` and `successCompletionBlock`.////// @param request The corresponding request.- requestDidStop:request;@end

所以只要某个对象遵从了这个代理,就可以追踪到请求将要开始,将要结束,已经结束的状态。

接着看一下第二步:YTKNetworkAgent把当前的请求对象添加到了自己身上并发送请求。来看一下它的具体实现:

//YTKNetworkAgent.m- addRequest:(YTKBaseRequest *)request { //1. 获取task NSParameterAssert(request != nil); NSError * __autoreleasing requestSerializationError = nil; //获取用户自定义的requestURL NSURLRequest *customUrlRequest= [request buildCustomUrlRequest]; if (customUrlRequest) { __block NSURLSessionDataTask *dataTask = nil; //如果存在用户自定义request,则直接走AFNetworking的dataTaskWithRequest:方法 dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { //响应的统一处理 [self handleRequestResult:dataTask responseObject:responseObject error:error]; }]; request.requestTask = dataTask; } else { //如果用户没有自定义url,则直接走这里 request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError]; } //序列化失败,则认定为请求失败 if (requestSerializationError) { //请求失败的处理 [self requestDidFailWithRequest:request error:requestSerializationError]; return; } NSAssert(request.requestTask != nil, @"requestTask should not be nil"); // 优先级的映射 // !!Available on iOS 8 + if ([request.requestTask respondsToSelector:@selector]) { switch (request.requestPriority) { case YTKRequestPriorityHigh: request.requestTask.priority = NSURLSessionTaskPriorityHigh; break; case YTKRequestPriorityLow: request.requestTask.priority = NSURLSessionTaskPriorityLow; break; case YTKRequestPriorityDefault: /*!!fall through*/ default: request.requestTask.priority = NSURLSessionTaskPriorityDefault; break; } } // Retain request YTKLog(@"Add request: %@", NSStringFromClass([request class])); //2. 将request放入保存请求的字典中,taskIdentifier为key,request为值 [self addRequestToRecord:request]; //3. 开始task [request.requestTask resume];}

这个方法挺长的,但是请不要被吓到,它总共分为三个部分:

  • 第一部分是获取当前请求对应的task并赋给request的requestTask属性(以后提到的request,都为用户自定义的当前请求类的实例)。
  • 第二部分是把request放入专门用来保存请求的字典中,key为taskIdentifier。
  • 第三部分是启动task。

下面我来依次讲解每个部分:

第一部分:获取当前请求对应的task并赋给request

//YTKNetworkAgent.m- addRequest:(YTKBaseRequest *)request { ... if (customUrlRequest) { __block NSURLSessionDataTask *dataTask = nil; //如果存在用户自定义request,则直接走AFNetworking的dataTaskWithRequest:方法 dataTask = [_manager dataTaskWithRequest:customUrlRequest completionHandler:^(NSURLResponse * _Nonnull response, id _Nullable responseObject, NSError * _Nullable error) { //统一处理请求响应 [self handleRequestResult:dataTask responseObject:responseObject error:error]; }]; request.requestTask = dataTask; } else { //如果用户没有自定义url,则直接走这里 request.requestTask = [self sessionTaskForRequest:request error:&requestSerializationError]; } ...}

在这里判断了用户是否自定义了request:

  1. 如果是,则直接调用AFNetworking的dataTaskWithRequest:方法。
  2. 如果不是,则调用YTKRequest自己的生成task的方法。

第一种情况就不说了,因为AF帮我们做好了。在这里看一下第二种情况,sessionTaskForRequest: error :方法内部:

//YTKNetworkAgent.m//根据不同请求类型,序列化类型,和请求参数来返回NSURLSessionTask- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error { //1. 获得请求类型(GET,POST等) YTKRequestMethod method = [request requestMethod]; //2. 获得请求url NSString *url = [self buildRequestUrl:request]; //3. 获得请求参数 id param = request.requestArgument; AFConstructingBlock constructingBlock = [request constructingBodyBlock]; //4. 获得request serializer AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request]; //5. 根据不同的请求类型来返回对应的task switch  { case YTKRequestMethodGET: if (request.resumableDownloadPath) { //下载任务 return [self downloadTaskWithDownloadPath:request.resumableDownloadPath requestSerializer:requestSerializer URLString:url parameters:param progress:request.resumableDownloadProgressBlock error:error]; } else { //普通get请求 return [self dataTaskWithHTTPMethod:@"GET" requestSerializer:requestSerializer URLString:url parameters:param error:error]; } case YTKRequestMethodPOST: //POST请求 return [self dataTaskWithHTTPMethod:@"POST" requestSerializer:requestSerializer URLString:url parameters:param constructingBodyWithBlock:constructingBlock error:error]; case YTKRequestMethodHEAD: //HEAD请求 return [self dataTaskWithHTTPMethod:@"HEAD" requestSerializer:requestSerializer URLString:url parameters:param error:error]; case YTKRequestMethodPUT: //PUT请求 return [self dataTaskWithHTTPMethod:@"PUT" requestSerializer:requestSerializer URLString:url parameters:param error:error]; case YTKRequestMethodDELETE: //DELETE请求 return [self dataTaskWithHTTPMethod:@"DELETE" requestSerializer:requestSerializer URLString:url parameters:param error:error]; case YTKRequestMethodPATCH: //PATCH请求 return [self dataTaskWithHTTPMethod:@"PATCH" requestSerializer:requestSerializer URLString:url parameters:param error:error]; }}

从这个方法最后的switch语句可以看出,这个方法的作用是返回当前request的NSURLSessionTask的实例。而且最终生成NSURLSessionTask实例的方法都是通过dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:error:这个私有方法来实现的。在讲解这个关键的私有方法之前,先来逐步讲解一下这个私有方法需要的每个参数的获取方法:

  1. 获得请求类型(GET,POST等):
//YTKNetworkAgent.m- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error { ... YTKRequestMethod method = [request requestMethod]; ...}

requestMethod方法最初在YTKBaseRequest里面已经实现了,默认返回了YTKRequestMethodGET。

它的枚举类型在YTKBaseRequest.h里面定义:

//YTKBaseRequest.h/// HTTP Request method.typedef NS_ENUM(NSInteger, YTKRequestMethod) { YTKRequestMethodGET = 0, YTKRequestMethodPOST, YTKRequestMethodHEAD, YTKRequestMethodPUT, YTKRequestMethodDELETE, YTKRequestMethodPATCH,};

用户可以根据实际的需求在自定义request类里面重写这个方法:

//RegisterAPI.m- (YTKRequestMethod)requestMethod { return YTKRequestMethodPOST;}

2.获得请求url:

//YTKNetworkAgent.m- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error { ... NSString *url = [self buildRequestUrl:request]; ...}//返回当前请求url- (NSString *)buildRequestUrl:(YTKBaseRequest *)request { NSParameterAssert(request != nil); //用户自定义的url(不包括在YTKConfig里面设置的base_url) NSString *detailUrl = [request requestUrl]; NSURL *temp = [NSURL URLWithString:detailUrl]; // 存在host和scheme的url立即返回正确 if (temp && temp.host && temp.scheme) { return detailUrl; } // 如果需要过滤url,则过滤 NSArray *filters = [_config urlFilters]; for (id<YTKUrlFilterProtocol> f in filters) { detailUrl = [f filterUrl:detailUrl withRequest:request]; } NSString *baseUrl; if ([request useCDN]) { //如果使用CDN,在当前请求没有配置CDN地址的情况下,返回全局配置的CDN if ([request cdnUrl].length > 0) { baseUrl = [request cdnUrl]; } else { baseUrl = [_config cdnUrl]; } } else { //如果使用baseUrl,在当前请求没有配置baseUrl,返回全局配置的baseUrl if ([request baseUrl].length > 0) { baseUrl = [request baseUrl]; } else { baseUrl = [_config baseUrl]; } } // 如果末尾没有/,则在末尾添加一个/ NSURL *url = [NSURL URLWithString:baseUrl]; if (baseUrl.length > 0 && ![baseUrl hasSuffix:@"/"]) { url = [url URLByAppendingPathComponent:@""]; } return [NSURL URLWithString:detailUrl relativeToURL:url].absoluteString;}

3.获得请求参数

//YTKNetworkAgent.m- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error { ... //获取用户提供的请求参数 id param = request.requestArgument; //获取用户提供的构造请求体的block AFConstructingBlock constructingBlock = [request constructingBodyBlock]; ...}

在这里,requestArgument是一个get方法,需要用户自己定义请求体,例如在RegisterAPI里面就定义了两个请求参数:

//RegisterApi.m- requestArgument { return @{ @"username": _username, @"password": _password };}

4.获得request serializer

//YTKNetworkAgent.m- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error { ... //4. 获得request serializer AFHTTPRequestSerializer *requestSerializer = [self requestSerializerForRequest:request]; ...}- (AFHTTPRequestSerializer *)requestSerializerForRequest:(YTKBaseRequest *)request { AFHTTPRequestSerializer *requestSerializer = nil; //HTTP or JSON if (request.requestSerializerType == YTKRequestSerializerTypeHTTP) { requestSerializer = [AFHTTPRequestSerializer serializer]; } else if (request.requestSerializerType == YTKRequestSerializerTypeJSON) { requestSerializer = [AFJSONRequestSerializer serializer]; } //超时时间 requestSerializer.timeoutInterval = [request requestTimeoutInterval]; //是否允许数据服务 requestSerializer.allowsCellularAccess = [request allowsCellularAccess]; //如果当前请求需要验证 NSArray<NSString *> *authorizationHeaderFieldArray = [request requestAuthorizationHeaderFieldArray]; if (authorizationHeaderFieldArray != nil) { [requestSerializer setAuthorizationHeaderFieldWithUsername:authorizationHeaderFieldArray.firstObject password:authorizationHeaderFieldArray.lastObject]; } //如果当前请求需要自定义 HTTPHeaderField NSDictionary<NSString *, NSString *> *headerFieldValueDictionary = [request requestHeaderFieldValueDictionary]; if (headerFieldValueDictionary != nil) { for (NSString *httpHeaderField in headerFieldValueDictionary.allKeys) { NSString *value = headerFieldValueDictionary[httpHeaderField]; [requestSerializer setValue:value forHTTPHeaderField:httpHeaderField]; } } return requestSerializer;}

上面这个方法通过传入的request实例,根据它的一些配置来获取AFHTTPRequestSerializer的实例。

到现在为止,获取NSURLSessionTask实例的几个参数都拿到了,剩下的就是调用dataTaskWithHTTPMethod:requestSerializer:URLString:parameters:error:方法来获取NSURLSessionTask实例了。我们来看一下这个方法的具体实现:

//YTKNetworkAgent.m- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method requestSerializer:(AFHTTPRequestSerializer *)requestSerializer URLString:(NSString *)URLString parameters:parameters error:(NSError * _Nullable __autoreleasing *)error { return [self dataTaskWithHTTPMethod:method requestSerializer:requestSerializer URLString:URLString parameters:parameters constructingBodyWithBlock:nil error:error];}//最终返回NSURLSessionDataTask实例- (NSURLSessionDataTask *)dataTaskWithHTTPMethod:(NSString *)method requestSerializer:(AFHTTPRequestSerializer *)requestSerializer URLString:(NSString *)URLString parameters:parameters constructingBodyWithBlock:(nullable void (id <AFMultipartFormData> formData))block error:(NSError * _Nullable __autoreleasing *)error { NSMutableURLRequest *request = nil; //根据有无构造请求体的block的情况来获取request if  { request = [requestSerializer multipartFormRequestWithMethod:method URLString:URLString parameters:parameters constructingBodyWithBlock:block error:error]; } else { request = [requestSerializer requestWithMethod:method URLString:URLString parameters:parameters error:error]; } //获得request以后来获取dataTask __block NSURLSessionDataTask *dataTask = nil; dataTask = [_manager dataTaskWithRequest:request completionHandler:^(NSURLResponse * __unused response, id responseObject, NSError *_error) { //响应的统一处理 [self handleRequestResult:dataTask responseObject:responseObject error:_error]; }]; return dataTask;}

这两个方法,上面的方法调用了下面的来获取最终的NSURLSessionDataTask实例。

OK,现在我们已经知道了NSURLSessionDataTask实例是如何获取的,再来看一下在addRequest:方法里接下来做的是对序列化失败的处理:

//YTKNetworkAgent.m- addRequest:(YTKBaseRequest *)request { ... //序列化失败 if (requestSerializationError) { //请求失败的处理 [self requestDidFailWithRequest:request error:requestSerializationError]; return; } ...} 

requestDidFailWithRequest:方法专门处理请求失败的情况,因为它被包含在统一处理请求回调的方法中,所以在稍后会在讲解统一处理请求回调的方法的时候再详细讲解这个方法。

继续往下走,到了优先级的映射部分:

//YTKNetworkAgent.m- addRequest:(YTKBaseRequest *)request { ... // 优先级的映射 // !!Available on iOS 8 + if ([request.requestTask respondsToSelector:@selector]) { switch (request.requestPriority) { case YTKRequestPriorityHigh: request.requestTask.priority = NSURLSessionTaskPriorityHigh; break; case YTKRequestPriorityLow: request.requestTask.priority = NSURLSessionTaskPriorityLow; break; case YTKRequestPriorityDefault: /*!!fall through*/ default: request.requestTask.priority = NSURLSessionTaskPriorityDefault; break; } } ...} 

requestPriority是YTKBaseRequest的一个枚举属性,它的枚举在YTKBaseRequest.h里面被定义:

typedef NS_ENUM(NSInteger, YTKRequestPriority) { YTKRequestPriorityLow = -4L, YTKRequestPriorityDefault = 0, YTKRequestPriorityHigh = 4,};

在这里,将用户设置的YTKRequestPriority映射到NSURLSessionTask的priority上。

到这里,我们拿到了task的实例并设置好了优先级,紧接着就是addRequest:方法里的第二个部分:YTKNetworkAgent将request实例放在了一个字典中,保存起来:

第二部分:把request放入专门用来保存请求的字典中,key为taskIdentifier:

//YTKNetworkAgent.m- addRequest:(YTKBaseRequest *)request { ... ... //将request实例放入保存请求的字典中,taskIdentifier为key,request为值 [self addRequestToRecord:request]; ...}- addRequestToRecord:(YTKBaseRequest *)request { //加锁 Lock(); _requestsRecord[@(request.requestTask.taskIdentifier)] = request; Unlock();}#define Lock() pthread_mutex_lock(&_lock)#define Unlock() pthread_mutex_unlock(&_lock)

可以看到,在添加前和添加后是进行了加锁和解锁的处理的。而且request实例被保存的时候,将其task的identifier作为key来保存。

在当前的request被保存以后,就到了最后一步,正式发起请求:

第三部分:启动task

//YTKNetworkAgent.m- (NSURLSessionTask *)sessionTaskForRequest:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error { ... [request.requestTask resume]; ...}

到现在为止,我们了解了YTKNetwork里面,一个请求开始之前做的事情:查找可用缓存,生成NSURLSessionTask实例,获取url,requestSerializer,将request放到YTKNetworkAgent的一个字典里等等(详细流程会在稍后给出)。

那么接下来我们看一下YTKNetwork是如何处理请求的回调的。

眼尖的同学们可能会注意到,在获取NSURLSessionTask实例的时候,出现了两次“响应的统一处理”的注释,大家可以搜索这个注释就可以找到这个方法:handleRequestResult:responseObject:error:。这个方法负责的是对请求回调的处理,当然包括了成功和失败的情况。我们来看一下在这个方法里都做了什么:

//YTKNetworkAgent.m//统一处理请求结果,包括成功和失败的情况- handleRequestResult:(NSURLSessionTask *)task responseObject:responseObject error:(NSError *)error { //1. 获取task对应的request Lock(); YTKBaseRequest *request = _requestsRecord[@(task.taskIdentifier)]; Unlock(); //如果不存在对应的request,则立即返回 if  { return; } 。。。 //2. 获取request对应的response request.responseObject = responseObject; //3. 获取responseObject,responseData和responseString if ([request.responseObject isKindOfClass:[NSData class]]) { //3.1 获取 responseData request.responseData = responseObject; //3.2 获取responseString request.responseString = [[NSString alloc] initWithData:responseObject encoding:[YTKNetworkUtils stringEncodingWithRequest:request]]; //3.3 获取responseObject(或responseJSONObject) //根据返回的响应的序列化的类型来得到对应类型的响应 switch (request.responseSerializerType) { case YTKResponseSerializerTypeHTTP: // Default serializer. Do nothing. break; case YTKResponseSerializerTypeJSON: request.responseObject = [self.jsonResponseSerializer responseObjectForResponse:task.response data:request.responseData error:&serializationError]; request.responseJSONObject = request.responseObject; break; case YTKResponseSerializerTypeXMLParser: request.responseObject = [self.xmlParserResponseSerialzier responseObjectForResponse:task.response data:request.responseData error:&serializationError]; break; } } //4. 判断是否有错误,将错误对象赋值给requestError,改变succeed的布尔值。目的是根据succeed的值来判断到底是进行成功的回调还是失败的回调 if  { //如果该方法传入的error不为nil succeed = NO; requestError = error; } else if (serializationError) { //如果序列化失败了 succeed = NO; requestError = serializationError; } else { //即使没有error而且序列化通过,也要验证request是否有效 succeed = [self validateResult:request error:&validationError]; requestError = validationError; } //5. 根据succeed的布尔值来调用相应的处理 if  { //请求成功的处理 [self requestDidSucceedWithRequest:request]; } else { //请求失败的处理 [self requestDidFailWithRequest:request error:requestError]; } //6. 回调完成的处理 dispatch_async(dispatch_get_main_queue(), ^{ //6.1 在字典里移除当前request [self removeRequestFromRecord:request]; //6.2 清除所有block [request clearCompletionBlock]; });}

简单讲解一下上面的代码:

  • 首先通过task的identifier值从YTKNetworkAgent保存的字典里获取对应的请求。
  • 然后将获得的responseObject进行处理,将处理后获得的responseObject,responseData和responseString赋值给当前的请求实例request。
  • 再根据这些值的获取情况来判断最终回调的成败(改变succeed的值)。
  • 最后根据succeed的值来进行成功和失败的回调。

这里先重点介绍一下是如何判断json的有效性的:

//YTKNetworkAgent.m//判断code是否符合范围和json的有效性- validateResult:(YTKBaseRequest *)request error:(NSError * _Nullable __autoreleasing *)error { //1. 判断code是否在200~299之间 BOOL result = [request statusCodeValidator]; if  { if  { *error = [NSError errorWithDomain:YTKRequestValidationErrorDomain code:YTKRequestValidationErrorInvalidStatusCode userInfo:@{NSLocalizedDescriptionKey:@"Invalid status code"}]; } return result; } //2. result 存在的情况判断json是否有效 id json = [request responseJSONObject]; id validator = [request jsonValidator]; if (json && validator) { //通过json和validator来判断json是否有效 result = [YTKNetworkUtils validateJSON:json withValidator:validator]; //如果json无效 if  { if  { *error = [NSError errorWithDomain:YTKRequestValidationErrorDomain code:YTKRequestValidationErrorInvalidJSONFormat userInfo:@{NSLocalizedDescriptionKey:@"Invalid JSON format"}]; } return result; } } return YES;}

在这里,首先,用statusCodeValidator方法判断响应的code是否在正确的范围:

//YTKBaseReqiest.m- statusCodeValidator { NSInteger statusCode = [self responseStatusCode]; return (statusCode >= 200 && statusCode <= 299);}- (NSInteger)responseStatusCode { return self.response.statusCode;}

然后再判断json的有效性:

//YTKNetworkUtils.m//判断json的有效性+ validateJSON:json withValidator:jsonValidator { if ([json isKindOfClass:[NSDictionary class]] && [jsonValidator isKindOfClass:[NSDictionary class]]) { NSDictionary * dict = json; NSDictionary * validator = jsonValidator; BOOL result = YES; NSEnumerator * enumerator = [validator keyEnumerator]; NSString * key; while ((key = [enumerator nextObject]) != nil) { id value = dict[key]; id format = validator[key]; if ([value isKindOfClass:[NSDictionary class]] || [value isKindOfClass:[NSArray class]]) { result = [self validateJSON:value withValidator:format]; if  { break; } } else { if ([value isKindOfClass:format] == NO && [value isKindOfClass:[NSNull class]] == NO) { result = NO; break; } } } return result; } else if ([json isKindOfClass:[NSArray class]] && [jsonValidator isKindOfClass:[NSArray class]]) { NSArray * validatorArray = (NSArray *)jsonValidator; if (validatorArray.count > 0) { NSArray * array = json; NSDictionary * validator = jsonValidator[0]; for (id item in array) { BOOL result = [self validateJSON:item withValidator:validator]; if  { return NO; } } } return YES; } else if ([json isKindOfClass:jsonValidator]) { return YES; } else { return NO; }}

注意,YTKNetworkUtils这个类是在YTKNetworkPirvate里面定义的,YTKNetworkPirvate里面有一些工具类的方法,在后面还会遇到。

在验证返回的JSON数据是否有效以后,就可以进行回调了:

//YTKNetworkAgent.m- handleRequestResult:(NSURLSessionTask *)task responseObject:responseObject error:(NSError *)error { ... //5. 根据succeed的布尔值来调用相应的处理 if  { //请求成功的处理 [self requestDidSucceedWithRequest:request]; } else { //请求失败的处理 [self requestDidFailWithRequest:request error:requestError]; } //6. 回调完成的处理 dispatch_async(dispatch_get_main_queue(), ^{ //6.1 在字典里移除当前request [self removeRequestFromRecord:request]; //6.2 清除所有block [request clearCompletionBlock]; }); ...}

我们先来分别看一下请求成功的处理和失败的处理:

请求成功的处理:

//YTKNetworkAgent.m//请求成功:主要负责将结果写入缓存&回调成功的代理和block- requestDidSucceedWithRequest:(YTKBaseRequest *)request { @autoreleasepool { //写入缓存 [request requestCompletePreprocessor]; } dispatch_async(dispatch_get_main_queue(), ^{ //告诉Accessories请求就要停止了 [request toggleAccessoriesWillStopCallBack]; //在真正的回调之前做的处理,用户自定义 [request requestCompleteFilter]; //如果有代理,则调用成功的代理 if (request.delegate != nil) { [request.delegate requestFinished:request]; } //如果传入了成功回调的代码,则调用 if (request.successCompletionBlock) { request.successCompletionBlock; } //告诉Accessories请求已经结束了 [request toggleAccessoriesDidStopCallBack]; });}

我么可以看到,在请求成功以后,第一个做的是写入缓存,我们来看一下requestCompletePreprocessor方法的实现:

//YTKRequest.m- requestCompletePreprocessor { [super requestCompletePreprocessor]; //是否异步将responseData写入缓存(写入缓存的任务放在专门的队列进行) if (self.writeCacheAsynchronously) { dispatch_async(ytkrequest_cache_writing_queue(), ^{ //写入缓存文件 [self saveResponseDataToCacheFile:[super responseData]]; }); } else { //写入缓存文件 [self saveResponseDataToCacheFile:[super responseData]]; }}//写入缓存文件- saveResponseDataToCacheFile:data { if ([self cacheTimeInSeconds] > 0 && ![self isDataFromCache]) { if (data != nil) { @try { // 1. 保存request的responseData到cacheFilePath [data writeToFile:[self cacheFilePath] atomically:YES]; // 2. 保存request的metadata到cacheMetadataFilePath YTKCacheMetadata *metadata = [[YTKCacheMetadata alloc] init]; metadata.version = [self cacheVersion]; metadata.sensitiveDataString = ((NSObject *)[self cacheSensitiveData]).description; metadata.stringEncoding = [YTKNetworkUtils stringEncodingWithRequest:self]; metadata.creationDate = [NSDate date]; metadata.appVersionString = [YTKNetworkUtils appVersionString]; [NSKeyedArchiver archiveRootObject:metadata toFile:[self cacheMetadataFilePath]]; } @catch (NSException *exception) { YTKLog(@"Save cache failed, reason = %@", exception.reason); } } }}

首先看一下写入缓存操作的执行条件:当cacheTimeInSeconds方法返回大于0并且isDataFromCache为NO的时候会进行写入缓存。

cacheTimeInSeconds方法返回的是缓存保存的时间,它最初定义在YTKBaseRquest里面,默认返回是-1:

//YTKBaseRequest.m- (NSInteger)cacheTimeInSeconds { return -1;}

所以说YTKNetwork默认是不进行缓存的,如果用户需要做缓存,则需要在自定义的request类里面返回一个大于0的整数,这个整数的单位是秒。

isDataFromCache属性在上面讲解发送请求部分里的查询缓存的步骤里有介绍。在这里再强调一下:isDataFromCache的默认值是NO。在请求发起之前,-查询缓存的时候:

  • 如果发现缓存不可用,则立即发送请求,这个时候,isDataFromCache的值不做更改,仍然是NO。
  • 如果发现缓存可用(在不忽略缓存的情况下),就要将isDataFromCache属性设置为YES,说明将不需要发送请求,直接在里获取数据了。

即是说,如果发送了请求,则isDataFromCache一定是NO的,那么在上面这个判断里面,(!isDataFromCache)就一定为YES了。

因此,如果用户设置了缓存保存的时间,在请求返回成功后,就会写入缓存。

我们接着往下看,对于缓存,YTKNetwork保存的是两种缓存:第一种是纯粹的NSData类型的实例。第二种是描述当前NSData实例的元数据YTKCacheMetadata的实例,从它的属性来看,分为这几种:

  1. 缓存的版本,默认返回为0,用户可以自定义。
  2. 敏感数据,类型为id,默认返回nil,用户可以自定义。
  3. NSString的编码格式,在YTKNetworkPrivate内的YTKNetworkUtils实现。
  4. 元数据的创建时间。
  5. app的版本号,在YTKNetworkPrivate内的YTKNetworkUtils实现。

在将元数据的实例的这些属性都被赋值以后,将元数据实例序列化写入磁盘中。保存的路径通过cacheMetadataFilePath方法获取。

现在知道了YTKRequest的缓存内容,我们来看一下这两种缓存的位置:

//YTKRequest.m//纯NSData数据缓存的文件名- (NSString *)cacheFileName { NSString *requestUrl = [self requestUrl]; NSString *baseUrl = [YTKNetworkConfig sharedConfig].baseUrl; id argument = [self cacheFileNameFilterForRequestArgument:[self requestArgument]]; NSString *requestInfo = [NSString stringWithFormat:@"Method:%ld Host:%@ Url:%@ Argument:%@", [self requestMethod], baseUrl, requestUrl, argument]; NSString *cacheFileName = [YTKNetworkUtils md5StringFromString:requestInfo]; return cacheFileName;}//纯NSData数据的缓存位置- (NSString *)cacheFilePath { NSString *cacheFileName = [self cacheFileName]; NSString *path = [self cacheBasePath]; path = [path stringByAppendingPathComponent:cacheFileName]; return path;}//元数据的缓存位置- (NSString *)cacheMetadataFilePath { NSString *cacheMetadataFileName = [NSString stringWithFormat:@"%@.metadata", [self cacheFileName]]; NSString *path = [self cacheBasePath]; path = [path stringByAppendingPathComponent:cacheMetadataFileName]; return path;}//创建用户保存所有YTKNetwork缓存的文件夹- (NSString *)cacheBasePath { //获取全路径 NSString *pathOfLibrary = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) objectAtIndex:0]; NSString *path = [pathOfLibrary stringByAppendingPathComponent:@"LazyRequestCache"]; // YTKCacheDirPathFilterProtocol定义了用户可以自定义存储位置的代理方法 NSArray<id<YTKCacheDirPathFilterProtocol>> *filters = [[YTKNetworkConfig sharedConfig] cacheDirPathFilters]; if (filters.count > 0) { for (id<YTKCacheDirPathFilterProtocol> f in filters) { path = [f filterCacheDirPath:path withRequest:self]; } } //创建文件夹 [self createDirectoryIfNeeded:path]; return path;}

可以看出,纯NSData数据缓存的文件名包含了请求方法(GET,POST..),baseURL,requestURL,请求参数拼接的字符串再进行md5加密而成。

而元数据的的文件名则在纯NSData数据缓存的文件名后面加上了.metadata后缀。

为了更形象地看到这两种缓存,我将缓存的保存时间设置为200秒之后再请求一次,然后打开文件夹找到了它们:

图片 4缓存和缓存元数据的文件

而且我们也确认了保存所有YTKNetwork缓存的文件夹的名字为LazyRequestCache。

OK,现在我们知道了在请求成功回调后的缓存写入,接下来看一下是如何回调的:

//YTKNetworkAgent.m- handleRequestResult:(NSURLSessionTask *)task responseObject:responseObject error:(NSError *)error { ... YTKRequest *strongSelf = self; //6. 执行回调 //6.1 请求完成的代理 [strongSelf.delegate requestFinished:strongSelf]; //6.2 请求成功的block if (strongSelf.successCompletionBlock) { strongSelf.successCompletionBlock(strongSelf); } //7. 把成功和失败的block都设置为nil,避免循环引用 [strongSelf clearCompletionBlock];}

我们可以看到,代理的回调是先于block的回调的。而且在block回调结束以后,会立即调用clearCompletionBlock方法将block清空。该方法的实现是在YTKBaseRequest里:

//YTKBaseRequest.m- clearCompletionBlock { // 清空请求结束的block,避免循环引用 self.successCompletionBlock = nil; self.failureCompletionBlock = nil;}

现在我们知道了请求成功的处理,那么再来看一下请求失败时的处理:

//YTKNetworkAgent.m//请求失败- requestDidFailWithRequest:(YTKBaseRequest *)request error:(NSError *)error { request.error = error; YTKLog(@"Request %@ failed, status code = %ld, error = %@", NSStringFromClass([request class]), request.responseStatusCode, error.localizedDescription); // 储存未完成的下载数据 NSData *incompleteDownloadData = error.userInfo[NSURLSessionDownloadTaskResumeData]; if (incompleteDownloadData) { [incompleteDownloadData writeToURL:[self incompleteDownloadTempPathForDownloadPath:request.resumableDownloadPath] atomically:YES]; } // Load response from file and clean up if download task failed. //如果下载任务失败,则取出对应的响应文件并清空 if ([request.responseObject isKindOfClass:[NSURL class]]) { NSURL *url = request.responseObject; //isFileURL:是否是文件,如果是,则可以再isFileURL获取;&&后面是再次确认是否存在改url对应的文件 if (url.isFileURL && [[NSFileManager defaultManager] fileExistsAtPath:url.path]) { //将url的data和string赋给request request.responseData = [NSData dataWithContentsOfURL:url]; request.responseString = [[NSString alloc] initWithData:request.responseData encoding:[YTKNetworkUtils stringEncodingWithRequest:request]]; [[NSFileManager defaultManager] removeItemAtURL:url error:nil]; } //清空request request.responseObject = nil; } @autoreleasepool { //请求失败的预处理,YTK没有定义,需要用户定义 [request requestFailedPreprocessor]; } dispatch_async(dispatch_get_main_queue(), ^{ //告诉Accessories请求就要停止了 [request toggleAccessoriesWillStopCallBack]; //在真正的回调之前做的处理 [request requestFailedFilter]; //如果有代理,就调用代理 if (request.delegate != nil) { [request.delegate requestFailed:request]; } //如果传入了失败回调的block代码,就调用block if (request.failureCompletionBlock) { request.failureCompletionBlock; } //告诉Accessories请求已经停止了 [request toggleAccessoriesDidStopCallBack]; });}

在这个方法里,首先判断了当前任务是否为下载任务,如果是,则储存当前已经下载好的data到resumableDownloadPath里面。而如果下载任务失败,则将其对应的在本地保存的路径上的文件清空。

到这里,我已经把单个请求从配置,发送,响应,回调的步骤都讲解完了。为了帮助大家理解整个过程,这里提供了整个的流程图:

图片 5YTKNetwork流程图

我们说YTKNetworkAgent是请求的发送者,既然有发送,也就会有取消等操作,这就不得不提它的另外两个接口:

//YTKNetworkAgent.h/// 取消某个request- cancelRequest:(YTKBaseRequest *)request;/// 取消所有添加的request- cancelAllRequests;

首先我们看下取消某个request这个方法的实现:

//YTKNetworkAgent.m/// 取消某个request- cancelRequest:(YTKBaseRequest *)request { NSParameterAssert(request != nil); //获取request的task,并取消 [request.requestTask cancel]; //从字典里移除当前request [self removeRequestFromRecord:request]; //清理所有block [request clearCompletionBlock];}//从字典里移除某request- removeRequestFromRecord:(YTKBaseRequest *)request { //加锁 Lock(); [_requestsRecord removeObjectForKey:@(request.requestTask.taskIdentifier)]; YTKLog(@"Request queue size = %zd", [_requestsRecord count]); Unlock();}

取消所有在字典里添加的request:

//YTKNetworkAgent.m- cancelAllRequests { Lock(); NSArray *allKeys = [_requestsRecord allKeys]; Unlock(); if (allKeys && allKeys.count > 0) { NSArray *copiedKeys = [allKeys copy]; for (NSNumber *key in copiedKeys) { Lock(); YTKBaseRequest *request = _requestsRecord[key]; Unlock(); //stop每个请求 [request stop]; } }}

这个stop方法是在YTKBaseRequest里面定义的:

//YTKBaseRequest.m- stop { //告诉Accessories将要回调了 [self toggleAccessoriesWillStopCallBack]; //清空代理 self.delegate = nil; //调用agent的取消某个request的方法 [[YTKNetworkAgent sharedAgent] cancelRequest:self]; //告诉Accessories回调完成了 [self toggleAccessoriesDidStopCallBack];}

OK,看到这里,相信你对YTKNetwork单个请求的流程有了比较好的了解了,下面我们来看一下YTKNetwork的高级功能:批量请求和链式请求。

YTKNetwork支持的批量请求有两种:

  1. 批量请求:多个请求几乎同时发起。
  2. 链式请求:当前个请求结束后才能发起下一个请求。

其实无论是批量请求,还是链式请求,我们都可以想到很可能是用一个数组将这些请求管理了起来。那么具体是如何实现的呢?

我们首先来看一下YTKNetwork是如何实现批量请求的。

  • 读取。初始化购物车的ViewModel时订阅全局ShoppingManager单例类change属性值RACObserve([ShoppingManager manager],change)信号,获取[ShoppingManager manager]goodsDic数据更新UI

  • 保存。单例类的意义在于实时读写数据。以商品模型id为键、商品模型model为值存入单例的[ShoppingManager manager].goodsDic属性中。

  • 添加。当用户在GoodManagerView视图上点击增加或减少商品数量的时候,重写[ShoppingManager manager].goodsDic

  • 更新。首先array数组保存[ShoppingManager manager].goodsDic字典的所有值allValue,一个值代表一个商品模型model,字典所有模型转移到数组之后清空goodsDic;遍历商品模型数组array的每一个商品模型model,通过model.isSelected判断商品是否被选中;未选中状态则继续以商品模型id为键、模型model为值存入[ShoppingManager currentUser].goodsDic字典中;处于选中状态则首先获取被选中商品模型model的单个商品数量,更新用户管理单例类的[UserManager currentUser].bageValue商品总数属性;最后,在商品模型数组array的每一个商品模型model都遍历一遍之后,需要将[ShoppingManager manger][UserManager currentUser]这两个单例类模型对象重新保存到本地沙盒,保证退出应用,购物车数据依然存在。

3.31批量请求

YTKNetwork 使用YTKBatchRequest类来发送无序的批量请求,它需要用一个含有YTKRequest子类的数组来初始化,并将这个数组保存起来赋给它的_requestArray实例变量:

//YTKBatchRequest.m- (instancetype)initWithRequestArray:(NSArray<YTKRequest *> *)requestArray { self = [super init]; if  { //保存为属性 _requestArray = [requestArray copy]; //批量请求完成的数量初始化为0 _finishedCount = 0; //类型检查,所有元素都必须为YTKRequest或的它的子类,否则强制初始化失败 for (YTKRequest * req in _requestArray) { if (![req isKindOfClass:[YTKRequest class]]) { YTKLog(@"Error, request item must be YTKRequest instance."); return nil; } } } return self;}

初始化以后,我们就可以调用start方法来发起当前YTKBatchRequest实例所管理的所有请求了:

//YTKBatchRequest.m//batch请求开始- startWithCompletionBlockWithSuccess:(YTKBatchRequest *batchRequest))success failure:(YTKBatchRequest *batchRequest))failure { [self setCompletionBlockWithSuccess:success failure:failure]; [self start];}//设置成功和失败的block- setCompletionBlockWithSuccess:(YTKBatchRequest *batchRequest))success failure:(YTKBatchRequest *batchRequest))failure { self.successCompletionBlock = success; self.failureCompletionBlock = failure;}- start { //如果batch里第一个请求已经成功结束,则不能再start if (_finishedCount > 0) { YTKLog(@"Error! Batch request has already started."); return; } //最开始设定失败的request为nil _failedRequest = nil; //使用YTKBatchRequestAgent来管理当前的批量请求 [[YTKBatchRequestAgent sharedAgent] addBatchRequest:self]; [self toggleAccessoriesWillStartCallBack]; //遍历所有request,并开始请求 for (YTKRequest * req in _requestArray) { req.delegate = self; [req clearCompletionBlock]; [req start]; }}

在这里,我们可以看出:1.在至少完成了其中一个请求以后,调用当前YTKBatchRequest实例的start方法会立即返回,否则可以无限制start。2.YTKBatchRequest的实例是需要在发起请求之前,要被添加在YTKBatchRequestAgent里的数组里:

//YTKBatchRequestAgent.m- addBatchRequest:(YTKBatchRequest *)request { @synchronized { [_requestArray addObject:request]; }}

3.因为是批量发送请求,所以在这里是遍历YTKBatchRequest实例的_requestArray并逐一发送请求。因为已经封装好了单个的请求,所以在这里直接start就好了。

发起请求以后,在每个请求回调的代理方法里,来判断这次批量请求是否成功。

YTKRequest子类成功的回调:

//YTKBatchRequest.m#pragma mark - Network Request Delegate- requestFinished:(YTKRequest *)request { //某个request成功后,首先让_finishedCount + 1 _finishedCount++; //如果_finishedCount等于_requestArray的个数,则判定当前batch请求成功 if (_finishedCount == _requestArray.count) { //调用即将结束的代理 [self toggleAccessoriesWillStopCallBack]; //调用请求成功的代理 if ([_delegate respondsToSelector:@selector(batchRequestFinished:)]) { [_delegate batchRequestFinished:self]; } //调用批量请求成功的block if (_successCompletionBlock) { _successCompletionBlock; } //清空成功和失败的block [self clearCompletionBlock]; //调用请求结束的代理 [self toggleAccessoriesDidStopCallBack]; //从YTKBatchRequestAgent里移除当前的batch [[YTKBatchRequestAgent sharedAgent] removeBatchRequest:self]; }}

我们可以看到,在某个请求的回调成功以后,会让成功计数+1。在+1以后,如果成功计数和当前批量请求数组里元素的个数相等,则判定当前批量请求成功,并进行当前批量请求的成功回调。

接下来我们看一下某个请求失败的处理:

YTKReques子类失败的回调:

//YTKBatchRequest.m- requestFailed:(YTKRequest *)request { _failedRequest = request; //调用即将结束的代理 [self toggleAccessoriesWillStopCallBack]; //停止batch里所有的请求 for (YTKRequest *req in _requestArray) { [req stop]; } //调用请求失败的代理 if ([_delegate respondsToSelector:@selector(batchRequestFailed:)]) { [_delegate batchRequestFailed:self]; } //调用请求失败的block if (_failureCompletionBlock) { _failureCompletionBlock; } //清空成功和失败的block [self clearCompletionBlock]; //调用请求结束的代理 [self toggleAccessoriesDidStopCallBack]; //从YTKBatchRequestAgent里移除当前的batch [[YTKBatchRequestAgent sharedAgent] removeBatchRequest:self];}

在这里不难看出,当前批量请求里面只要有一个request失败了,则判定当前批量请求失败。而当前批量请求失败的回调会传入这个失败的request的实例。而且这个失败的request会先被赋给_failedRequest这个实例变量里。

总的来说,YTKBatchRequest类用一个数组来保存当前批量请求所要处理的所有request实例。而且用一个成功计数来判定当前批量请求整体是否成功。而当前批量请求的失败则是由这些request实例里面第一个失败的实例导致的:只要有一个request回调失败了,则立即停止其他的所有请求并调用当前批量请求的失败回调。

现在讲完了批量请求的处理,我们接下来看一下链式请求的处理。

3.32链式请求

和批量请求类似,处理链式请求的类是YTKChainRequest,并且用YTKChainRequestAgent单例来管理YTKChainRequest的实例。

但是和批量请求不同的是,YTKChainRequest实例的初始化是不需要传入一个含有request的数组的:

//YTKChainRequest.m- (instancetype)init { self = [super init]; if  { //下一个请求的index _nextRequestIndex = 0; //保存链式请求的数组 _requestArray = [NSMutableArray array]; //保存回调的数组 _requestCallbackArray = [NSMutableArray array]; //空回调,用来填充用户没有定义的回调block _emptyCallback = ^(YTKChainRequest *chainRequest, YTKBaseRequest *baseRequest) { // do nothing }; } return self;}

但是它提供了添加和删除request的接口:

//YTKChainRequest.m//在当前chain添加request和callback- addRequest:(YTKBaseRequest *)request callback:(YTKChainCallback)callback { //保存当前请求 [_requestArray addObject:request]; if (callback != nil) { [_requestCallbackArray addObject:callback]; } else { //之所以特意弄一个空的callback,是为了避免在用户没有给当前request的callback传值的情况下,造成request数组和callback数组的不对称 [_requestCallbackArray addObject:_emptyCallback]; }}

注意,在给YTKChainRequest实例添加request实例的同时,还可以传入回调的block。当然也可以不传,但是为了保持request数组和callback数组的对称性(因为回调的时候是需要根据request数组里的index来获取callback数组里对应的callback的),YTKNetwork给我们提供了一个空的block。

我们接着看一下链式请求的发起:

//YTKChainRequest.m- start { //如果第1个请求已经结束,就不再重复start了 if (_nextRequestIndex > 0) { YTKLog(@"Error! Chain request has already started."); return; } //如果请求队列数组里面还有request,则取出并start if ([_requestArray count] > 0) { [self toggleAccessoriesWillStartCallBack]; //取出当前request并start [self startNextRequest]; //在当前的_requestArray添加当前的chain(YTKChainRequestAgent允许有多个chain) [[YTKChainRequestAgent sharedAgent] addChainRequest:self]; } else { YTKLog(@"Error! Chain request array is empty."); }}

我们可以看到,YTKChainRequest用_nextRequestIndex来保存下一个请求的index,它的默认值是0。而它的值的累加是在当前请求结束后,发起下面的请求之前进行的。所以说,如果已经完成了请求队列里的第一个请求,就无法在启动当前的请求队列了,会立即返回。

这里startNextRequest方法比较重要:在判断请求队列数组里面还有request的话,就会调用这个方法:

//YTKChainRequest.m- startNextRequest { if (_nextRequestIndex < [_requestArray count]) { YTKBaseRequest *request = _requestArray[_nextRequestIndex]; _nextRequestIndex++; request.delegate = self; [request clearCompletionBlock]; [request start]; return YES; } else { return NO; }}

这个方法有两个作用:

  1. 第一个作用是判断是否能进行下一个request(如果index 大于或等于 request数组的count的话就不能在request数组里取出request,因为会造成数组越界)
  2. 第二个作用是如果可以进行下一个request,则发起该request。并将_nextRequestIndex+1。

所以和批量请求不同的是,链式请求的请求队列是可以变动的,用户可以无限制地添加请求。只要请求队列里面有请求存在,则YTKChainRequest就会继续发送它们。

现在我们知道了YTKChainRequest的发送,接下来看一下回调部分:

和YTKBatchRequest相同的是,YTKChainRequest也实现了YTKRequest的代理:

//某个request请求成功的代理的实现//YTKChainRequest.m- requestFinished:(YTKBaseRequest *)request { //1. 取出当前的request和callback,进行回调 NSUInteger currentRequestIndex = _nextRequestIndex - 1; YTKChainCallback callback = _requestCallbackArray[currentRequestIndex]; callback(self, request);//注意:这个回调只是当前request的回调,而不是当前chain全部完成的回调。当前chain的回调在下面 //2. 如果不能再继续请求了,说明当前成功的request已经是chain里最后一个request,也就是说当前chain里所有的回调都成功了,即这个chain请求成功了。 if (![self startNextRequest]) { [self toggleAccessoriesWillStopCallBack]; if ([_delegate respondsToSelector:@selector(chainRequestFinished:)]) { [_delegate chainRequestFinished:self]; [[YTKChainRequestAgent sharedAgent] removeChainRequest:self]; } [self toggleAccessoriesDidStopCallBack]; }}

我们可以看到,在某个request回调成功以后,会根据当前请求的index(_nextRequestIndex-1)来获取其对应的block并调用。接着,再调用startNextRequest方法来判断当前的YTKChainRequest的请求队列里面是否还有其他的请求了:

  • 如果没有了,则调用当前YTKChainRequest的最终成功的回调。
  • 如果还有,则发起接下来的request。

接下来我们再看一下某个request失败的代理的实现:

//YTKChainRequest.m//某个reqeust请求失败的代理- requestFailed:(YTKBaseRequest *)request { //如果当前 chain里的某个request失败了,则判定当前chain失败。调用当前chain失败的回调 [self toggleAccessoriesWillStopCallBack]; if ([_delegate respondsToSelector:@selector(chainRequestFailed:failedBaseRequest:)]) { [_delegate chainRequestFailed:self failedBaseRequest:request]; [[YTKChainRequestAgent sharedAgent] removeChainRequest:self]; } [self toggleAccessoriesDidStopCallBack];}

如果当前的request请求失败了,则判定当前链式请求是失败的,则立即调用当前链式请求的失败回调。

现在我们知道了链式请求的请求和回调,再来看一下链式请求的终止:

//YTKChainRequest.m//终止当前的chain- stop { //首先调用即将停止的callback [self toggleAccessoriesWillStopCallBack]; //然后stop当前的请求,再清空chain里所有的请求和回掉block [self clearRequest]; //在YTKChainRequestAgent里移除当前的chain [[YTKChainRequestAgent sharedAgent] removeChainRequest:self]; //最后调用已经结束的callback [self toggleAccessoriesDidStopCallBack];}

这个stop方法是可以在外部调用的,所以用户可以随时终止当前链式请求的进行。它首先调用clearReuqest方法,将当前request停止,再将请求队列数组和callback数组清空。

//YTKChainRequest.m- clearRequest { //获取当前请求的index NSUInteger currentRequestIndex = _nextRequestIndex - 1; if (currentRequestIndex < [_requestArray count]) { YTKBaseRequest *request = _requestArray[currentRequestIndex]; [request stop]; } [_requestArray removeAllObjects]; [_requestCallbackArray removeAllObjects];}

然后在YTKChainRequestAgent单例里面,将自己移除掉。

不知不觉写了好多,请原谅我一如既往啰嗦的风格~

阅读这个框架的源码我的收获是:加深了对命令模式,对Block的理解,知道了一个网络请求都需要什么元素组成,知道了网络缓存该怎么设计,也知道了链式请求怎么设计等等。

我还记得当初听说YTKNetwork能发起链式请求的时候觉得毫无思路的感觉,不过现在应该没什么问题了。

所以说多阅读源码对技术水平的提升是很有帮助的,除了能增多对本语言API的了解,其实更有意义的是它能让你接触到一些新的设计和解决问题的办法,这些都是脱离某个语言本身的东西,也是作为一名程序员所必不可少的东西。

希望这篇文章能对读者们有所帮助~

本文已经同步到我的个人博客:YTKNetwork源码解析

欢迎来参观 ^^

本文已在版权印备案,如需转载请访问版权印。48422928

获取授权

注意注意!!!

笔者在近期开通了个人公众号,主要分享编程,读书笔记,思考类的文章。

  • 编程类文章:包括笔者以前发布的精选技术文章,以及后续发布的技术文章,并且逐渐脱离 iOS 的内容,将侧重点会转移到提高编程能力的方向上。
  • 读书笔记类文章:分享编程类思考类心理类职场类书籍的读书笔记。
  • 思考类文章:分享笔者平时在技术上生活上的思考。

因为公众号每天发布的消息数有限制,所以到目前为止还没有将所有过去的精选文章都发布在公众号上,后续会逐步发布的。

而且因为各大博客平台的各种限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~

扫下方的公众号二维码并点击关注,期待与您的共同成长~

图片 6公众号:程序员维他命

返回顶部