美文网首页iOS网络相关网络iOS
网络层缓存的思考(成长中的XDNetworking)

网络层缓存的思考(成长中的XDNetworking)

作者: 欣东 | 来源:发表于2016-09-16 17:11 被阅读843次

前言

最近一直在思考一个问题,网络层的要如何设计?

纯粹的网络层

何为纯粹呢?就是服务器返回什么数据,回调给业务层就是什么数据。因为我发现有些基于AFNetworking封装的网络框架耦合太多业务的东西,例如他们返回的数据都是经过处理,很多人都直接转化为模型数据返回。
设计一个App整体架构的时候,我倾向于各层次分明,网络层就纯粹做网络请求,持久层就纯粹做强业务的数据的增删查改,业务层就纯粹做与业务相关的东西,各层之间通信不是直接耦合,而是通过构建中间层来实现,例如业务层与网络层之间夹着Service层,业务层与持久层之间夹着DataManager层。说回到我们的网络层的设计,我的思路就是He gives me what,I give you what。

缓存问题

引用iOS程序犭袁的一篇文章iOS网络缓存扫盲篇--使用两行代码就能完成80%的缓存需求的观点,缓存按功能可以分为两种:优化型缓存和功能型缓存。优化型缓存,可以类比浏览器的缓存,它的好处就是减少相应的延迟、减少网络的带宽消耗、更好的用户体验(用户不用空等菊花转),这种缓存是网络层设计需要考虑的;而功能型缓存主要做离线缓存,这是持久层设计需要考虑。
其实苹果已经帮我们提供一个缓存解决方案:NSURLCache,他依赖于HTTP的缓存机制,但是他并不是100%完美的:
1、只支持GET请求的缓存,有时候我们也会用POST请求获取数据,然而这部分数据就无法使用NSURLCache进行缓存;
2、不够灵活,所有缓存都存储在同一个文件(记得好像是sql文件,如果有错,大家指正一下),不能分别指定每一个文件缓存的位置;
3、不能指定缓存淘汰策略;

重复请求问题

为了刷新数据或者加载更多数据,用户会触发上下拉刷新,当网络状态不好的时候,用户可能会不断地去刷新,这样会触发很多个重复的网络请求,回调逻辑会触发很多次,不作处理的话,不仅会浪费用户流量,还会造成数据错乱(数据列表存在很多重复的数据)。

原有的AFNetworking没有提供直接的方法解决上面的问题,所以我在AFNetworking3.0的基础上做了一层封装,XDNetworking就此诞生。

成长中的XDNetworking

XDNetworking是集约型的网络框架,发起网络请求集中在一个类上,统一管理,适合中小型的项目,需要对网络请求进行更加细致的配置和管理,这个网络框架可能不太适合,这种需求需要一个离散型的网络框架做支撑。下一版的计划就是将XDNetworking转化为一个离散型的网络框。大家敬请期待。

框架结构

框架结构框架结构
XDNetworking:提供调用的API
RequestManager目录:存放请求管理相关的类
Cache目录:存放缓存管理相关的类
AFSourceCore目录:存放AFNetworking的源码

API设计

API面向业务更加友好,回调方式采用block,基础功能包括GET、POST、下载、单文件上传、多文件上传、请求管理、缓存管理

GET

+ (XDURLSessionTask *)getWithUrl:(NSString *)url
                  refreshRequest:(BOOL)refresh
                           cache:(BOOL)cache
                          params:(NSDictionary *)params
                   progressBlock:(XDGetProgress)progressBlock
                    successBlock:(XDResponseSuccessBlock)successBlock
                       failBlock:(XDResponseFailBlock)failBlock;

POST

+ (XDURLSessionTask *)postWithUrl:(NSString *)url
                   refreshRequest:(BOOL)refresh
                            cache:(BOOL)cache
                           params:(NSDictionary *)params
                    progressBlock:(XDPostProgress)progressBlock
                     successBlock:(XDResponseSuccessBlock)successBlock
                        failBlock:(XDResponseFailBlock)failBlock;

Download

+ (XDURLSessionTask *)downloadWithUrl:(NSString *)url
                        progressBlock:(XDDownloadProgress)progressBlock
                         successBlock:(XDDownloadSuccessBlock)successBlock
                            failBlock:(XDDownloadFailBlock)failBlock;

Upload

+ (XDURLSessionTask *)uploadFileWithUrl:(NSString *)url
                               fileData:(NSData *)data
                                   type:(NSString *)type
                                   name:(NSString *)name
                               mimeType:(NSString *)mimeType
                          progressBlock:(XDUploadProgressBlock)progressBlock
                           successBlock:(XDResponseSuccessBlock)successBlock
                              failBlock:(XDResponseFailBlock)failBlock;

请求相关

/**
 *  正在运行的网络任务
 *
 *  @return 
 */
+ (NSArray *)currentRunningTasks;

/**
 *  取消GET请求
 */
+ (void)cancelRequestWithURL:(NSString *)url;

/**
 *  取消所有请求
 */
+ (void)cancleAllRequest;

缓存相关

@interface XDNetworking (cache)

/**
 *  获取缓存目录路径
 *
 *  @return 缓存目录路径
 */
+ (NSString *)getCacheDiretoryPath;

/**
 *  获取下载目录路径
 *
 *  @return 下载目录路径
 */
+ (NSString *)getDownDirectoryPath;

/**
 *  获取缓存大小
 *
 *  @return 缓存大小
 */
+ (NSUInteger)totalCacheSize;

/**
 *  清除所有缓存
 */
+ (void)clearTotalCache;

/**
 *  获取所有下载数据大小
 *
 *  @return 下载数据大小
 */
+ (NSUInteger)totalDownloadDataSize;

/**
 *  清除下载数据
 */
+ (void)clearDownloadData;

@end

重复请求管理

大家会发现GET和POST的API有refresh参数,这个参数的主要目的是用于刷新请求,当遇到重复请求时,若为YES,则会取消旧的请求,用新的请求,若为NO,则忽略新请求,用旧请求,大家针对自己的业务需求自己取舍。下面我给大家分析是如何判断重复和刷新请求的。
大家可以点进GET请求或者POST请求的源码查看他的判断方法:

if ([self haveSameRequestInTasksPool:session] && !refresh) {
        //取消新请求
        [session cancel];
        return session;
    }else {
        //无论是否有旧请求,先执行取消旧请求,反正都需要刷新请求
        XDURLSessionTask *oldTask = [self cancleSameRequestInTasksPool:session];
        if (oldTask) [[self allTasks] removeObject:oldTask];
        if (session) [[self allTasks] addObject:session];
        [session resume];
        return session;
    }

判断的相关逻辑在XDNetworking+requestManager.h这个分类文件中,大家可以看下它提供的API,注释在代码中:

/**
 *  判断网络请求池中是否有相同的请求
 *
 *  @param task 网络请求任务
 *
 *  @return
 */
+ (BOOL)haveSameRequestInTasksPool:(XDURLSessionTask *)task;

/**
 *  如果有旧请求则取消旧请求
 *
 *  @param task 新请求
 *
 *  @return 旧请求
 */
+ (XDURLSessionTask *)cancleSameRequestInTasksPool:(XDURLSessionTask *)task;

判断一个请求是否重复,也就是判断新来的请求和旧的请求是否一样,判断的依据有以下几点:
1、请求的方法是否相同,是否同为GET或者同为POST;
2、请求的url是否相同,如果是GET请求,到这一步就可以做出判断了,如果是POST请求,则还需进行下一步的验证;
3、请求体的内容是否相同(POST请求的参数放在HTTP body里);
于是我为NSURLRequest拓展一个分类用于判断请求异同:

@implementation NSURLRequest (decide)

- (BOOL)isTheSameRequest:(NSURLRequest *)request {
    if ([self.HTTPMethod isEqualToString:request.HTTPMethod]) {
        if ([self.URL.absoluteString isEqualToString:request.URL.absoluteString]) {
            if ([self.HTTPMethod isEqualToString:@"GET"]||[self.HTTPBody isEqualToData:request.HTTPBody]) {
                return YES;
            }
        }
    }
    return NO;
}

@end

于是乎,我们可以遍历XDNetworking当前的运行任务(调用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;
}

取消旧请求的逻辑也很容易实现,遍历获取重复的请求,调用它的cancle方法就取消旧请求了。

缓存方案的设计

先说说如何启动我们的缓存机制,GET和POST的API有一个cache参数,它用于给大家决定是否开启缓存机制,大家针对自己的业务数据的特征来决定是否开启cache,即时性或时效性的数据建议不开启缓存,一般建议开启,开启缓存后会回调两次,第一次获取是缓存数据,第二次获取的是最新的网络数据。
在上面我们已经分析了NSURLCache的局限性,而且基于HTTP缓存机制做的缓存需要客户端和服务器双边配合。所以这里我自己设计了一个网络的缓存方案,思路来源SDWebImage。
我的缓存方案分两级缓存:内存缓存和磁盘缓存,缓存的过程我给大家简单的梳理下:第一次请求获取响应数据,先缓存到内存,再缓存到磁盘,下一次再发起相同的请求时,会先查找内存之中会不会有相应的缓存,如果有则返回缓存数据,如果没有,则向磁盘查找,如果磁盘存在缓存则返回,否则发起网络请求获取数据。
上面就是缓存的整一个过程,思路还是比较清晰,除此之外,缓存的设计还需要考虑两个问题:
1、缓存的淘汰策略
2、缓存的过期机制
针对以上那些内容,我通过解析源码的方式给大家过一遍:
缓存相关的类放在Cache这个目录下:

CacheCache
XDCacheManager是一个缓存管理类,暴露出简单的API给XDNetworking进行缓存的存取,底层是使用XDMemoryCache(NSCache)进行内存缓存,使用XDDiskCache(NSFileManager)进行磁盘缓存,缓存淘汰策略采用LRU算法(XD_LRUManager)。它是一个单例,通过一个全局入口统一访问:
+ (XDCacheManager *)shareManager;

默认是磁盘大小是40MB,有效期是7天,如果想自定义设置,可以通过以下方法设置:

- (void)setCacheTime:(NSTimeInterval) time diskCapacity:(NSUInteger) capacity;

API:

存缓存的调用栈:

- [XDCacheManager cacheResponseObject:requestUrl:params:]
    - [XDMemoryCache writeData:forKey:]
    - [XDDiskCache writeData:toDir:filename:]
    - [XD_LRUManager addFileNode:]

取缓存的调用栈:

- [XDCacheManager getCacheResponseObjectWithRequestUrl:params:]
    - [XDMemoryCache readDataWithKey:]
    - [XDDiskCache readDataFromDir:filename:]
    - [XD_LRUManager refreshIndexOfFileNode:]

删除LRU缓存的调用栈:

- [XDCacheManager clearLRUCache]
    - [XD_LRUManager removeLRUFileNodeWithCacheTime:]
    - [XDDiskCache deleteCache:]

每一份缓存都有一个唯一的索引建,从方法的参数可以看出这个键是由请求的url和请求参数决定,大家不用担心接口和参数的暴露问题,键不是直接url加参数,而是两者共同作用的哈希值(采用MD5哈希算法)。
这里重点讲下XD_LRUManager做了哪些处理。

XD_LRUManager

XD_LRUManager是一个基于LRU(最近最少使用算法)实现的缓存数据管理类,它底层是由一个动态数组实现的队列,数组的元素是字典,字典包含两个键:fileName(缓存文件名字)和date(缓存文文件最近的访问时间),这个队列保存在NSUserDefault里。
LRU算法的实现:
创建一个队列,新加的结点添加在队列的尾部;命中缓存时,调整结点的位置,将其放在队列的尾部;要淘汰缓存时,删除队列的头部结点。
应用情景:
1、当有数据缓存时,会调用XD_LRUManager的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队列的尾部,我们通过调用
XD_LRUManager的refreshIndexOfFileNode方法实现,它的实现原理跟addFileNode一样。
3、当删除LRU缓存的时候,调用XD_LRUManager的removeLRUFileNodeWithCacheTime方法,他需要传一个有效期的参数,这个参数由上层的XDCacheManager提供。遍历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的缓存。

源码地址

https://github.com/caixindong/XDNetworking
大家在使用的过程中出现什么问题或者有什么地方不清楚,欢迎来github issue 我。如果大家觉得不错,give me a star。

相关文章

网友评论

  • Damon_Rao:真机测试: createDirectory error is You don’t have permission to save the file “networkCache” in the folder “XDNetworking”. 没有权限是什么原因造成的
    ecac7ef86eaf:同样的问题,真机操作直接告诉我没有权限写入这个地方
    博BlingBing:mark,这个可以看下
    欣东:@Damon_Rao 嗯嗯,我debug下

本文标题:网络层缓存的思考(成长中的XDNetworking)

本文链接:https://www.haomeiwen.com/subject/quriettx.html