iOS网络模块优化(失败重发、缓存请求有网发送)

  iOS开发中,通常都是经过AFN搭建一个简易的网络模块来进行与服务器的通讯,这一模块要优化好没那么简单,须要花费不少时间与精力,仅仅根据这几年来的填坑经验,总结下这一块的须要注意的地方,也是给本身梳理下知识。html

  以前写的博客提到了DNS优化、请求数据大小的优化(http://www.cnblogs.com/ziyi--caolu/p/8058577.html)。这里主要想理一理合理的并发数以及网络请求可靠性的保障。git

  优化的理论以前,先创建代码样例,假设咱们有这样两个类:  github

@interface ZYRequest : NSObject<NSCopying>

@end

@interface ZYRequestManager : NSObject

@end

   ZYRequest类用来处理公共的逻辑,Manager负责管理Request。在iOS开发中,不少时候会遇到多个Request集中发送的状况,好比说第一次进入App首页,须要请求骨架文件、首页Banner图片、展现Cell数据等等,若是这时候并发数太少,那些须要优先展现的数据请求可能会被次要的数据请求“阻塞”住。若是并发数太大,带宽有限的场景下,会增长请求的总体延迟。通常而言,在实际开发中,让请求的并发数限制在3~~5便可(也能够给每一个请求设计优先级,而后在调度队列里面让优先级高的请求先出队列)。面试

  请求的可靠性保障是个很容易被忽视的问题,见过的不少App的网络请求都是只进行一次请求,失败后直接给用户提示网络错误。比较好的作法,是将Request按业务分类:算法

    第一类,关键核心业务,指望在任何条件下能百分百送达服务器。数据库

    第二类,重要的内容请求、数据展现,须要较高的成功率。设计模式

    第三类,通常性内容请求,对成功率无要求。缓存

  理论上来讲,须要咱们应该尽可能让每一个请求的成功率都达到最高,可是客户端流量、带宽、电量、服务器压力等都是有限的资源,因此只能采起将关键性请求作高强度的可靠性保证。服务器

    

    代码Github地址:https://github.com/wzpziyi1/iOSNetwork微信

    

  1、代码结构分析

    其中Storage文件夹里面,主要是处理将NSData数据缓存到沙盒的,实际我将它们的调用封装在ZYRequestCache文件里面,关于数据存储到沙盒、数据库,从沙盒取出数据、从数据库取出数据,删除、查询等等全部操做,都是封装在ZYRequestCache里面,直接调用它的接口便可。

    数据库采用的是realm数据库,而且实现了在子线程进行数据的存取,不占用主线程的资源,以避免形成卡顿。因为是第一次使用realm,踩了不少坑。全部的关于数据库的操做,都封装在ZYRequestRealm文件里面,里面也有许多操做realm时踩过的坑的提示,最须要注意的一点是,在realm数据库的使用中,对同一份数据的读、写、查询后使用,都必须是在同一线程,在编码时因为将除查询操做外的其余数据库操做放在子线程中,形成了各类线程错误的崩溃。

    YQDHttpClinetCore文件是基于AFN的封装,在里面设置了超时时间为5s,主要是由于我设置的重发请求次数是3次,那么真正交互的超时时间会是15s,若是有须要能够自行进行调整。

    ZYRequest文件里面是全部发送一次请求所须要的数据,例如url\params\method\type等。

    ZYRequestManager文件里面是进行request调度的主题逻辑,也没有进行复杂的算法,不按照优先级别,只是一个先入先出队列来进行调用的。里面有两个dispatch_queue:

//这个串行队列用来控制任务有序的执行
@property (nonatomic, strong) dispatch_queue_t taskQueue;

//添加、删除队列,维护添加与删除request在同一个线程
@property (nonatomic, strong) dispatch_queue_t addDelQueue;

    taskQueue主要是用来处理调度队列的,也就是requestQueue,让它在子线程进行循环查询、处理request,而后再并发进行网络请求,这样能够防止请求不少的状况下,卡住主线程。

    addDelQeueu主要是用来处理requestQueue里面的requset增长与删除的。在添加和删除的时候,采用的方案都是串行+同步,主要是避免数据竞争。(由于在AFN发送request要删除requestQueue里面的request的时候,是并发状态)

    在处理最大并发数的时候,我使用的是dispatch_semaphore_t(信号量),设置最大并发数是4。

    逻辑并不复杂,须要注意的是,如何避免数据竞争,如何尽量的不消耗主线程资源。 

 

  2、针对百分百送达服务器的请求  

    根据业务来讲,这类请求应用的地方不少。相似于咱们发微信发消息时,消息数据一旦从数据框中发出,从用户的角度感知这条消息是必定会到达对方的;在小说阅读App的书架收藏功能,理论上来讲户收藏一本书时,在用户感知角度,这本书必定会被收藏进入书架的等业务。若是网络环境差,网络模块会在后台悄悄重试,一段时间仍然没法成功的话,就直接通知用户发送失败了,可是即便失败,请求数据也会保存在本地,以便用户从新触发此条请求数据的发送。

    对于这类请求的处理,第一步并非直接发送,而是存入本地数据库中,一旦存入了数据库,即便是杀掉进程、断电、重启等极端操做,请求数据也依旧存在,咱们只须要在App重启或者进入该业务界面时,还原请求数据到内存中,从新进行发送便可。代码阐释:

    

#import <Foundation/Foundation.h>
#import "ZYRequestMacro.h"
#import <Realm/Realm.h>



typedef NS_ENUM(NSInteger, ZYRequestReliability){

    //若是没有发送成功,就放入调度队列再次发送
    ZYRequestReliabilityRetry,
    
    //必需要成功的请求,若是不成功就存入DB,而后在网络好的状况下继续发送,相似微信发消息
    //须要注意的是,这类请求不须要回调的
    //相似于发微信成功与否
    //就是一定成功的请求,只须要在有网的状态下,一定成功
    ZYRequestReliabilityStoreToDB,
    
    //普通请求,成不成功不影响业务,不须要从新发送
    //相似统计、后台拉取本地已有的配置之类的请求
    ZYRequestReliabilityNormal
};


@interface ZYRequest : RLMObject<NSCopying>

//存入数据库的惟一标示
@property (nonatomic, assign) int requestId;

/**请求参数对*/
@property (nonatomic, strong) NSDictionary *params;


/**
 请求的url
 */
@property (nonatomic, copy) NSString *urlStr;

/**
 请求重复策略,默认重发
 */
@property (nonatomic, assign) ZYRequestReliability reliability;

/**
 请求方法,默认get请求
 */
@property (nonatomic, assign) YQDRequestType method;


/**
 是否须要缓存响应的数据,若是cacheKey为nil,就不会缓存响应的数据
 */
@property (nonatomic, copy) NSString *cacheKey;

/**
 请求没发送成功,从新发送的次数
 */
@property (nonatomic, assign, readonly) int retryCount;


/**
 realm不支持NSDictionary,因此params直接转化为字符串存储
 只在请求须要存入数据库中,此参数才有相应的做用
 ZYRequestReliabilityStoreToDB这种类型下
 */
@property (nonatomic, copy, readonly) NSString *paramStr;


- (void)reduceRetryCount;
@end

     第一类请求就是ZYRequestReliabilityStoreToDB,requestId是它存入数据库的惟一标示,下面是请求的发送流程:

    

- (void)sendRequest:(ZYRequest *)request successBlock:(SuccessBlock)successBlock failureBlock:(FailedBlock)failedBlock
{
    //若是是ZYRequestReliabilityStoreToDB类型
    //第一时间先存储到数据库,而后再发送该请求,若是成功再从数据库中移除
    //不成功再出发某机制从数据库中取出从新发送
    if (request.reliability == ZYRequestReliabilityStoreToDB)
    {
        [[ZYRequestCache sharedInstance] saveRequestToRealm:request];
    }
    
    [self queueAddRequest:request successBlock:successBlock failureBlock:failedBlock];
    [self dealRequestQueue];
}


//在成功的时候移除realm数据库中的缓存
 if (request.reliability == ZYRequestReliabilityStoreToDB)
{
    [[ZYRequestCache sharedInstance] deleteRequestFromRealmWithRequestId:request.requestId];
}

//请求失败以后,根据约定的错误码判断是否须要再次请求
                //这里,-1001是AFN的超时error
 if (error.code == -1001 &&request.retryCount > 0)
{
    [request reduceRetryCount];
    [self queueAddRequest:request successBlock:successBlock failureBlock:failedBlock];
    [self dealRequestQueue];
}
else  //处理错误信息
{
    failedBlock(error);
}

     若是是ZYRequestReliabilityStoreToDB请求,第一步是存入数据库。

    第二步,将请求添加到调度队列里面,让调度队列调用AFN去处理该请求。在AFN的成功block里面,判断状态码,若是是真的成功状态,那么将数据库里面的请求移除掉,若是是失败状态,将从新请求的次数递减,再添加到调度队列末尾从新排队请求。

    我设计的是,最多重发三次请求。另外还有一个定时器,这个定时器会每隔60s,从数据库查询须要全部存储的请求,而后将它们尝试加入调度队列再次发送。这样的设计,即便App被kill,再次重启60s以后,也会把数据库中的请求拿出来进行发送。(只是一种思路,实际开发中会进入到具体业务才讲请求拿出来发送)

    经过上面的几个步骤,基本上能够极大的提升请求的可靠性,可是真的100%是没法实现的,若是用户卸载App,再下载,相关数据就没法恢复了。

 

  3、失败重发

    第二类请求的可靠性为ZYRequestReliabilityRetry,这类请求的例子能够是咱们App启动时用户看到的首页,首页的内容从服务器获取,若是第一次请求就失败体验较差,这种场景下咱们应该容许请求有机会多试几回,增长一个retryCount便可。

/**
 请求没发送成功,从新发送的次数
 */
@property (nonatomic, assign, readonly) int retryCount;

     在Manager里面,有一个调度队列:

@property (nonatomic, strong) NSMutableArray *requestQueue;

     每次将请求加入这个队列,而后在AFN发送完成回调以后,若是失败就进行重发,实际开发时,须要自行处理失败重发的错误码判断:

//请求失败以后,根据约定的错误码判断是否须要再次请求
//这里,-1001是AFN的超时error
if (error.code == -1001 &&request.retryCount > 0)
{
    [request reduceRetryCount];
    [self queueAddRequest:request successBlock:successBlock failureBlock:failedBlock];
    [self dealRequestQueue];
}

     在这里,是设置若是超时才进行重发请求,也能够将这个判断去掉,只要retryCount大于0即进行重发。通常开发的时候会和后台肯定一些错误码,根据错误码的类型判断是否须要重发会更合理些。

    通常3次的重试基本能够排除网络抖动的状况。三次失败以后便可认为请求失败,经过产品交互告知用户。

    第三类请求的重要性最低,好比进入Controller的UV采集打点、收集数据等。这类请求只须要作一次,即便失败也不会对App体验产生什么负面影响。

 

  4、设计思路

    这是某天在开发群里群友发出来的一道面试题,当一个复杂界面上的数据要根据n个请求返回的数据进行更新的时候,要求设计一个架构来发送这些请求。

    当时简单的和群友聊了聊,趁着最近有时间就本身撸了一套这样的机制,首先是疑问:

    一、为何须要设计框架?全部请求直接利用AFN并发发送不行么?(不行,由于网络带宽是有限的,这样作会致使数据返回总体慢上不少,并且,一个网络请求的超时时间是必定的,一次性并发极可能形成原本能够发送成功的请求超时)

    二、基于问题1,架构如何设计?(我的认为,面试官主要是想考察对平时写代码对于封装、设计模式、网络回调的理解,若是仅仅只是一个最大并发的限制,明显不会是理想答案,那么须要注意的点?除开最大并发,开发中网络错误时,都有错误码返回,对这一块应该作好处理。当请求失败时,需不须要进行重发?请求之间需不须要设置依赖?须要不要有优先级等等)

 

    这一次的设计,并无依赖、优先级等,只进行了重发、最大并发设计。思路是同样的,无非就是一个调度队列进行request的处理,这个队列的出队规则能够是按优先级高低来进行,固然得本身封装优先队列的算法。这里是最简单的先进先出队列,每次请求失败,要进行重发的话,就把请求丢到队列末尾。额,理论上来讲,请求无限多的状况下,调度队列会是个死循环,这样会形成主线程卡顿,因此把它放到子线程来处理。在并发发送请求之下,不作处理的话,会并发的删除调度队列里面的request,那么如何避免数据竞争?在代码里面都有解答,以上。

相关文章
相关标签/搜索