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)贊但指,最后再次附上代碼下載!