前言
最近一直在思考一個(gè)問題,網(wǎng)絡(luò)層的要如何設(shè)計(jì)后豫?
純粹的網(wǎng)絡(luò)層
何為純粹呢涌穆?就是服務(wù)器返回什么數(shù)據(jù),回調(diào)給業(yè)務(wù)層就是什么數(shù)據(jù)独旷。因?yàn)槲野l(fā)現(xiàn)有些基于AFNetworking封裝的網(wǎng)絡(luò)框架耦合太多業(yè)務(wù)的東西署穗,例如他們返回的數(shù)據(jù)都是經(jīng)過處理寥裂,很多人都直接轉(zhuǎn)化為模型數(shù)據(jù)返回。
設(shè)計(jì)一個(gè)App整體架構(gòu)的時(shí)候案疲,我傾向于各層次分明封恰,網(wǎng)絡(luò)層就純粹做網(wǎng)絡(luò)請求,持久層就純粹做強(qiáng)業(yè)務(wù)的數(shù)據(jù)的增刪查改褐啡,業(yè)務(wù)層就純粹做與業(yè)務(wù)相關(guān)的東西诺舔,各層之間通信不是直接耦合,而是通過構(gòu)建中間層來實(shí)現(xiàn)备畦,例如業(yè)務(wù)層與網(wǎng)絡(luò)層之間夾著Service層低飒,業(yè)務(wù)層與持久層之間夾著DataManager層。說回到我們的網(wǎng)絡(luò)層的設(shè)計(jì)懂盐,我的思路就是He gives me what逸嘀,I give you what。
緩存問題
引用iOS程序犭袁的一篇文章iOS網(wǎng)絡(luò)緩存掃盲篇--使用兩行代碼就能完成80%的緩存需求的觀點(diǎn)允粤,緩存按功能可以分為兩種:優(yōu)化型緩存和功能型緩存。優(yōu)化型緩存翼岁,可以類比瀏覽器的緩存类垫,它的好處就是減少相應(yīng)的延遲、減少網(wǎng)絡(luò)的帶寬消耗琅坡、更好的用戶體驗(yàn)(用戶不用空等菊花轉(zhuǎn))悉患,這種緩存是網(wǎng)絡(luò)層設(shè)計(jì)需要考慮的;而功能型緩存主要做離線緩存榆俺,這是持久層設(shè)計(jì)需要考慮售躁。
其實(shí)蘋果已經(jīng)幫我們提供一個(gè)緩存解決方案:NSURLCache,他依賴于HTTP的緩存機(jī)制茴晋,但是他并不是100%完美的:
1陪捷、只支持GET請求的緩存,有時(shí)候我們也會用POST請求獲取數(shù)據(jù)诺擅,然而這部分?jǐn)?shù)據(jù)就無法使用NSURLCache進(jìn)行緩存市袖;
2、不夠靈活烁涌,所有緩存都存儲在同一個(gè)文件(記得好像是sql文件苍碟,如果有錯(cuò),大家指正一下)撮执,不能分別指定每一個(gè)文件緩存的位置微峰;
3、不能指定緩存淘汰策略抒钱;
重復(fù)請求問題
為了刷新數(shù)據(jù)或者加載更多數(shù)據(jù)蜓肆,用戶會觸發(fā)上下拉刷新颜凯,當(dāng)網(wǎng)絡(luò)狀態(tài)不好的時(shí)候,用戶可能會不斷地去刷新症杏,這樣會觸發(fā)很多個(gè)重復(fù)的網(wǎng)絡(luò)請求装获,回調(diào)邏輯會觸發(fā)很多次,不作處理的話厉颤,不僅會浪費(fèi)用戶流量穴豫,還會造成數(shù)據(jù)錯(cuò)亂(數(shù)據(jù)列表存在很多重復(fù)的數(shù)據(jù))。
原有的AFNetworking沒有提供直接的方法解決上面的問題逼友,所以我在AFNetworking3.0的基礎(chǔ)上做了一層封裝精肃,XDNetworking就此誕生。
成長中的XDNetworking
XDNetworking是集約型的網(wǎng)絡(luò)框架帜乞,發(fā)起網(wǎng)絡(luò)請求集中在一個(gè)類上司抱,統(tǒng)一管理,適合中小型的項(xiàng)目黎烈,需要對網(wǎng)絡(luò)請求進(jìn)行更加細(xì)致的配置和管理习柠,這個(gè)網(wǎng)絡(luò)框架可能不太適合,這種需求需要一個(gè)離散型的網(wǎng)絡(luò)框架做支撐照棋。下一版的計(jì)劃就是將XDNetworking轉(zhuǎn)化為一個(gè)離散型的網(wǎng)絡(luò)框资溃。大家敬請期待。
框架結(jié)構(gòu)
XDNetworking:提供調(diào)用的API
RequestManager目錄:存放請求管理相關(guān)的類
Cache目錄:存放緩存管理相關(guān)的類
AFSourceCore目錄:存放AFNetworking的源碼
API設(shè)計(jì)
API面向業(yè)務(wù)更加友好烈炭,回調(diào)方式采用block溶锭,基礎(chǔ)功能包括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;
請求相關(guān)
/**
* 正在運(yùn)行的網(wǎng)絡(luò)任務(wù)
*
* @return
*/
+ (NSArray *)currentRunningTasks;
/**
* 取消GET請求
*/
+ (void)cancelRequestWithURL:(NSString *)url;
/**
* 取消所有請求
*/
+ (void)cancleAllRequest;
緩存相關(guān)
@interface XDNetworking (cache)
/**
* 獲取緩存目錄路徑
*
* @return 緩存目錄路徑
*/
+ (NSString *)getCacheDiretoryPath;
/**
* 獲取下載目錄路徑
*
* @return 下載目錄路徑
*/
+ (NSString *)getDownDirectoryPath;
/**
* 獲取緩存大小
*
* @return 緩存大小
*/
+ (NSUInteger)totalCacheSize;
/**
* 清除所有緩存
*/
+ (void)clearTotalCache;
/**
* 獲取所有下載數(shù)據(jù)大小
*
* @return 下載數(shù)據(jù)大小
*/
+ (NSUInteger)totalDownloadDataSize;
/**
* 清除下載數(shù)據(jù)
*/
+ (void)clearDownloadData;
@end
重復(fù)請求管理
大家會發(fā)現(xiàn)GET和POST的API有refresh參數(shù),這個(gè)參數(shù)的主要目的是用于刷新請求丽蝎,當(dāng)遇到重復(fù)請求時(shí)欺栗,若為YES,則會取消舊的請求征峦,用新的請求迟几,若為NO,則忽略新請求栏笆,用舊請求类腮,大家針對自己的業(yè)務(wù)需求自己取舍。下面我給大家分析是如何判斷重復(fù)和刷新請求的蛉加。
大家可以點(diǎn)進(jìn)GET請求或者POST請求的源碼查看他的判斷方法:
if ([self haveSameRequestInTasksPool:session] && !refresh) {
//取消新請求
[session cancel];
return session;
}else {
//無論是否有舊請求蚜枢,先執(zhí)行取消舊請求缸逃,反正都需要刷新請求
XDURLSessionTask *oldTask = [self cancleSameRequestInTasksPool:session];
if (oldTask) [[self allTasks] removeObject:oldTask];
if (session) [[self allTasks] addObject:session];
[session resume];
return session;
}
判斷的相關(guān)邏輯在XDNetworking+requestManager.h這個(gè)分類文件中,大家可以看下它提供的API厂抽,注釋在代碼中:
/**
* 判斷網(wǎng)絡(luò)請求池中是否有相同的請求
*
* @param task 網(wǎng)絡(luò)請求任務(wù)
*
* @return
*/
+ (BOOL)haveSameRequestInTasksPool:(XDURLSessionTask *)task;
/**
* 如果有舊請求則取消舊請求
*
* @param task 新請求
*
* @return 舊請求
*/
+ (XDURLSessionTask *)cancleSameRequestInTasksPool:(XDURLSessionTask *)task;
判斷一個(gè)請求是否重復(fù)需频,也就是判斷新來的請求和舊的請求是否一樣,判斷的依據(jù)有以下幾點(diǎn):
1筷凤、請求的方法是否相同昭殉,是否同為GET或者同為POST;
2藐守、請求的url是否相同挪丢,如果是GET請求,到這一步就可以做出判斷了卢厂,如果是POST請求乾蓬,則還需進(jìn)行下一步的驗(yàn)證;
3慎恒、請求體的內(nèi)容是否相同(POST請求的參數(shù)放在HTTP body里)任内;
于是我為NSURLRequest拓展一個(gè)分類用于判斷請求異同:
@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當(dāng)前的運(yùn)行任務(wù)(調(diào)用currentRunningTasks獲取當(dāng)前的運(yùn)行任務(wù))强挫,根據(jù)任務(wù)源請求判斷新來的請求,是否已經(jīng)有相同的請求正在執(zhí)行當(dāng)中:
+ (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;
}
取消舊請求的邏輯也很容易實(shí)現(xiàn)二庵,遍歷獲取重復(fù)的請求丹鸿,調(diào)用它的cancle方法就取消舊請求了。
緩存方案的設(shè)計(jì)
先說說如何啟動我們的緩存機(jī)制棚品,GET和POST的API有一個(gè)cache參數(shù)靠欢,它用于給大家決定是否開啟緩存機(jī)制,大家針對自己的業(yè)務(wù)數(shù)據(jù)的特征來決定是否開啟cache,即時(shí)性或時(shí)效性的數(shù)據(jù)建議不開啟緩存铜跑,一般建議開啟门怪,開啟緩存后會回調(diào)兩次,第一次獲取是緩存數(shù)據(jù)锅纺,第二次獲取的是最新的網(wǎng)絡(luò)數(shù)據(jù)掷空。
在上面我們已經(jīng)分析了NSURLCache的局限性,而且基于HTTP緩存機(jī)制做的緩存需要客戶端和服務(wù)器雙邊配合囤锉。所以這里我自己設(shè)計(jì)了一個(gè)網(wǎng)絡(luò)的緩存方案坦弟,思路來源SDWebImage。
我的緩存方案分兩級緩存:內(nèi)存緩存和磁盤緩存官地,緩存的過程我給大家簡單的梳理下:第一次請求獲取響應(yīng)數(shù)據(jù)酿傍,先緩存到內(nèi)存,再緩存到磁盤驱入,下一次再發(fā)起相同的請求時(shí)赤炒,會先查找內(nèi)存之中會不會有相應(yīng)的緩存氯析,如果有則返回緩存數(shù)據(jù),如果沒有莺褒,則向磁盤查找掩缓,如果磁盤存在緩存則返回,否則發(fā)起網(wǎng)絡(luò)請求獲取數(shù)據(jù)遵岩。
上面就是緩存的整一個(gè)過程你辣,思路還是比較清晰,除此之外旷余,緩存的設(shè)計(jì)還需要考慮兩個(gè)問題:
1绢记、緩存的淘汰策略
2、緩存的過期機(jī)制
針對以上那些內(nèi)容正卧,我通過解析源碼的方式給大家過一遍:
緩存相關(guān)的類放在Cache這個(gè)目錄下:
XDCacheManager是一個(gè)緩存管理類蠢熄,暴露出簡單的API給XDNetworking進(jìn)行緩存的存取,底層是使用XDMemoryCache(NSCache)進(jìn)行內(nèi)存緩存炉旷,使用XDDiskCache(NSFileManager)進(jìn)行磁盤緩存签孔,緩存淘汰策略采用LRU算法(XD_LRUManager)。它是一個(gè)單例窘行,通過一個(gè)全局入口統(tǒng)一訪問:
+ (XDCacheManager *)shareManager;
默認(rèn)是磁盤大小是40MB饥追,有效期是7天,如果想自定義設(shè)置罐盔,可以通過以下方法設(shè)置:
- (void)setCacheTime:(NSTimeInterval) time diskCapacity:(NSUInteger) capacity;
API:
存緩存的調(diào)用棧:
- [XDCacheManager cacheResponseObject:requestUrl:params:]
- [XDMemoryCache writeData:forKey:]
- [XDDiskCache writeData:toDir:filename:]
- [XD_LRUManager addFileNode:]
取緩存的調(diào)用棧:
- [XDCacheManager getCacheResponseObjectWithRequestUrl:params:]
- [XDMemoryCache readDataWithKey:]
- [XDDiskCache readDataFromDir:filename:]
- [XD_LRUManager refreshIndexOfFileNode:]
刪除LRU緩存的調(diào)用棧:
- [XDCacheManager clearLRUCache]
- [XD_LRUManager removeLRUFileNodeWithCacheTime:]
- [XDDiskCache deleteCache:]
每一份緩存都有一個(gè)唯一的索引建但绕,從方法的參數(shù)可以看出這個(gè)鍵是由請求的url和請求參數(shù)決定,大家不用擔(dān)心接口和參數(shù)的暴露問題惶看,鍵不是直接url加參數(shù)捏顺,而是兩者共同作用的哈希值(采用MD5哈希算法)。
這里重點(diǎn)講下XD_LRUManager做了哪些處理纬黎。
XD_LRUManager
XD_LRUManager是一個(gè)基于LRU(最近最少使用算法)實(shí)現(xiàn)的緩存數(shù)據(jù)管理類幅骄,它底層是由一個(gè)動態(tài)數(shù)組實(shí)現(xiàn)的隊(duì)列,數(shù)組的元素是字典本今,字典包含兩個(gè)鍵:fileName(緩存文件名字)和date(緩存文文件最近的訪問時(shí)間)拆座,這個(gè)隊(duì)列保存在NSUserDefault里。
LRU算法的實(shí)現(xiàn):
創(chuàng)建一個(gè)隊(duì)列冠息,新加的結(jié)點(diǎn)添加在隊(duì)列的尾部挪凑;命中緩存時(shí),調(diào)整結(jié)點(diǎn)的位置逛艰,將其放在隊(duì)列的尾部岖赋;要淘汰緩存時(shí),刪除隊(duì)列的頭部結(jié)點(diǎn)瓮孙。
應(yīng)用情景:
1唐断、當(dāng)有數(shù)據(jù)緩存時(shí)选脊,會調(diào)用XD_LRUManager的addFileNode方法在LRU隊(duì)列上記錄一個(gè)文件結(jié)點(diǎn),文件結(jié)點(diǎn)也就是上面解釋的字典脸甘,記錄文件名和此時(shí)的訪問時(shí)間恳啥,先判斷隊(duì)列是否已經(jīng)存在同文件名的結(jié)點(diǎn),如果有則將結(jié)點(diǎn)取出并插入到隊(duì)列的尾部丹诀,沒有則直接插入到尾部钝的。
在遍歷隊(duì)列查找同文件名的結(jié)點(diǎn)的時(shí)候我做了遍歷優(yōu)化,先將隊(duì)列逆序铆遭,再查找硝桩,因?yàn)樵谖膊康慕Y(jié)點(diǎn)被重用的概率會大一些,從尾部查找會減少遍歷的次數(shù):
//優(yōu)化遍歷
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枚荣、當(dāng)使用緩存的時(shí)候碗脊,說明緩存文件被訪問,所有應(yīng)修改LRU隊(duì)列對應(yīng)文件結(jié)點(diǎn)的最近訪問并它插入LRU隊(duì)列的尾部橄妆,我們通過調(diào)用
XD_LRUManager的refreshIndexOfFileNode方法實(shí)現(xiàn)衙伶,它的實(shí)現(xiàn)原理跟addFileNode一樣。
3害碾、當(dāng)刪除LRU緩存的時(shí)候矢劲,調(diào)用XD_LRUManager的removeLRUFileNodeWithCacheTime方法,他需要傳一個(gè)有效期的參數(shù)慌随,這個(gè)參數(shù)由上層的XDCacheManager提供芬沉。遍歷LRU隊(duì)列,從頭部開始刪除阁猜,刪掉已經(jīng)過期的文件結(jié)點(diǎn)丸逸,用一個(gè)數(shù)組保存刪除的文件結(jié)點(diǎn)里的文件名,用于回調(diào)給上層通過文件名刪除真正的磁盤緩存蹦漠。如果發(fā)現(xiàn)沒有文件過期,則刪除頭結(jié)點(diǎn)车海,它對應(yīng)著最近最少使用的文件:
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];
}
}];
這里有個(gè)注意點(diǎn),就是每次操作完LRU隊(duì)列笛园,無論是增刪查改,都要強(qiáng)制刷新LRU隊(duì)列在NSUserDefault的緩存侍芝。
源碼地址
https://github.com/caixindong/XDNetworking
大家在使用的過程中出現(xiàn)什么問題或者有什么地方不清楚研铆,歡迎來github issue 我。如果大家覺得不錯(cuò)州叠,give me a star棵红。