zoukankan      html  css  js  c++  java
  • iOS网络模块优化(失败重发、缓存请求有网发送)

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

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

      优化的理论之前,先建立代码样例,假设我们有这样两个类:  

    @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

        

      一、代码结构分析

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

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

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

        ZYRequest文件里面是所有发送一次请求所需要的数据,例如urlparamsmethod ype等。

        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。

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

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

        根据业务来说,这类请求应用的地方很多。类似于我们发微信发消息时,消息数据一旦从数据框中发出,从用户的角度感知这条消息是一定会到达对方的;在小说阅读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,再下载,相关数据就无法恢复了。

      三、失败重发

        第二类请求的可靠性为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体验产生什么负面影响。

      四、设计思路

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

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

        1、为什么需要设计框架?所有请求直接利用AFN并发发送不行么?(不行,因为网络带宽是有限的,这样做会导致数据返回整体慢上很多,而且,一个网络请求的超时时间是一定的,一次性并发很可能造成本来可以发送成功的请求超时)

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

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

  • 相关阅读:
    k近邻 KNN
    聚类之k-means
    支持向量机SVM、优化问题、核函数
    [THUSC 2016] 补退选 (Trie树)
    [CQOI2016] 手机号码 (数位dp)
    [CQOI2012] 交换棋子 (费用流)
    [SCOI2016] 背单词 (Trie树)
    [JSOI2009] 球队收益 (费用流)
    [BZOJ1878][SDOI2009] HH的项链 (树状数组)
    [BZOJ2151]种树
  • 原文地址:https://www.cnblogs.com/ziyi--caolu/p/8176331.html
Copyright © 2011-2022 走看看