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

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

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

@interface ZYRequest : NSObject

@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

#import "ZYRequestMacro.h"

#import

typedef NS_ENUM(NSInteger, ZYRequestReliability){

//如果没有发送成功,就放入调度队列再次发送

ZYRequestReliabilityRetry,

//必须要成功的请求,如果不成功就存入DB,然后在网络好的情况下继续发送,类似微信发消息

//需要注意的是,这类请求不需要回调的

//类似于发微信成功与否

//就是必定成功的请求,只需要在有网的状态下,必定成功

ZYRequestReliabilityStoreToDB,

//普通请求,成不成功不影响业务,不需要重新发送

//类似统计、后台拉取本地已有的配置之类的请求

ZYRequestReliabilityNormal

};

@interface ZYRequest : RLMObject

//存入数据库的唯一标示

@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网络加载图片缓存策略之ASIDownloadCache缓存优化

iOS网络加载图片缓存策略之ASIDownloadCache缓存优化   在我们实际工程中,很多情况需要从网络上加载图片,然后将图片在imageview中显示出来,但每次都要从网络上请求,会严重影响用 ...

美图App的移动端DNS优化实践:HTTPS请求耗时减小近半

本文引用了颜向群发表于高可用架构公众号上的文章的部分内容,感谢原作者. 1.引言 移动互联网时代,APP 厂商之间的竞争非常 ...

【腾讯Bugly干货分享】微信读书iOS性能优化

无限小说网 内部服务器错误,iOS网络模块优化(失败重发、缓存请求有网发送)...相关推荐

  1. 控制台报400、500内部服务器错误是什么原因?怎么解决?

    控制台报400.500内部服务器错误是什么原因?怎么解决? 状态码为400时 显示Bad Request,此时你可以看一下你的控制台->网络部分 -> 数据返回是不是后端要求上传的数据格式 ...

  2. 内部服务器错误500是什么?该如何解决?

    500内部服务器错误是什么 5XX错误是由服务器返回的状态代码,当服务器遇到这使它不能满足来自客户端的请求的意外情况.500内部服务器错误包含很多种情况,不能确切知道到底是哪个异常导致的.于是很难解决 ...

  3. ios的vn服务器未响应,iOS应用程序导致内部服务器错误

    我有一个很奇怪的问题.我的应用程序正在导致我的服务器狂暴.iOS应用程序导致内部服务器错误 我的iOS应用程序使用TFHPPLE解析来自服务器上的PHP页面的数据.它从页面抓取一个名称,并检查该广告系 ...

  4. 官网显示500内部服务器有错误代码,【500错误】http 500 - 内部服务器错误(错误代码500)解决方法...

    在上网浏览网页的时候时不时的会遇到500错误,会提示内部服务器错误.你查找的资源存在问题,网页无法显示,一般来说这是网站的问题,对于浏览用户一般没办法解决,只有换个时间再试,但是对于站长来说就要着手解 ...

  5. 500服务器无法正常提供信息,http 500 内部服务器错误怎么办

    http 500 内部服务器错误怎么办?现在有很多站长在建站的时候会遇到网站无法正常显示网页的问题,这些原因有很多,可能是域名解析除了问题,也可能是服务器不稳定,或者是http 500 内部服务器错误 ...

  6. php 500 内部服务器错误,php 500 - 内部服务器错误的解决方法

    php 500 - 内部服务器错误的解决方法 发布时间:2020-11-04 09:55:31 来源:亿速云 阅读:71 作者:小新 小编给大家分享一下php 500 - 内部服务器错误的解决方法,相 ...

  7. php捕获500错误信息,php – 如何获取有关错误500的更多信息 – 内部服务器错误?...

    我在我的 PHP网站的许多部分使用$ajax请求一切正常,直到几天前我所有的$ajax请求开始给出错误500 – 内部服务器错误. 我可以在控制台中看到该错误,并且我还使用错误处理程序来获取有关错误的 ...

  8. “HTTP 500 - 内部服务器错误”解决办法

    IIS安装完成,一运行出现"HTTP 500 - 内部服务器错误",从一位大哥那里找到解决办法,只要三步就可以解决问题. 现在把它写在这里,用的时候看看 1. 运行:regsvr3 ...

  9. HttpWebResponse远程服务器返回错误: (500) 内部服务器错误。

    现象 我们编码实现请求一个页面时,请求的代码类似如下代码: HttpWebRequest req = (HttpWebRequest)WebRequest.Create(strUrl);req.Use ...

  10. 解决:此错误(HTTP 500 内部服务器错误)意味着您正在访问的网站出现了服务器问题,此问题阻止了该网页的显示...

    问题:此错误(HTTP 500 内部服务器错误)意味着您正在访问的网站出现了服务器问题,此问题阻止了该网页的显示 我的网站解决办法: 可能是因为IIS服务器没有开启父路径 解决:       在IIS ...

最新文章

  1. NET中验证控件表达式汇总
  2. python:文件操作
  3. Go 语言编程 — 程序运行环境
  4. sublime tex创建可服用的片段
  5. 做程序开发的你如果经常用Redis,这些问题肯定会遇到
  6. weex android 性能,跨越适配性能那道坎,企鹅电竞Android weex优化
  7. linux spidev 应用_Linux下SPI驱动的移植和应用程序的测试
  8. KVM 介绍(6):Nova 通过 libvirt 管理 QEMU/KVM 虚机 [Nova Libvirt QEMU/KVM Domain]
  9. linux 中文输入鼠标跳动,解决wps for linux 中文输入法光标不跟随的问题
  10. Win11中文包下载失败?Win11无法安装中文包语言包解决方法
  11. 流行前端几大UI框架排行榜
  12. 深入分析AIL语言及init.rc文件
  13. 【学习记录——unity 3D】Stealth秘密行动游戏制作(一)
  14. 百度地图API V2.0 离线版本
  15. 清爽的VS开发字体 -- Consolas
  16. 信源编码课程实验一:基于Audacity的浊音、清音和爆破音信号的时域及频域特性分析
  17. html在线添加页码,Wkhtmltopdf添加页码
  18. python笔记05: 程序结构
  19. 前端(DOM 、BOM 和 事件 )
  20. 十六进制与字符串的转换

热门文章

  1. 动手DIY一个智能镜子
  2. ios html跳转appstore,H5跳转app store问题
  3. 【算法java版09】:利用java实现对二进制数进行AMI编码
  4. 尼枚罗指数matlab,洛伦兹系统李雅普诺夫指数的MATLAB源代码
  5. C语言输出图形:宝塔形(三角形)字母。第一行A,第二行BB,第三行CCC……
  6. PLC通过智能网关,实现HTTP协议的POST请求,与服务端JSON格式双向通讯
  7. STM32F4 OLED详解
  8. 计数器matlab,MATLAB中的几个时间计数器
  9. 原子结构示意图全部_所有原子结构示意图 (1)
  10. 浅析局域网聊天软件的能力