本文转载自崔江涛(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 {