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

概覽

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

Loading

System介紹一下如何利用系統自身緩存設計來實現一套緩存機制,使用這套緩存設計你無需自己編寫內存和磁盤存儲,無需自行檢查緩存過期策略就能輕松實現數據緩存。

URL Loading System

URL Loading System是類和協議的集合,使用URL

Loading System

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

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

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

NSURLProtocol

URL

Loading System默認支持http、https、ftp、file和data

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

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

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

忽略某些請求,使用本地緩存數據

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

進行網絡全局配置

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

解決DNS劫持

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

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

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

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

import UIKit

class MyURLProtocol: URLProtocol{

// MARK: - URLProtocol虛方法實現

// 是否處理對應的請求

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,實際開發中應該從服務器下方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

}

// 判斷兩個請求是否相等,相等則考慮使用緩存,此方法不是必須實現

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"

}

}

// 注意實際開發中應該盡可能處理所有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進行網絡請求時如果使用的不是默認會話(URLSession.shared)則需要在URLSessionConfiguration中指定protocolClasses自定義URLProtocol才能進行處理(即使使用URLProtocol.registerClass進行注冊),URLSession.shared默認則可以響應已注冊URLProtocol。

在MyURLProtocol的startLoading方法內同樣發起了URL請求,如果此時使用了URLSession.shared進行網絡請求則同樣會造成MyURLProtocol調用,如此會引起循環調用。考慮到startLoading方法能可能是NSURLConnnection實現,安全起見在MyURLProtocol內部使用URLProtocol.setProperty(true, forKey: MyCacheURLProtocolTagKey, in: newRequest)來標記一個請求,調用前使用URLProtocol.property(forKey: MyCacheURLProtocolTagKey, in: request)判斷當前請求是否已經標記,如果已經標記則視為同一請求,MyURLProtocol則不再處理,從而避免同一個請求循環調用。

如果你的網絡請求使用的NSURLConnection,上面的代碼需要做相應修改,但相信現在NSURLConnection使用應該越來越少了,很多第三方網絡庫也不支持了。

NSURLProtocol緩存

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

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

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

import UIKit

class MyCacheURLProtocol: URLProtocol{

// MARK: - URLProtocol虛方法實現

// 是否處理對應的請求

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

}

// 判斷兩個請求是否相等,相等則考慮使用緩存,此方法不是必須實現

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"

}

}

// 注意實際開發中應該盡可能處理所有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中也同樣包含緩存設計,只是沒有提供緩存配置接口)。對于多數開發者而言緩存設計考慮更多的是磁盤緩存(如果需要做內存緩存建議使用NSCache,提供了緩存過高自動移除功能

or

YYCache),而磁盤緩存設計大致可以分為API訪問返回的JSON緩存(通過NSURLConnection或者NSURLSession請求標準的JSON數據)和客戶端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使用網絡協議中實現的緩存策略。

reloadIgnoringLocalCacheData(或者reloadIgnoringCacheData):不使用緩存,直接請求原始數據。

returnCacheDataElseLoad:無論緩存是否過期,有緩存則使用緩存,否則重新請求原始數據。

returnCacheDataDontLoad:無論緩存是否過期,有緩存則使用緩存,否則視為失敗,不會重新請求原始數據。

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

HTTP的請求和響應使用headers來進行元數據交換,例如MIME、Encoding,當然也包括緩存執行,下面會著重介紹相關緩存配置。

請求頭信息 Request cache headers

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

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

響應頭信息 Response cache headers

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

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

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

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

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

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

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

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則說明此次請求不要使用緩存數據作為響應。

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

Not

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

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

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:

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

對于NSURLConnnection而言可以通過

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

進行二次緩存設置,如果此方法返回nil則不進行緩存,默認不實現這個代理則會走默認緩存策略。而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是不會使用緩存數據的。如果將緩存策略設置為優先考慮緩存使用(例如使用:.returnCacheDataElseLoad),則可以看到下次請求不會再發送請求,Demo3中的示例4演示了這一情況。不過一旦如此設置之后以后想要更新緩存就變得艱難了,因為只要不清空緩存或超過緩存限制,緩存數據就一直存在,而且在應用中隨時換切換緩存策略成本也并不低。因此,要合理利用系統默認緩存的出發點還是應該著眼在默認的基于網絡協議的緩存設置,因為使用這個緩存策略基本已經很完美了。

不過這樣一來緩存的控制邏輯就上升為解決緩存問題的重點,比如說一個API接口設計多數情況下可以緩存,但是一旦用戶修改了部分信息則希望及時更新使用最新數據,但是緩存不過期服務器端即使很了解客戶端設計也無法做到強制更新緩存,因此客戶端就不得不自行控制緩存。那么能不能強制NSURLCache使用網絡協議緩存策略呢,其實也是可以的,對于服務器端沒有添加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

// 使用緩存數據

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緩存(上)?


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容