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文件里面是所有发送一次请求所需要的数据,例如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。
逻辑并不复杂,需要注意的是,如何避免数据竞争,如何尽可能的不消耗主线程资源。
二、针对百分百送达服务器的请求
根据业务来说,这类请求应用的地方很多。类似于我们发微信发消息时,消息数据一旦从数据框中发出,从用户的角度感知这条消息是一定会到达对方的;在小说阅读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,那么如何避免数据竞争?在代码里面都有解答,以上。
iOS网络模块优化(失败重发、缓存请求有网发送)的更多相关文章
- iOS网络加载图片缓存策略之ASIDownloadCache缓存优化
iOS网络加载图片缓存策略之ASIDownloadCache缓存优化 在我们实际工程中,很多情况需要从网络上加载图片,然后将图片在imageview中显示出来,但每次都要从网络上请求,会严重影响用 ...
- 美图App的移动端DNS优化实践:HTTPS请求耗时减小近半
本文引用了颜向群发表于高可用架构公众号上的文章<聊聊HTTPS环境DNS优化:美图App请求耗时节约近半案例>的部分内容,感谢原作者. 1.引言 移动互联网时代,APP 厂商之间的竞争非常 ...
- 【腾讯Bugly干货分享】微信读书iOS性能优化
本文来自于腾讯bugly开发者社区,非经作者同意,请勿转载,原文地址:http://dev.qq.com/topic/578c93ca9644bd524bfcabe8 “8小时内拼工作,8小时外拼成长 ...
- iOS性能优化:Instruments使用实战
iOS性能优化:Instruments使用实战 最近采用Instruments 来分析整个应用程序的性能.发现很多有意思的点,以及性能优化和一些分析性能消耗的技巧,小结如下. Instrument ...
- iOS开发优化的25个方案
写在前面 本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员.这是他的个人网站:http://www.marcelofabri.com/ ...
- iOS tableview 优化总结
根据网络上的优化方法进行了总括.并未仔细进行语言组织.正在这些优化方法进行学习,见另一篇文章 提高app流畅度 1.cell子控件创建写在 initWithStyle:reuseIdentifier ...
- IOS 性能优化的建议和技巧
IOS 性能优化的建议和技巧 本文来自iOS Tutorial Team 的 Marcelo Fabri,他是Movile的一名 iOS 程序员.这是他的个人网站:http://www.marcelo ...
- MySQL优化-一 、缓存优化
body { font-family: Helvetica, arial, sans-serif; font-size: 14px; line-height: 1.6; padding-top: 10 ...
- iOS开发——优化篇—— 25个性能优化/内存优化常用方法
1. 用ARC管理内存 ARC(Automatic ReferenceCounting, 自动引用计数)和iOS5一起发布,它避免了最常见的也就是经常是由于我们忘记释放内存所造成的内存泄露.它自动为你 ...
随机推荐
- ip地址与整数相互转换
一.将ip地址转成long数值 将IP地址转化成整数的方法如下: 1.通过String的split方法按.分隔得到4个长度的数组 2.通过左移位操作(<<)给每一段的数字加权,第一段的权为 ...
- 输入一个A和B,,A<=B,A>=1,B<=pow(10,18)计算F=B!/A!结果的最后一位
*************************************************************************代理运行函数,判断结果,进行输出*********** ...
- Python之三目运算符
Python语言不像Java.JavaScript等这些语言有类似: 判段的条件?条件为真时的结果:条件为假时的结果 这样的三目运算,但是Python也有自己的三目运算符: 条件为真时的结果 if 判 ...
- 算法帖——用舞蹈链算法(Dancing Links)求解俄罗斯方块覆盖问题
问题的提出:如下图,用13块俄罗斯方块覆盖8*8的正方形.如何用计算机求解? 解决这类问题的方法不一而足,然而核心思想都是穷举法,不同的方法仅仅是对穷举法进行了优化 用13块不同形状的俄罗斯方块(每个 ...
- Oracle存储过程和自定义函数
新博客文章链接,欢迎大家评论探讨 概述 存储过程和存储函数是指存储在数据库中供所有用户程序调用的子程序叫存储过程.存储函数. 异同点: 存储过程和存储函数的相同点:完成特定功能的程序. 存储过程和存储 ...
- Error parsing column 8 (IsRecommended=0 - SByte) Dapper查询mysql数据库可空的tinyint(1)一个错误
出错条件: 1.实体属性为bool?类型 2.对应字段为可空的tinyint(1)类型 3.该字段查询结果内即含有null,又含有正常值 google答案,两种建议: 1.修改sql语句,直接cast ...
- ChatterBot之使用mongodb 03
上一篇我们已经搭建好了mongodb环境,本篇为简单示例. 废话不多说先上代码然后开始讲解; !!!别忘了打开你的mongdb服务!!!,如果没有mongodb请看上篇如何安装mongodb; # - ...
- JavaScript OOP(三):prototype原型对象(即构造函数的prototype属性)
通过构造函数生成的实例化对象,无法共享属性或方法(即每个实例化对象上都有构造函数中的属性和方法):造成了一定的资源浪费 function Obj(name,age){ this.name=name; ...
- C#在自定义事件里传递自定义数据,使用EventArgs的姿势
EventArgs是包含事件数据的类的基类,用于传递事件的细节.今天分享的是使用泛型来约束EventArgs,在事件里传递自定义数据的例子. 正题 由于这个关注点很小,直接上代码了. 定义泛型类TEv ...
- DIN(Deep Interest Network of CTR) [Paper笔记]
背景 经典MLP不能充分利用结构化数据,本文提出的DIN可以(1)使用兴趣分布代表用户多样化的兴趣(不同用户对不同商品有兴趣)(2)与attention机制一样,根据ad局部激活用户兴趣相关的兴趣(用 ...