iOS架构设计-URL缓存(下)
2017-07-15崔江涛Cocoa开发者社区
本文转载自崔江涛(KenshinCui)
缓存设计
从前面对于URL Loading System的分析可以看出利用NSURLProtocol或者NSURLCache都可以做客户端缓存,但是NSURLProtocol更多的用于拦截处理,而且如果使用它来做缓存的话需要自己发起请求。而选择URLSession配合NSURLCache的话,则对于接口调用方有更多灵活的控制,而且默认情况下NSURLCache就有缓存,我们只要操作缓存响应的Cache headers即可,因此后者作为我们优先考虑的设计方案。鉴于本文代码使用Swift编写,因此结合目前Swift中流行的网络库Alamofire实现一种相对简单的缓存方案。
根据前面的思路,最早还是想从URLSessionDataDelegate的缓存设置方法入手,而且Alamofire确实对于每个URLSessionDataTask都留有缓存代理方法的回调入口,但查看源码发现这个入口dataTaskWillCacheResponse并未对外开发,而如果直接在SessionDelegate的回调入口dataTaskWillCacheResponseWithCompletion上进行回调又无法控制每个请求的缓存情况(NSURLSession是多个请求共用的)。当然如果沿着这个思路可以再扩展一个DataTaskDelegate对象以暴漏缓存入口,但是这么一来必须实现URLSessionDataDelegate,而且要想办法Swizzle NSURLSession的缓存代理(或者继承SessionDelegate切换代理),在代理中根据不同的NSURLDataTask进行缓存处理,整个过程对于调用方并不是太友好。
另一个思路就是等Response请求结束后获取缓存的响应CachedURLResponse并且修改(事实上只要是同一个NSURLRequest存储进去默认会更新原有缓存),而且NSURLCache本身就是有内存缓存的,过程并不会太耗时。当然这个方案最重要的是得保证响应完成,所以这里通过Alamofire链式调用使用response(queue: queue, responseSerializer: responseSerializer, completionHandler: completionHandler重新请求以保证及时掌握回调时机。主要的代码片段如下:
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)
}
}
}
}
要完成整个缓存处理自然还包括缓存刷新、缓存清理等操作,关于缓存清理本身NSURLCache是提供了remove方法的,不过缓存清理并不及时,调用并不会立即生效,具体参见NSURLCache does not clear stored responses in iOS8。因此,这里借助了上面提到的Cache-Control进行缓存过期控制,一方面可以快速清理缓存,另一方面缓存控制可以更加精确。
AlamofireURLCache
为了更好的配合Alamofire使用,此代码以AlamofireURLCache类库形式在github开源,所有接口API尽量和原有接口保持一致,便于对Alamofire二次封装。此外还提供了手动清理缓存、出错之后自动清理缓存、覆盖服务器端缓存配置等方便的功能,可以满足多数情况下缓存需求细节。
AlamofireURLCache在request方法添加了refreshCache参数用于缓存刷新,设为false或者不提供此参数则不会刷新缓存,只有等到上次缓存数据过了有效期才会再次发起请求。
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)
服务器端缓存headers设置并不都是最优选择,某些情况下客户端必须自行控制缓存策略,此时可以使用AlamofireURLCache的ignoreServer参数忽略服务器端配置,通过maxAge参数自行控制缓存时长。
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)
另外,有些情况下未必需要刷新缓存而是要清空缓存保证下次访问时再使用最新数据,此时就需要使用AlamofireURLCache提供的缓存清理API来完成。需要特别说明的是,对于请求出错、序列化出错等情况如果调用了cache(maxAge)方法进行缓存后,那么下次请求会使用错误的缓存数据,需要开发人员根据返回情况自行调用API清理缓存。但更好的选择是使用AlamofireURLCache提供的autoClearCache参数来自动处理此种情况,所以任何时候都推荐将autoClearCache参数设为true以保证不会缓存出错数据。
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)
如果阅读本文让你有所收获,欢迎推荐点赞,最后再次附上代码下载!