iOS架構設計-URL緩存(上)

概覽

緩存組件應該說是每個客戶端程序必備的核心組件堵腹,試想對于每個界面的訪問都必須重新請求勢必降低用戶體驗。但是如何處理客戶端緩存貌似并沒有統(tǒng)一的解決方案,多數(shù)開發(fā)者選擇自行創(chuàng)建數(shù)據(jù)庫直接將服務器端請求的JSON(或Model)緩存起來疫鹊,下次請求則查詢數(shù)據(jù)庫檢查緩存是否存在杉武;另外還有些開發(fā)者會選擇以歸檔文件的方式保存緩存數(shù)據(jù),每次請求資源之前檢查相應的緩存文件褪子。事實上iOS系統(tǒng)自身就提供了一套緩存機制量淌,本文將結合URL

Loading

System介紹一下如何利用系統(tǒng)自身緩存設計來實現(xiàn)一套緩存機制,使用這套緩存設計你無需自己編寫內存和磁盤存儲嫌褪,無需自行檢查緩存過期策略就能輕松實現(xiàn)數(shù)據(jù)緩存呀枢。

URL Loading System

URL Loading System是類和協(xié)議的集合,使用URL

Loading System

iOS系統(tǒng)和服務器端進行網(wǎng)絡交互笼痛。URL作為其中的核心裙秋,能夠讓app和資源進行輕松的交互。為了增強URL的功能Foundation提供了豐富的類集合缨伊,能夠讓你根據(jù)地址加載資源摘刑、上傳資源到服務器、管理cookie刻坊、控制響應緩存(這也是我們今天的重點內容)枷恕、處理證書和認證、擴展用戶協(xié)議(后面也會提到相關內容)等紧唱,因此URL緩存之前熟悉URL

Loading System是必要的活尊。下圖一系列集合的關系:

本文代碼一律使用Swift編寫,但是鑒于很多朋友接觸URL Loading System都是從Objective-C開始漏益,所以文章中文字部分還是采用OC命名驻谆,其區(qū)別不大,主要是少了NS前綴融虽。

NSURLProtocol

URL

Loading System默認支持http柑肴、https、ftp轻庆、file和data

協(xié)議癣猾,但是它同樣也支持你注冊自己的類來支持更多應用層網(wǎng)絡協(xié)議,當然你也可以指定其他屬性到URL reqeust和URL

response上余爆。具體而言NSURLProtocl可以實現(xiàn)以下需求(包含但不限):

重定向網(wǎng)絡請求(或進行域名轉化纷宇、攔截等,例如:netfox)

忽略某些請求蛾方,使用本地緩存數(shù)據(jù)

自定義網(wǎng)絡請求的返回結果 (比如:GYHttpMocking)

進行網(wǎng)絡全局配置

NSURLProtocol類似中間人設計像捶,將網(wǎng)絡求細節(jié)提供給開發(fā)者上陕,而又以一種優(yōu)雅的方式暴漏出來。NSURLProtocol的定義更像是一個URL協(xié)議拓春,盡管它繼承自NSObject卻不能直接使用释簿,使用時自定義協(xié)議繼承NSURLProtocol,然后在app啟動時注冊即可硼莽,這樣一來所有請求細節(jié)開發(fā)者只需要在自己的類中控制即可(這個設計確實完美??)庶溶。

解決DNS劫持

隨著互聯(lián)網(wǎng)的發(fā)展,運營商劫持這些年逐漸被大家所提及懂鸵,常見的劫持包括HTTP劫持和DNS劫持偏螺。對于HTTP劫持更多的是篡改網(wǎng)絡響應加入一些腳本廣告之類的內容,解決這個問題只需要使用https加密請求交互內容矾瑰;而對于DNS劫持則更加可惡砖茸,在DNS解析時讓請求重新定向到一個非預期IP從而達到內容篡改。

解決DNS劫持普遍的做法就是將URL從域名替換成IP殴穴,這么一來訪問內容并不經過運營商的Local

DNS而到達指定的服務器凉夯,因此也就避免了DNS劫持問題。當然采幌,域名和IP的對應要通常通過服務器下發(fā)保證獲取最近的資源節(jié)點(當然也可以采用一些收費的HTTPDNS服務)劲够,不過這樣一來操作卻不得不依賴于具體請求,而使用自定義NSURLProtocol的方式則可以徹底解決具體依賴問題休傍,不管是使用NSURLConnection征绎、NSURLSession、AFNetworking還是UIWebView(注意WKWebView有所不同)磨取,所有的替換操作都可以統(tǒng)一進行控制人柿。

下面的demo中自定義協(xié)議MyURLProtocol實現(xiàn)了將域名轉化成IP進行請求的過程:

import UIKit

class MyURLProtocol: URLProtocol{

// MARK: - URLProtocol虛方法實現(xiàn)

// 是否處理對應的請求

override class func canInit(with request: URLRequest) -> Bool {

if URLProtocol.property(forKey: MyURLProtocol.PropertyKey.tagKey, in: request) != nil {

return false

}

return true

}

// 返回請求,在此方法中可以修改請求

override class func canonicalRequest(for request: URLRequest) -> URLRequest {

var newRequest = request

// 例如這里域名為指定ip忙厌,實際開發(fā)中應該從服務器下方domain list

let originHost = request.url?.host

if "baidu.com" == originHost {

let originURL = request.url?.absoluteString

let newURL = originURL?.replacingOccurrences(of: originHost!, with: "61.135.169.121")

newRequest.url = URL(string: newURL!)

}

return newRequest

}

// 開始加載

override func startLoading() {

guard let newRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else { return}

URLProtocol.setProperty(true, forKey: MyURLProtocol.PropertyKey.tagKey, in: newRequest)

let sessionConfig = URLSessionConfiguration.default

let urlSession = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)

self.dataTask = urlSession.dataTask(with: self.request)

self.dataTask?.resume()

}

// 停止加載

override func stopLoading() {

self.dataTask?.cancel()

self.dataTask ? ? ? = nil

self.receivedData ? = nil

self.urlResponse ? ?= nil

}

// 判斷兩個請求是否相等凫岖,相等則考慮使用緩存,此方法不是必須實現(xiàn)

override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool {

return super.requestIsCacheEquivalent(a, to: b)

}

// MARK: - 私有屬性

private struct MyURLProtocolKey {

var tagKey = "MyURLProtocolTagKey"

}

fileprivate var dataTask: URLSessionDataTask?

fileprivate var urlResponse: URLResponse?

fileprivate var receivedData: NSMutableData?

}

extension MyURLProtocol {

struct PropertyKey{

static var tagKey = "MyURLProtocolTagKey"

}

}

// 注意實際開發(fā)中應該盡可能處理所有self.client?.urlProtocol回傳方法逢净,以免客戶端有些方法無法做出響應

extension MyURLProtocol:URLSessionTaskDelegate,URLSessionDataDelegate {

// MARK: - URLSessionDataDelegate方法

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,

didReceive response: URLResponse, completionHandler: @escaping

(URLSession.ResponseDisposition) -> Void) {

self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)

self.urlResponse = response

self.receivedData = NSMutableData()

completionHandler(.allow)

}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {

self.client?.urlProtocol(self, didLoad: data as Data)

self.receivedData?.append(data as Data)

}

// URLSessionTaskDelegate

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {

if error != nil {

self.client?.urlProtocol(self, didFailWithError: error!)

} else {

//saveCachedResponse()

self.client?.urlProtocolDidFinishLoading(self)

}

}

}

注意使用URLSession進行網(wǎng)絡請求時如果使用的不是默認會話(URLSession.shared)則需要在URLSessionConfiguration中指定protocolClasses自定義URLProtocol才能進行處理(即使使用URLProtocol.registerClass進行注冊)哥放,URLSession.shared默認則可以響應已注冊URLProtocol。

在MyURLProtocol的startLoading方法內同樣發(fā)起了URL請求爹土,如果此時使用了URLSession.shared進行網(wǎng)絡請求則同樣會造成MyURLProtocol調用,如此會引起循環(huán)調用胀茵∩缏叮考慮到startLoading方法能可能是NSURLConnnection實現(xiàn),安全起見在MyURLProtocol內部使用URLProtocol.setProperty(true, forKey: MyCacheURLProtocolTagKey, in: newRequest)來標記一個請求琼娘,調用前使用URLProtocol.property(forKey: MyCacheURLProtocolTagKey, in: request)判斷當前請求是否已經標記峭弟,如果已經標記則視為同一請求赁濒,MyURLProtocol則不再處理,從而避免同一個請求循環(huán)調用孟害。

如果你的網(wǎng)絡請求使用的NSURLConnection,上面的代碼需要做相應修改挪拟,但相信現(xiàn)在NSURLConnection使用應該越來越少了挨务,很多第三方網(wǎng)絡庫也不支持了。

NSURLProtocol緩存

其實無論是NSURLConnection玉组、NSURLSession還是UIWebView谎柄、WKWebView默認都是有緩存設計的(使用NSURLCache,后面會著重介紹)惯雳,不過這要配合服務器端response header使用朝巫,對于有緩存的頁面(或者API接口),當緩存過期后石景,默認情況下(NSURLRequestUseProtocolCachePolicy)遇到同一個請求則通常會發(fā)出一個header中包含If-Modified-Since的請求到服務器端驗證劈猿,如果內容沒有過期則返回一個不含有body的響應(Response code為304),客戶端使用緩存數(shù)據(jù)潮孽,否則重新返回新的數(shù)據(jù)揪荣。

由于WKWebView默認有幾十秒的緩存時間,在第一次緩存響應后過一段時間才會進行緩存請求檢查(緩存過期后才會發(fā)送包含If-Modified-Since的請求檢查)往史。但是這并不是說自己設計緩存就完全沒有必要仗颈,第一它做不到完全的離線后閱讀(盡管在一定時間內不需要檢查,但是過一段時間還是需要聯(lián)網(wǎng)檢查的)椎例,第二無法做到緩存細節(jié)的控制挨决。

下面簡單利用NSURLProtocol來實現(xiàn)WKWebView的離線緩存功能,不過需要注意的是WKWebView默認僅僅調用NSURLProtocol的canInitWithRequest:方法订歪,如果要真正利用NSURLProtocol進行緩存還必須使用WKBrowsingContextController的registerSchemeForCustomProtocol進行注冊脖祈,不過這是個私有對象,需要使用黑魔法陌粹。下面的demo中簡單實現(xiàn)了WKWebView的離線緩存功能撒犀,有了它之后遇到訪問過的資源即使沒有網(wǎng)絡也同樣可以訪問。當然掏秩,示例主要用以說明緩存的原理或舞,實際開發(fā)中還有很多問題需要思考,比如說緩存過期機制蒙幻、磁盤緩存保存方式等等映凳。

import UIKit

class MyCacheURLProtocol: URLProtocol{

// MARK: - URLProtocol虛方法實現(xiàn)

// 是否處理對應的請求

override class func canInit(with request: URLRequest) -> Bool {

if URLProtocol.property(forKey: MyCacheURLProtocol.PropertyKey.tagKey, in: request) != nil {

return false

}

return true

}

// 返回請求,在此方法中可以修改請求

override class func canonicalRequest(for request: URLRequest) -> URLRequest {

return request

}

// 開始加載

override func startLoading() {

func sendRequest() {

guard let newRequest = (request as NSURLRequest).mutableCopy() as? NSMutableURLRequest else { return}

URLProtocol.setProperty(true, forKey: MyCacheURLProtocol.PropertyKey.tagKey, in: newRequest)

let sessionConfig = URLSessionConfiguration.default

let urlSession = URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil)

self.dataTask = urlSession.dataTask(with: self.request)

self.dataTask?.resume()

}

if let cacheResponse = self.getResponse() {

self.client?.urlProtocol(self, didReceive: cacheResponse.response, cacheStoragePolicy: .notAllowed)

self.client?.urlProtocol(self, didLoad: cacheResponse.data)

self.client?.urlProtocolDidFinishLoading(self)

} else {

sendRequest()

}

}

// 停止加載

override func stopLoading() {

self.dataTask?.cancel()

self.dataTask ? ? ? = nil

self.receivedData ? = nil

self.urlResponse ? ?= nil

}

// 判斷兩個請求是否相等邮破,相等則考慮使用緩存诈豌,此方法不是必須實現(xiàn)

override class func requestIsCacheEquivalent(_ a: URLRequest, to b: URLRequest) -> Bool {

return super.requestIsCacheEquivalent(a, to: b)

}

// MARK: - 私有方法

fileprivate func saveResponse(_ response:URLResponse,_ data:Data) {

if let key = self.request.url?.absoluteString {

let tempDic = NSTemporaryDirectory() as NSString

let filePath = tempDic.appendingPathComponent(key.md5())

let cacheResponse = CachedURLResponse(response: response,

data: data, userInfo: nil, storagePolicy:

URLCache.StoragePolicy.notAllowed)

NSKeyedArchiver.archiveRootObject(cacheResponse, toFile: filePath)

}

}

fileprivate func getResponse() -> CachedURLResponse? {

if let key = self.request.url?.absoluteString {

let tempDic = NSTemporaryDirectory() as NSString

let filePath = tempDic.appendingPathComponent(key.md5())

if FileManager.default.fileExists(atPath: filePath) {

return NSKeyedUnarchiver.unarchiveObject(withFile: filePath) as? CachedURLResponse

}

return nil

}

return nil

}

// MARK: - 私有屬性

fileprivate var dataTask: URLSessionDataTask?

fileprivate var urlResponse: URLResponse?

fileprivate var receivedData: NSMutableData?

}

extension MyCacheURLProtocol {

struct PropertyKey{

static var tagKey = "MyURLProtocolTagKey"

}

}

// 注意實際開發(fā)中應該盡可能處理所有self.client?.urlProtocol回傳方法仆救,以免客戶端有些方法無法做出響應

extension MyCacheURLProtocol:URLSessionTaskDelegate,URLSessionDataDelegate {

// MARK: - URLSessionDataDelegate方法

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask,

didReceive response: URLResponse, completionHandler: @escaping

(URLSession.ResponseDisposition) -> Void) {

self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)

self.urlResponse = response

self.receivedData = NSMutableData()

completionHandler(.allow)

}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {

self.client?.urlProtocol(self, didLoad: data as Data)

self.receivedData?.append(data as Data)

}

// URLSessionTaskDelegate

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {

if error != nil {

self.client?.urlProtocol(self, didFailWithError: error!)

} else {

self.client?.urlProtocolDidFinishLoading(self)

//save cache

if self.urlResponse != nil && self.receivedData != nil {

self.saveResponse(self.urlResponse!, self.receivedData?.copy() as! Data)

}

}

}

}

NSURLCache

事實上無論是NSURLConnection、URLSession還是UIWebView矫渔、WKWebView默認都是包含緩存的(注意WKWebView的緩存配置是從iOS

9.0開始提供的彤蔽,但是其實iOS

8.0中也同樣包含緩存設計,只是沒有提供緩存配置接口)庙洼。對于多數(shù)開發(fā)者而言緩存設計考慮更多的是磁盤緩存(如果需要做內存緩存建議使用NSCache顿痪,提供了緩存過高自動移除功能

or

YYCache),而磁盤緩存設計大致可以分為API訪問返回的JSON緩存(通過NSURLConnection或者NSURLSession請求標準的JSON數(shù)據(jù))和客戶端web頁面緩存(UIWebView油够、WKWebView)蚁袭。

NSURLConnection和UIWebView來說,默認都會使用NSURLCache石咬,通常在應用啟動中會進行NSURLCache配置揩悄,當然即使不進行配置也是有默認配置的。但二者并不是今天介紹的重點鬼悠,我們重點關注NSURLSession和WKWebView删性。對于NSURLSession而言默認仍然會使用全局的NSURLCache(可以在啟動時自己初始化,例如

URLCache.shared = URLCache(memoryCapacity: 5*1024*1024, diskCapacity: 20*1024*1024, diskPath: nil))

但是相比于默認NSURLConnection而言NSURLSession更加靈活焕窝,因為每個URLSessionConfiguration都可以指定獨立的URLCache,默認情況下是使用一個私有內存緩存镇匀,如果設置為nil則不再使用緩存。而且還可以通過URLSessionConfiguration的requestCachePolicy屬性指定緩存策略袜啃。

緩存策略CachePolicy

useProtocolCachePolicy:默認緩存策略汗侵,對于特定URL使用網(wǎng)絡協(xié)議中實現(xiàn)的緩存策略。

reloadIgnoringLocalCacheData(或者reloadIgnoringCacheData):不使用緩存群发,直接請求原始數(shù)據(jù)晰韵。

returnCacheDataElseLoad:無論緩存是否過期,有緩存則使用緩存熟妓,否則重新請求原始數(shù)據(jù)雪猪。

returnCacheDataDontLoad:無論緩存是否過期,有緩存則使用緩存起愈,否則視為失敗只恨,不會重新請求原始數(shù)據(jù)。

其實對于多數(shù)開發(fā)者而言抬虽,第二種根本不緩存官觅,其他兩種也存在著很大的使用風險,所以默認緩存策略才是我們最關心的阐污,它使用網(wǎng)絡協(xié)議中實現(xiàn)的緩存策略休涤,那我們就應該首先弄清網(wǎng)絡協(xié)議中的緩存策略是如何來控制的(注意:無論是NSURLConnection還是NSURLSession都支持多種協(xié)議,這里重點關注HTTP、HTTPS)功氨。

HTTP的請求和響應使用headers來進行元數(shù)據(jù)交換序苏,例如MIME、Encoding捷凄,當然也包括緩存執(zhí)行忱详,下面會著重介紹相關緩存配置。

請求頭信息 Request cache headers

If-Modified-Since:與響應頭Last-Modified相對應跺涤,其值為最后一次響應頭中的Last-Modified踱阿。

If-None-Match:與響應頭Etag相對應,其值為最后一次響應頭中的Etag钦铁。

響應頭信息 Response cache headers

Last-Modified:資源最近修改時間

Etag:(Entity tag縮寫)是請求資源的標識符,主要用于動態(tài)生成才漆、沒有Last-Modified值的資源牛曹。

Cache-Control:緩存控制,只有包含此設置可能使用默認緩存策略醇滥±璞龋可能包含如下選項:

max-age:緩存時間(單位:秒)。

public:可以被任何區(qū)緩存鸳玩,包括中間經過的代理服務器也可以緩存阅虫。通常不會被使用,因為 max-age已經表示此響應可以緩存不跟。

private:只能被當前客戶端緩存颓帝,中間代理無法進行緩存。

no-cache:必須與服務器端確認響應是否發(fā)生了變化窝革,如果沒有變化則可以使用緩存购城,否則使用新請求的響應。

no-store:禁止使用緩存

Vary:決定如何決定請求是否可以使用緩存虐译,通常用于緩存key唯一值確定因素瘪板,同一個資源不同的Vary設置會被作為兩個緩存資源(注意,NSURLCache會忽略Vary請求緩存)漆诽。

注意:Expires是HTTP

1.0標準緩存控制侮攀,不建議使用,請使用Cache-Control:max-age代替厢拭,類似的還有Pragma:no-cache和Cache-Control:no-cache兰英。此外,Request

cache headers中也是可以包含Cache-Control的供鸠,例如如果設置為no-cache則說明此次請求不要使用緩存數(shù)據(jù)作為響應箭昵。

默認緩存策略下當客戶端發(fā)起一個請求時首先會檢查本地是否包含緩存,如果有緩存則繼續(xù)檢查緩存是否過期(通過Cache-Control:max-age或者Expires)回季,如果沒有過期則直接使用緩存數(shù)據(jù)家制。如果緩存過期了正林,則發(fā)起一個請求給服務器端,此時服務器端對比資源Last-Modified或者Etags(二者都存在的情況下下如果有一個不同則認為緩存已過期)颤殴,如果不同則返回新數(shù)據(jù)觅廓,否則返回304

Not

Modified繼續(xù)使用緩存數(shù)據(jù)(客戶端可以再使用"max-age"秒緩存數(shù)據(jù))。在這個過程中可以發(fā)現(xiàn)涵但,客戶端發(fā)送不發(fā)送請求主要看max-age是否過期杈绸,而過期后是否繼續(xù)訪問則需要重新發(fā)起請求,服務器端根據(jù)情況通知客戶端是否可以繼續(xù)使用緩存(這個過程是必須請求的矮瘟,只是返回結果可能是200或者304)瞳脓。

清楚了默認網(wǎng)絡協(xié)議緩存相關的設置之后,要使用默認緩存就很簡單了澈侠,通常對于NSURLSession你不做任何設置劫侧,只要服務器端響應頭部加上Cache-Control:max-age:xxx就可以使用緩存了。下面Demo3中演示了如何使用使用NSURLSession通過max-age進行為期60s的緩存哨啃,運行會發(fā)現(xiàn)在第一次請求之后60s內不會進行再次請求烧栋,60s后才會發(fā)起第二次請求。

let config = URLSessionConfiguration.default

// urlCache默認使用私有內存緩存

// config.urlCache = URLCache(memoryCapacity: 5*1024*1024, diskCapacity: 20*1024*1024, diskPath: nil)

// config.requestCachePolicy = .useProtocolCachePolicy

let urlSession = URLSession(configuration: config)

if let url = URL(string: "http://myapi.applinzi.com/url-cache/default-cache.php") {

let dataTask = urlSession.dataTask(with: url, completionHandler: { (data, response, error) in

if let tempError = error {

debugPrint(tempError)

} else {

guard let tempData = data else { return }

let responseText = String(data: tempData, encoding: String.Encoding.utf8)

debugPrint(responseText ?? "no text")

}

})

dataTask.resume()

}

服務器端default-cache.php內容如下:


$time=time();

$interval=60;

header('Last-Modified: '.gmdate('r',$time));

header('Expires: '.gmdate('r',($time+$interval)));

header('Cache-Control: max-age='.$interval);

header('Content-type: text/json');

$arr = array('a'=>1,'b'=>2);

echo json_encode($arr);

?>

對應的請求和相應頭信息如下,服務器端設置緩存60s:

當然拳球,配合服務器端使用緩存是一種不錯的方案审姓,自然官方設計時也是希望盡可能使用默認緩存策略。但是有些時候服務器端出于其他原因考慮祝峻,或者說或客戶端需要自定義緩存策略時還是有必要進行手動緩存管理的魔吐。比如說如果服務器端根本沒有設置緩存過期時間或者服務器端根本無法獲知用戶何時清理緩存、何時使用緩存這些具體邏輯等都需要服務器端自行制定緩存策略莱找。有不少朋友選擇自建數(shù)據(jù)庫直接緩存JSON模型(通常是NSArray或者NSDictionary)或者緩存成歸檔文件等画畅,其實使用NSURLCache默認的緩存策略依然可行,只是需要使用相關的代理方法宋距、控制緩存邏輯:

對于NSURLConnnection而言可以通過

- (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse

進行二次緩存設置轴踱,如果此方法返回nil則不進行緩存,默認不實現(xiàn)這個代理則會走默認緩存策略谚赎。而URLSessionDataDelegate也有一個類似的方法是

func

urlSession(_ session: URLSession, dataTask: URLSessionDataTask,

willCacheResponse proposedResponse: CachedURLResponse,

completionHandler: @escaping (CachedURLResponse?) -> Swift.Void)

它的使用和NSURLConnection是類似的淫僻,不同的是

dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Swift.Void)

等一系列帶有completionHandler的方法并不會走代理方法,所以這種情況下

func

urlSession(_ session: URLSession, dataTask: URLSessionDataTask,

willCacheResponse proposedResponse: CachedURLResponse,

completionHandler: @escaping (CachedURLResponse?) -> Swift.Void)

也是無法使用的壶唤,使用時需要特別注意雳灵。

事實上無論URLSession走緩存相關的代理,還是通過completionHandler進行回調闸盔,默認都會使用NSURLCache進行緩存悯辙,無需做任何工作。例如Demo3中的示例2、3都都打印出了默認的緩存信息躲撰,不過如果服務器端不進行緩存設置的話(header中設置Cache-Control)针贬,默認情況下NSURLSession是不會使用緩存數(shù)據(jù)的。如果將緩存策略設置為優(yōu)先考慮緩存使用(例如使用:.returnCacheDataElseLoad)拢蛋,則可以看到下次請求不會再發(fā)送請求桦他,Demo3中的示例4演示了這一情況。不過一旦如此設置之后以后想要更新緩存就變得艱難了谆棱,因為只要不清空緩存或超過緩存限制快压,緩存數(shù)據(jù)就一直存在,而且在應用中隨時換切換緩存策略成本也并不低垃瞧。因此蔫劣,要合理利用系統(tǒng)默認緩存的出發(fā)點還是應該著眼在默認的基于網(wǎng)絡協(xié)議的緩存設置,因為使用這個緩存策略基本已經很完美了个从。

不過這樣一來緩存的控制邏輯就上升為解決緩存問題的重點脉幢,比如說一個API接口設計多數(shù)情況下可以緩存,但是一旦用戶修改了部分信息則希望及時更新使用最新數(shù)據(jù)信姓,但是緩存不過期服務器端即使很了解客戶端設計也無法做到強制更新緩存,因此客戶端就不得不自行控制緩存绸罗。那么能不能強制NSURLCache使用網(wǎng)絡協(xié)議緩存策略呢意推,其實也是可以的,對于服務器端沒有添加cache

headers控制的響應只需要添加上響應的緩存控制即可珊蟀。Demo3的示例5說明了這一點菊值。

import UIKit

class DemoViewController3: UIViewController {

override func viewDidLoad() {

super.viewDidLoad()

}

@IBAction func requestWithServerCache1() {

let config = URLSessionConfiguration.default

// urlCache默認使用私有內存緩存

// config.urlCache = URLCache(memoryCapacity: 5*1024*1024, diskCapacity: 20*1024*1024, diskPath: nil)

// config.requestCachePolicy = .useProtocolCachePolicy

let urlSession = URLSession(configuration: config)

if let url = URL(string: "http://myapi.applinzi.com/url-cache/default-cache.php") {

let dataTask = urlSession.dataTask(with: url, completionHandler: { (data, response, error) in

if let tempError = error {

debugPrint(tempError)

} else {

guard let tempData = data else { return }

let responseText = String(data: tempData, encoding: String.Encoding.utf8)

debugPrint(responseText ?? "no text")

}

})

dataTask.resume()

}

}

@IBAction func requestWithoutServerCache2() {

let config = URLSessionConfiguration.default

let urlSession = URLSession(configuration: config, delegate: self.delegate, delegateQueue: nil)

if let url = URL(string: "http://myapi.applinzi.com/url-cache/no-cache.php") {

let dataTask = urlSession.dataTask(with: url)

dataTask.resume()

}

}

@IBAction func requestWithoutServerCache3() {

let config = URLSessionConfiguration.default

let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)

if let url = URL(string: "http://myapi.applinzi.com/url-cache/no-cache.php") {

let urlRequest = URLRequest(url: url)

let dataTask = urlSession.dataTask(with: urlRequest, completionHandler: { (data, response, error) in

if let tempError = error {

debugPrint(tempError)

} else {

guard let tempData = data else { return }

let responseText = String(data: tempData, encoding: String.Encoding.utf8)

let cacheResponse = URLCache.shared.cachedResponse(for: urlRequest)

debugPrint(cacheResponse)

debugPrint(responseText ?? "no text")

}

})

dataTask.resume()

}

}

@IBAction func requestWithoutServerCache4() {

let config = URLSessionConfiguration.default

// 使用緩存數(shù)據(jù)

config.requestCachePolicy = .returnCacheDataDontLoad

let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)

if let url = URL(string: "http://myapi.applinzi.com/url-cache/no-cache.php") {

let urlRequest = URLRequest(url: url)

let dataTask = urlSession.dataTask(with: urlRequest, completionHandler: { (data, response, error) in

if let tempError = error {

debugPrint(tempError)

} else {

guard let tempData = data else { return }

let responseText = String(data: tempData, encoding: String.Encoding.utf8)

let cacheResponse = URLCache.shared.cachedResponse(for: urlRequest)

debugPrint(cacheResponse)

debugPrint(responseText ?? "no text")

}

})

dataTask.resume()

}

}

@IBAction func requestWithoutServerCache5() {

let config = URLSessionConfiguration.default

let urlSession = URLSession(configuration: config, delegate: self, delegateQueue: nil)

if let url = URL(string: "http://myapi.applinzi.com/url-cache/no-cache.php") {

let dataTask = urlSession.dataTask(with: url)

dataTask.resume()

}

}

private var delegate = ?DemoViewController3Delegate()

}

extension DemoViewController3:URLSessionDelegate, URLSessionDataDelegate {

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {

let responseText = String(data: data, encoding: String.Encoding.utf8)

debugPrint(responseText ?? "no text")

}

public func urlSession(_ session: URLSession, dataTask:

URLSessionDataTask, willCacheResponse proposedResponse:

CachedURLResponse, completionHandler: @escaping (CachedURLResponse?)

-> Swift.Void) {

if let httpResponse = proposedResponse.response as? HTTPURLResponse {

if httpResponse.allHeaderFields["Cache-Control"] == nil {

let newHeaders = (httpResponse.allHeaderFields as NSDictionary).mutableCopy() as? NSDictionary

newHeaders?.setValue("max-age=60", forKey: "Cache-Control")

let newResponse = HTTPURLResponse(url: httpResponse.url!,

statusCode: httpResponse.statusCode, httpVersion: "HTTP/1.1",

headerFields: newHeaders as? [String : String])

let newCacheResponse = CachedURLResponse(response: newResponse!, data: proposedResponse.data)

completionHandler(newCacheResponse)

return

}

}

completionHandler(proposedResponse)

}

}

// for requestWithoutServerCache2

class DemoViewController3Delegate:NSObject,URLSessionDelegate, URLSessionDataDelegate {

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {

let responseText = String(data: data, encoding: String.Encoding.utf8)

debugPrint(responseText ?? "no text")

}

public func urlSession(_ session: URLSession, dataTask:

URLSessionDataTask, willCacheResponse proposedResponse:

CachedURLResponse, completionHandler: @escaping (CachedURLResponse?)

-> Swift.Void) {

completionHandler(proposedResponse)

debugPrint(proposedResponse)

}

}

原文地址:iOS架構設計-URL緩存(上)?


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市育灸,隨后出現(xiàn)的幾起案子腻窒,更是在濱河造成了極大的恐慌,老刑警劉巖磅崭,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件儿子,死亡現(xiàn)場離奇詭異,居然都是意外死亡砸喻,警方通過查閱死者的電腦和手機柔逼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來割岛,“玉大人愉适,你說我怎么就攤上這事⊙⑵幔” “怎么了维咸?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我癌蓖,道長瞬哼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任费坊,我火速辦了婚禮倒槐,結果婚禮上,老公的妹妹穿的比我還像新娘附井。我一直安慰自己讨越,他們只是感情好,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布永毅。 她就那樣靜靜地躺著把跨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪沼死。 梳的紋絲不亂的頭發(fā)上着逐,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音意蛀,去河邊找鬼耸别。 笑死,一個胖子當著我的面吹牛县钥,可吹牛的內容都是我干的秀姐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼若贮,長吁一口氣:“原來是場噩夢啊……” “哼省有!你這毒婦竟也來了?” 一聲冷哼從身側響起谴麦,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤蠢沿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后匾效,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舷蟀,經...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年面哼,在試婚紗的時候發(fā)現(xiàn)自己被綠了雪侥。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡精绎,死狀恐怖速缨,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情代乃,我是刑警寧澤旬牲,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布仿粹,位于F島的核電站,受9級特大地震影響原茅,放射性物質發(fā)生泄漏吭历。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一擂橘、第九天 我趴在偏房一處隱蔽的房頂上張望晌区。 院中可真熱鬧,春花似錦通贞、人聲如沸朗若。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哭懈。三九已至,卻和暖如春茎用,著一層夾襖步出監(jiān)牢的瞬間遣总,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工轨功, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留旭斥,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓古涧,卻偏偏與公主長得像垂券,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蒿褂,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內容