iOS架構(gòu)設(shè)計(jì)-URL緩存(下)

iOS架構(gòu)設(shè)計(jì)-URL緩存(下)

2017-07-15崔江濤Cocoa開(kāi)發(fā)者社區(qū)

本文轉(zhuǎn)載自崔江濤(KenshinCui)

緩存設(shè)計(jì)

從前面對(duì)于URL Loading System的分析可以看出利用NSURLProtocol或者NSURLCache都可以做客戶端緩存,但是NSURLProtocol更多的用于攔截處理,而且如果使用它來(lái)做緩存的話需要自己發(fā)起請(qǐng)求册踩。而選擇URLSession配合NSURLCache的話,則對(duì)于接口調(diào)用方有更多靈活的控制朽基,而且默認(rèn)情況下NSURLCache就有緩存敛劝,我們只要操作緩存響應(yīng)的Cache headers即可丐黄,因此后者作為我們優(yōu)先考慮的設(shè)計(jì)方案巍佑。鑒于本文代碼使用Swift編寫(xiě)茴迁,因此結(jié)合目前Swift中流行的網(wǎng)絡(luò)庫(kù)Alamofire實(shí)現(xiàn)一種相對(duì)簡(jiǎn)單的緩存方案。

根據(jù)前面的思路萤衰,最早還是想從URLSessionDataDelegate的緩存設(shè)置方法入手堕义,而且Alamofire確實(shí)對(duì)于每個(gè)URLSessionDataTask都留有緩存代理方法的回調(diào)入口,但查看源碼發(fā)現(xiàn)這個(gè)入口dataTaskWillCacheResponse并未對(duì)外開(kāi)發(fā)脆栋,而如果直接在SessionDelegate的回調(diào)入口dataTaskWillCacheResponseWithCompletion上進(jìn)行回調(diào)又無(wú)法控制每個(gè)請(qǐng)求的緩存情況(NSURLSession是多個(gè)請(qǐng)求共用的)倦卖。當(dāng)然如果沿著這個(gè)思路可以再擴(kuò)展一個(gè)DataTaskDelegate對(duì)象以暴漏緩存入口,但是這么一來(lái)必須實(shí)現(xiàn)URLSessionDataDelegate,而且要想辦法Swizzle NSURLSession的緩存代理(或者繼承SessionDelegate切換代理),在代理中根據(jù)不同的NSURLDataTask進(jìn)行緩存處理筹吐,整個(gè)過(guò)程對(duì)于調(diào)用方并不是太友好糖耸。

另一個(gè)思路就是等Response請(qǐng)求結(jié)束后獲取緩存的響應(yīng)CachedURLResponse并且修改(事實(shí)上只要是同一個(gè)NSURLRequest存儲(chǔ)進(jìn)去默認(rèn)會(huì)更新原有緩存),而且NSURLCache本身就是有內(nèi)存緩存的丘薛,過(guò)程并不會(huì)太耗時(shí)。當(dāng)然這個(gè)方案最重要的是得保證響應(yīng)完成邦危,所以這里通過(guò)Alamofire鏈?zhǔn)秸{(diào)用使用response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler重新請(qǐng)求以保證及時(shí)掌握回調(diào)時(shí)機(jī)洋侨。主要的代碼片段如下:

public func cache(maxAge:Int,isPrivate:Bool = false,ignoreServer:Bool = true)

-> Self

{

var useServerButRefresh = false

if let newRequest = self.request {

if !ignoreServer {

if newRequest.allHTTPHeaderFields?[AlamofireURLCache.refreshCacheKey] == AlamofireURLCache.RefreshCacheValue.refreshCache.rawValue {

useServerButRefresh = true

}

}

if newRequest.allHTTPHeaderFields?[AlamofireURLCache.refreshCacheKey] != AlamofireURLCache.RefreshCacheValue.refreshCache.rawValue {

if let urlCache = self.session.configuration.urlCache {

if let value = (urlCache.cachedResponse(for: newRequest)?.response as? HTTPURLResponse)?.allHeaderFields[AlamofireURLCache.refreshCacheKey] as? String {

if value == AlamofireURLCache.RefreshCacheValue.useCache.rawValue {

return self

}

}

}

}

}

return response { [unowned self](defaultResponse) in

if defaultResponse.request?.httpMethod != "GET" {

debugPrint("Non-GET requests do not support caching!")

return

}

if defaultResponse.error != nil {

debugPrint(defaultResponse.error!.localizedDescription)

return

}

if let httpResponse = defaultResponse.response {

guard let newRequest = defaultResponse.request else { return }

guard let newData = defaultResponse.data else { return }

guard let newURL = httpResponse.url else { return }

guard let urlCache = self.session.configuration.urlCache else { return }

guard let newHeaders = (httpResponse.allHeaderFields as NSDictionary).mutableCopy() as? NSMutableDictionary else { return }

if AlamofireURLCache.isCanUseCacheControl {

if httpResponse.allHeaderFields["Cache-Control"] == nil || httpResponse.allHeaderFields.keys.contains("no-cache") || httpResponse.allHeaderFields.keys.contains("no-store") || ignoreServer || useServerButRefresh {

DataRequest.addCacheControlHeaderField(headers: newHeaders, maxAge: maxAge, isPrivate: isPrivate)

} else {

return

}

} else {

if httpResponse.allHeaderFields["Expires"] == nil || ignoreServer || useServerButRefresh {

DataRequest.addExpiresHeaderField(headers: newHeaders, maxAge: maxAge)

if ignoreServer && httpResponse.allHeaderFields["Pragma"] != nil {

newHeaders["Pragma"] = "cache"

}

} else {

return

}

}

newHeaders[AlamofireURLCache.refreshCacheKey] = AlamofireURLCache.RefreshCacheValue.useCache.rawValue

if let newResponse = HTTPURLResponse(url: newURL, statusCode: httpResponse.statusCode, httpVersion: AlamofireURLCache.HTTPVersion, headerFields: newHeaders as? [String : String]) {

let newCacheResponse = CachedURLResponse(response: newResponse, data: newData, userInfo: ["framework":AlamofireURLCache.frameworkName], storagePolicy: URLCache.StoragePolicy.allowed)

urlCache.storeCachedResponse(newCacheResponse, for: newRequest)

}

}

}

}

要完成整個(gè)緩存處理自然還包括緩存刷新、緩存清理等操作倦蚪,關(guān)于緩存清理本身NSURLCache是提供了remove方法的希坚,不過(guò)緩存清理并不及時(shí),調(diào)用并不會(huì)立即生效陵且,具體參見(jiàn)NSURLCache does not clear stored responses in iOS8裁僧。因此,這里借助了上面提到的Cache-Control進(jìn)行緩存過(guò)期控制慕购,一方面可以快速清理緩存聊疲,另一方面緩存控制可以更加精確。

AlamofireURLCache

為了更好的配合Alamofire使用沪悲,此代碼以AlamofireURLCache類(lèi)庫(kù)形式在github開(kāi)源获洲,所有接口API盡量和原有接口保持一致,便于對(duì)Alamofire二次封裝殿如。此外還提供了手動(dòng)清理緩存贡珊、出錯(cuò)之后自動(dòng)清理緩存最爬、覆蓋服務(wù)器端緩存配置等方便的功能,可以滿足多數(shù)情況下緩存需求細(xì)節(jié)门岔。

AlamofireURLCache在request方法添加了refreshCache參數(shù)用于緩存刷新爱致,設(shè)為false或者不提供此參數(shù)則不會(huì)刷新緩存,只有等到上次緩存數(shù)據(jù)過(guò)了有效期才會(huì)再次發(fā)起請(qǐng)求寒随。

Alamofire.request("https://myapi.applinzi.com/url-cache/no-cache.php",refreshCache:false).responseJSON(completionHandler: { response in

if response.value != nil {

self.textView.text = (response.value as! [String:Any]).debugDescription

} else {

self.textView.text = "Error!"

}

}).cache(maxAge: 10)

服務(wù)器端緩存headers設(shè)置并不都是最優(yōu)選擇糠悯,某些情況下客戶端必須自行控制緩存策略,此時(shí)可以使用AlamofireURLCache的ignoreServer參數(shù)忽略服務(wù)器端配置牢裳,通過(guò)maxAge參數(shù)自行控制緩存時(shí)長(zhǎng)逢防。

Alamofire.request("https://myapi.applinzi.com/url-cache/default-cache.php",refreshCache:false).responseJSON(completionHandler: { response in

if response.value != nil {

self.textView.text = (response.value as! [String:Any]).debugDescription

} else {

self.textView.text = "Error!"

}

}).cache(maxAge: 10,isPrivate: false,ignoreServer: true)

另外,有些情況下未必需要刷新緩存而是要清空緩存保證下次訪問(wèn)時(shí)再使用最新數(shù)據(jù)蒲讯,此時(shí)就需要使用AlamofireURLCache提供的緩存清理API來(lái)完成忘朝。需要特別說(shuō)明的是,對(duì)于請(qǐng)求出錯(cuò)判帮、序列化出錯(cuò)等情況如果調(diào)用了cache(maxAge)方法進(jìn)行緩存后局嘁,那么下次請(qǐng)求會(huì)使用錯(cuò)誤的緩存數(shù)據(jù),需要開(kāi)發(fā)人員根據(jù)返回情況自行調(diào)用API清理緩存晦墙。但更好的選擇是使用AlamofireURLCache提供的autoClearCache參數(shù)來(lái)自動(dòng)處理此種情況悦昵,所以任何時(shí)候都推薦將autoClearCache參數(shù)設(shè)為true以保證不會(huì)緩存出錯(cuò)數(shù)據(jù)。

Alamofire.clearCache(dataRequest: dataRequest) // clear cache by DataRequest

Alamofire.clearCache(request: urlRequest) // clear cache by URLRequest

// ignore data cache when request error

Alamofire.request("https://myapi.applinzi.com/url-cache/no-cache.php",refreshCache:false).responseJSON(completionHandler: { response in

if response.value != nil {

self.textView.text = (response.value as! [String:Any]).debugDescription

} else {

self.textView.text = "Error!"

}

},autoClearCache:true).cache(maxAge: 10)

如果閱讀本文讓你有所收獲晌畅,歡迎推薦點(diǎn)贊但指,最后再次附上代碼下載!

代碼下載

閱讀原文

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末抗楔,一起剝皮案震驚了整個(gè)濱河市棋凳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌连躏,老刑警劉巖剩岳,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異入热,居然都是意外死亡拍棕,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)勺良,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)绰播,“玉大人,你說(shuō)我怎么就攤上這事郑气》澹” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)忙芒。 經(jīng)常有香客問(wèn)我示弓,道長(zhǎng),這世上最難降的妖魔是什么呵萨? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任奏属,我火速辦了婚禮,結(jié)果婚禮上潮峦,老公的妹妹穿的比我還像新娘囱皿。我一直安慰自己,他們只是感情好忱嘹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布嘱腥。 她就那樣靜靜地躺著,像睡著了一般拘悦。 火紅的嫁衣襯著肌膚如雪齿兔。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天础米,我揣著相機(jī)與錄音分苇,去河邊找鬼。 笑死屁桑,一個(gè)胖子當(dāng)著我的面吹牛医寿,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蘑斧,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼靖秩,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了竖瘾?” 一聲冷哼從身側(cè)響起盆偿,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎准浴,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體捎稚,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡乐横,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了今野。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片葡公。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖条霜,靈堂內(nèi)的尸體忽然破棺而出催什,到底是詐尸還是另有隱情,我是刑警寧澤宰睡,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布蒲凶,位于F島的核電站气筋,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏旋圆。R本人自食惡果不足惜宠默,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望灵巧。 院中可真熱鬧搀矫,春花似錦、人聲如沸刻肄。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)敏弃。三九已至卦羡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間权她,已是汗流浹背虹茶。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留隅要,地道東北人蝴罪。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像步清,于是被迫代替她去往敵國(guó)和親要门。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容