这是一篇别人写的博客,只是为了保存在这里方便查看和写下使用心得。
原文地址:https://www.jianshu.com/p/d0751b9a8d65
前言
之前做项目的时候,由于需要重构网络层,所以自己参考网上的方法对AFNeworking做了一次二次封装,我把一些业务耦合的东西去掉了之后,整理并增加了缓存策略等一些功能就分享出来。
概述
AFN3.1其实已经很封装的很好了,但是还是有一些需求需要自己添加。比如说缓存策略、重复请求管理功能,这些AFN3.1都没有提供直接的方法,所以我在AFN3.1之上做了一层封装,API面向业务层更加友好。
YQNetworking是一个集约型框架,发起请求集中在一个类上,统一管理,适合中小型的项目,需要对网路请求进行更加细致的配置和管理,这个网络框架就可能不太适合。框架目录如下
图片.png方案设计
主要为大家讲解重复管理功能设计和缓存方案设计
重复请求管理方案设计
GET和POST的API有refresh参数,这个参数的主要目的是用于刷新请求,当遇到重复请求时,若为YES,则会取消旧的请求,用新的方法,若为NO,则忽略新请求,用旧请求,大家针对自己的业务需求自己取舍。判断代码如下:
if ([self haveSameRequestInTasksPool:session] && !refresh) {
//取消新请求
[session cancel];
return session;
}else {
//无论是否有旧请求,先执行取消旧请求,反正都需要刷新请求 YQURLSessionTask *oldTask = [self cancleSameRequestInTasksPool:session];
if (oldTask) [[self allTasks] removeObject:oldTask];
if (session) [[self allTasks] addObject:session];
[session resume];
return session;
}
判断的相关逻辑在RequestManager.h这个分类文件中,大家可以看下它提供的API,注释在代码中:
@interface YQNetworking (RequestManager)
/*
*
* 判断网络请求池中是否有相同的请求
*
* @param task 网络请求任务
*
* @return bool
*/
+ (BOOL)haveSameRequestInTasksPool:(YQURLSessionTask *)task; /**
* 如果有旧请求则取消旧请求
*
* @param task 新请求
*
* @return 旧请求
*/
+ (YQURLSessionTask *)cancleSameRequestInTasksPool:(YQURLSessionTask *)task;
判断一个请求是否重复,也就是判断新来的请求和旧的请求是否一样,判断的依据有一些爱几点:
1.请求的方法是否相同,是否同为GET和POST;
2.请求的URL是否相同,如果是GET请求,到这一步就可以做出判断了,如果是POST请求,则还需进行下一步的验证;
3.请求体的内容是否相同(POST请求的参数放在HTTP body里);
遍历YQNetworking当前的运行任务(调动currentRunningTasks获取当前的运行任务),根据任务源请求判断新来的请求,是否已经有相同的请求正在执行当中:
+ (BOOL)haveSameRequestInTasksPool:(XDURLSessionTask *)task {
__block BOOL isSame = NO;
[[self currentRunningTasks] enumerateObjectsUsingBlock:^(XDURLSessionTask *obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([task.originalRequest isTheSameRequest:obj.originalRequest]) {
isSame = YES;
*stop = YES;
}
}];
return isSame;
}
取消旧的请求逻辑也很容易实现,遍历获取重复的请求,调用它的cancel方法就可以了
缓存方案的设计
先说说如何启动我们的缓存机制,GET和POST的API有一个cache参数,它用于给大家决定是否开启缓存机制,大家针对自己的业务数据的特征来决定是否开启cache,即时性和时效性的数据建议不开启缓存,一般建议开启,开启缓存后会回调两次,第一次获取是缓存数据,第二次获取的是最新的网络数据
在上边我们已经分析了NSURLCache的局限性,基于HTTP缓存机制的缓存方案需要客户端和服务器双边配合。所以我自己设计了一个网络的请求方案,思路来自SDWebImage
我的缓存方案分两级缓存:内存缓存和磁盘缓存,缓存的过程如下:
第一次请求获取相应数据,先缓存到内存,再缓存到磁盘,下一次再发起相同的请求时,会先查找内存之中会不会有相应的缓存,如果有则返回缓存数据,如果没有,则向磁盘查找,如果磁盘存在缓存则返回,否则发起网络请求获取数据
上面就是缓存的整一个过程,思路还是比较清晰,除此之外,缓存的设计还需要考虑两个问题:
1.缓存的淘汰策略
2.缓存的过期机制
YQCacheManager是一个缓存管理类,暴露出简单的API给XDNetworking进行缓存的存取,底层是使用YQMemoryCache(NSCache)进行内存缓存,使用YQDiskCache(NSFileManager)进行磁盘缓存,缓存淘汰策略采用LRU算法(YQLRUManager)。它是一个单例,通过一个全局入口统一访问:
+ (YQCacheManager *)shareManager;
默认是磁盘大小是40MB,有效期是7天,如果想自定义设置,可以通过以下方法设置:
- (void)setCacheTime:(NSTimeInterval) time diskCapacity:(NSUInteger) capacity;
问题:
关于缓存方面,这里有一个问题。在运行程序后第一次请求获取的缓存是NSData格式,并不是之后获取到的字典格式。
作者的获取缓存分为从内存缓存中获取和从沙盒中获取,首先从内存缓存中获取数据,如果有则直接返回response,如果没有再从沙盒中获取。从沙盒中获取到的缓存数据是NSData格式,所以每次运行的第一次请求返回的缓存数据是NSData格式。解决方法就是在回调中判断一下response是否是NSData类型,如果是则自己解析一下,如果不是则直接使用。或者修改作者的框架在YQNetworking->Cache->YQCacheManager.m文件的78行
-(id)getCacheResponseObjectWithRequestUrl:(NSString *)requestUrl
params:(NSDictionary *)params
这个方法里面的97行的判断改为
if (cacheData) {
[[YQLRUManager shareManager] refreshIndexOfFileNode:hash];
cacheData = [NSJSONSerialization JSONObjectWithData:cacheData options:NSJSONReadingMutableContainers error:nil];
}
YQLRUManager
YQLRUManager是一个基于LRU(最近最少使用算法)实现的缓存数据管理类,它底层是由一个动态数组实现的队列,数组的元素的字典,字典包含两个键:fileName (缓存文件名字)和date (缓存文件最近的访问时间),这个队列保存在NSUserDefault里。
LRU算法的实现:
创建一个队列,新加的节点添加在队列的尾部;命中缓存时,调整结点的位置,将其放在队列的尾部;要淘汰缓存时,删除队列的头部节点。
应用场景:
1.当有数据缓存时,会调用YQLRUManager的addFileNode方法在LRU队列上记录一个文件节点,文件结点也就是上面解释的字典,记录文件名和此时的访问时间,先判断队列是否已经存在同文件名的结点,如果有则将节点去除并插入到队列的尾部,没有则直接插入到尾部。在遍历队列查找同文件名的结点的时候我做了遍历优化,先将队列逆序,在查找,因为在尾部的结点被重用的概率会打一些,从尾部查找会减少遍历的次数:
//优化遍历
NSArray *reverseArray = [[array reverseObjectEnumerator] allObjects];
[reverseArray enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL * _Nonnull stop) {
if ([obj[@"fileName"] isEqualToString:filename]) {
[operationQueue removeObjectAtIndex:idx];
*stop = YES;
}
}];
2.当时用缓存的时候,说明缓存文件被访问,所有应修改LRU队列对应文件结点的最近访问并它插入LRU的尾部,我们通过调用refreshIndexOfFileNode方法实现,它的实现原理根addFileNode一样。
3.当删除LRU缓存的时候,调用removeLRUFileNodeWithCacheTime方法,他需要传一个有效期的参数,这个参数由上层的YQCacheManager提供。遍历LRU队列,从头部开始删除,删掉已经过期的文件节点,用一个数组保存删除的文件节点里的文件名,用于回调给上层通过文件名删除真正的磁盘缓存。如果没有文件过期,则删除头结点,它对应着最近最少使用的文件:
NSArray *tmpArray = [operationQueue copy];
[tmpArray enumerateObjectsUsingBlock:^(NSDictionary *obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSDate *date = obj[@"date"];
NSDate *newDate = [date dateByAddingTimeInterval:time];
if ([[NSDate date] compare:newDate] == NSOrderedDescending) {
[result addObject:obj[@"fileName"]];
[operationQueue removeObjectAtIndex:idx];
}
}];
这里有个注意点,就是你每次操作完LRU队列,无论是增删改查,都要强制刷新LRU队列在NSUserDefault的缓存。
API设计
API面向业务更加友好,回调方式采用block,功能包括GET、POST、下载、单文件上传、多文件上传、请求管理、缓存管理
GET请求
/**
* GET请求
*
* @param url 请求路径
* @param cache 是否缓存 如果缓存会走两次回调,第一次是返回的缓存数据,第二次才是请求的数据
* @param refresh 是否刷新请求(遇到重复请求,若为YES,则会取消旧的请求,用新的请求,若为NO,则忽略新请求,用旧请求)
* @param params 拼接参数
* @param progressBlock 进度回调
* @param successBlock 成功回调
* @param failBlock 失败回调
*
* @return 返回的对象中可取消请求
*/
+ (YQURLSessionTask *)getWithUrl:(NSString *)url
refreshRequest:(BOOL)refresh
cache:(BOOL)cache
params:(NSDictionary *)params
progressBlock:(YQGetProgress)progressBlock
successBlock:(YQResponseSuccessBlock)successBlock
failBlock:(YQResponseFailBlock)failBlock;
POST请求
/**
* POST请求
*
* @param url 请求路径
* @param cache 是否缓存
* @param refresh 是否刷新,当遇到重复请求时,若为YES,则会取消旧的请求,用新的方法,若为NO,则忽略新请求,用旧请求
* @param params 拼接参数
* @param progressBlock 进度回调
* @param successBlock 成功回调
* @param failBlock 失败回调
*
* @return 返回的对象中可取消请求
*/
+ (YQURLSessionTask *)postWithUrl:(NSString *)url
refreshRequest:(BOOL)refresh
cache:(BOOL)cache
params:(NSDictionary *)params
progressBlock:(YQPostProgress)progressBlock
successBlock:(YQResponseSuccessBlock)successBlock
failBlock:(YQResponseFailBlock)failBlock;
下载请求
/**
* 文件下载
*
* @param url 下载文件接口地址
* @param progressBlock 下载进度
* @param successBlock 成功回调
* @param failBlock 下载回调
*
* @return 返回的对象可取消请求
*/
+ (YQURLSessionTask *)downloadWithUrl:(NSString *)url
progressBlock:(YQDownloadProgress)progressBlock
successBlock:(YQDownloadSuccessBlock)successBlock
failBlock:(YQDownloadFailBlock)failBlock;
文件上传
/**
* 文件上传
*
* @param url 上传文件接口地址
* @param data 上传文件数据
* @param type 上传文件类型(后缀名)
* @param name 上传文件服务器文件夹名
* @param mimeType mimeType
* @param progressBlock 上传文件路径
* @param successBlock 成功回调
* @param failBlock 失败回调
*
* @return 返回的对象中可取消请求
*/
+ (YQURLSessionTask *)uploadFileWithUrl:(NSString *)url
fileData:(NSData *)data
type:(NSString *)type
name:(NSString *)name
mimeType:(NSString *)mimeType
progressBlock:(YQUploadProgressBlock)progressBlock
successBlock:(YQResponseSuccessBlock)successBlock
failBlock:(YQResponseFailBlock)failBlock;
多文件上传
/**
* 多文件上传
*
* @param url 上传文件地址
* @param datas 数据集合
* @param type 类型
* @param name 服务器文件夹名
* @param mimeTypes mimeTypes
* @param progressBlock 上传进度
* @param successBlock 成功回调
* @param failBlock 失败回调
*
* @return 任务集合
*/
+ (NSArray *)uploadMultFileWithUrl:(NSString *)url
fileDatas:(NSArray *)datas
type:(NSString *)type
name:(NSString *)name
mimeType:(NSString *)mimeTypes
progressBlock:(YQUploadProgressBlock)progressBlock
successBlock:(YQMultUploadSuccessBlock)successBlock
failBlock:(YQMultUploadFailBlock)failBlock;
获取当前正在运行的任务
/**
* 正在运行的网络任务
*
* @return task
*/
+ (NSArray *)currentRunningTasks;
取消所有网络请求
+ (void)cancleAllRequest;
取消下载请求
+ (void)cancelRequestWithURL:(NSString *)url;
获取缓存大小
+ (NSUInteger)totalCacheSize;
清理缓存
+ (void)clearTotalCache;
自动清理缓存
//每次网络请求的时候,检查此时磁盘中的缓存大小,阈值默认是40MB,如果超过阈值,则清理LRU缓
//存,同时也会清理过期缓存,缓存默认SSL是7天,磁盘缓存的大小和SSL的设置可以通过该方法
//[YQCacheManager shareManager] setCacheTime: diskCapacity:]设置。
[[YQCacheManager shareManager] clearLRUCache];
网友评论