An overview of

Caching design should be a consideration for every client program development. If the same function needs to be accessed multiple times, each access will degrade the user experience. However, there seems to be no universal solution for how to handle the client cache. Most developers choose to create their own database and cache the JSON (or Model) requested by the server directly, and then query the database for the next request to check whether the cache exists. In fact, iOS System itself also provides a set of caching mechanism. This paper will briefly introduce how to use the System’s own cache design to achieve a set of caching mechanism to smooth and extend the cache processing of the worm client by combining with URL Loading System.

URL Loading System

URL Loading System is a collection of classes and protocols. The iOS System uses URL Loading System to interact with the server. Urls, as the core, enable easy interaction between app and resources. To enhance urls, Foundation provides a rich collection of classes that allow you to load resources by address, upload resources to the server, manage cookies, control response caching (which is also the focus of this article), handle certificates and authentication, extend user protocols, and more. Therefore, it is necessary to familiarize yourself with URL Loading System before understanding URL caching. The following is the relationship of this series of collections:

NSURLProtocol

URL Loading System supports HTTP, HTTPS, FTP, File, and data by default, but it also supports registering its own classes to support more application-layer network protocols. Specifically, NSURLProtocl can fulfill the following requirements (including but not limited to) :

  • Redirect network requests (or convert domain names, block them, etc., e.g. Netfox)
  • Ignore some requests and use locally cached data
  • Return result of a custom network request (e.g. GYHttpMocking)
  • Perform global network configuration

NSURLProtocol is like a man-in-the-middle design, providing the details of the network to the developer but leaking them out in an elegant way. NSURLProtocol is defined more like a URL protocol, although it inherits from NSObject but cannot be used directly. To use it, you need to define a protocol that inherits from NSURLProtocol and register it at app startup.

Resolving DNS hijacking

With the development of the Internet, operator hijacking has been mentioned gradually in recent years, including HTTP hijacking and DNS hijacking. For HTTP hijacking, it is more about tampering with the network response by adding some scripted ads and so on, which can be solved by encrypting the request interaction content using HTTPS; DNS hijacking is even more egregious, achieving content tampering by redirecting requests to an unexpected IP address during DNS resolution.

A common solution to DNS hijacking is to replace the URL with an IP address. In this way, access content does not go through the carrier’s Local DNS server to the specified server, thus avoiding DNS hijacking. Of course, domain names and IP addresses are usually matched by the server that guarantees access to the nearest resource node (although some charging HTTPDNS services can also be used), but this operation has to rely on the specific request. The custom NSURLProtocol can solve the specific dependency problem completely. Whether using NSURLConnection, NSURLSession, or UIWebView(WKWebView is different), all replacement operations can be controlled uniformly.

MyURLProtocol converts domain names into IP addresses for requests.

import UIKit class MyURLProtocol: URLProtocol{ // MARK: Override class func canInit(with request: override func canInit(with request: URLRequest) -> Bool { if URLProtocol.property(forKey: MyURLProtocol.PropertyKey.tagKey, in: request) ! Override class func canonicalRequest(for request: URLRequest) -> URLRequest {var newRequest = request 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. 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: 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 attributes 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" } } // Note that actual development should handle all self.client?. UrlProtocol postback methods as much as possible, In order to avoid the client some methods unable to respond to the 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) } } }Copy the code

ProtocolClasses must be specified in URLSessionConfiguration when network requests are made using URLSession (urlsession.shared). This allows custom URLProtocol to handle. The MyURLProtocol startLoading method also initiates a URL request. If a network request is made using urlsession. shared, MyURLProtocol will be called, which will cause a circular call. Since the startLoading method can be implemented by NSURLConnnection, it is safe to use urlProtocol. setProperty(true, forKey:) inside MyURLProtocol. MyCacheURLProtocolTagKey, in: newRequest) to mark a request, before invoking use URLProtocol. Property (forKey: MyCacheURLProtocolTagKey, in: Request) determines whether the current request is marked. If so, the same request will not be processed, thus avoiding the same request loop.

NSURLProtocol cache

NSURLConnection, NSURLSession, UIWebView, WKWebView all have cache design by default (using NSURLCache), but this needs to be used in conjunction with the server-side response header. For a page with a cache (or apis), after the cache expiration, by default (NSURLRequestUseProtocolCachePolicy) have the same request will usually send a header contains the If – Modified – Since requests to server-side validation, If the content has not expired, a Response without the body is returned (Response code 304). The client uses cached data, otherwise it returns new data.

Since WKWebView is cached for a period of time by default, cache request checks are not performed until some time after the first cached response (request checks containing if-Modified-since are sent after the cache expires). It doesn’t do full offline reading (though it doesn’t need to be checked for a while), and it doesn’t have control over cache details.

Note that WKWebView only calls NSURLProtocol’s canInitWithRequest: method by default. If you want to really take NSURLProtocol cache must also use WKBrowsingContextController registerSchemeForCustomProtocol register, but it is a private object, requires dynamic Settings. The following demo has simply implemented the offline cache function of WKWebView, so that the resources visited can also be accessed even if there is no network (of course, the example is mainly used to illustrate the principle of cache, and there are many problems to think about in the actual development, such as cache expiration mechanism, disk cache preservation method, etc.).

import UIKit class MyCacheURLProtocol: 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: newRequest as URLRequest) 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) } 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 } fileprivate var dataTask: URLSessionDataTask? fileprivate var urlResponse: URLResponse? fileprivate var receivedData: NSMutableData? } extension MyCacheURLProtocol { struct PropertyKey{ static var tagKey = "MyURLProtocolTagKey" } } extension MyCacheURLProtocol:URLSessionTaskDelegate,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) } func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { if error ! = nil { self.client? .urlProtocol(self, didFailWithError: error!) } else { self.client? .urlProtocolDidFinishLoading(self) if self.urlResponse ! = nil && self.receivedData ! = nil { self.saveResponse(self.urlResponse! , self.receivedData? .copy() as! Data) } } } }Copy the code

NSURLCache

In fact, NSURLConnection, URLSession, UIWebView, and WKWebView all use caching by default. (Note that WKWebView’s caching configuration was provided in iOS 9.0, but iOS 8.0 also includes caching. No cache configuration interface is provided. NSURLConnection, NSURLSession, and UIWebView all use NSURLCache by default, and all data requested by them will be processed by NSURLCache. NSURLCache not only provides memory and disk caching methods, but also has a complete cache policy to configure. For example, using NRURLSession for network requests, you can specify a separate URLCache using URLSessionConfiguration (if set to nil, no cache cache policy is used). Specify a specific cache policy using the requestCachePolicy attribute of URLSessionConfiguration.

CachePolicy CachePolicy

  • UseProtocolCachePolicy: Indicates the default cache policy. The cache policy implemented in network protocols is used for a specific URL.
  • ReloadIgnoringLocalCacheData (or reloadIgnoringCacheData) : do not use the cache, directly request the raw data.
  • ReturnCacheDataElseLoad: Regardless of whether the cache is expired, use the cache if there is a cache, otherwise re-request the original data.
  • ReturnCacheDataDontLoad: Regardless of whether the cache is expired, the cache is used if there is a cache. Otherwise, the cache is regarded as a failure and the original data will not be re-requested.

For most developers, it’s the default cache policy that matters most, so it’s important to understand how HTTP requests and responses use HEADERS for metadata exchange (both NSURLConnection and NSURLSession support multiple protocols, The focus here is on HTTP and HTTPS.

Request Cache headers

  • If-modified-since: corresponds to last-modified in the response header, and the value is last-modified in the Last response header.
  • If-none-match: corresponds to the Etag in the response header. The value is the Etag in the last response header.

Response Cache headers

  • Last-modified: indicates the Last modification time of the resource
  • Etag (Entity Tag abbreviation) is the identifier of the requested resource. It is mainly used for dynamically generated resources that do not have a last-Modified value.
  • Cache-control: Cache Control. Only if this setting is included may the default Cache policy be used. The options are as follows: max-age: indicates the cache time (unit: second). Public: can be cached by any partition, including proxy servers. Is usually not used because max-age itself already indicates that the response can be cached. Private: can only be cached by the current client. No-cache: You must check with the server side whether the response has changed. If not, you can use the cache. Otherwise, the response of the new request is used. No-store: disables the cache
  • Vary: Determines whether a request can use the cache, usually as a determinant of whether the cache key is valid. Different Vary Settings for the same resource are treated as two cache resources (note: NSURLCache ignores the Vary request cache).

Note: Expires is a standard HTTP 1.0 Cache Control and is not recommended. Use cache-control :max-age instead, similar to Pragma:no-cache and cache-control :no-cache. The Request cache headers can also contain cache-control. For example, if the Request cache headers is set to no-cache, the Request does not use cached data as a response.

By default, when a client makes a request, it checks whether the Cache is locally contained, if it is, it checks whether the Cache is expired (cache-control :max-age or Expires), and if it is not, it uses the cached data directly. If the cache is expired, a request is sent to the server, which compares last-Modified or Etags to the resource and returns new data if both are different. Otherwise return 304 Not Modified to continue using cached data (clients can continue to cache data using “max-age” seconds). In this process, whether the client sends a request or not depends on whether max-age has expired, and whether to continue to use the cache after the expiration, the client needs to initiate a request again, and the server informs the client whether to continue to use the cache according to the situation (the return result may be 200 or 304).

It is easy to use the default Cache once you know the Settings for the default network protocol Cache. Usually you do nothing about the NSURLSession, just add cache-control :max-age: XXX to the server response header to use the Cache. The following Demo demonstrates how to use NSURLSession to cache for 60 seconds through max-age. When running, it will find that no request will be made for 60 seconds after the first request, and the second request will be made after 60 seconds.

let config = URLSessionConfiguration.default  
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()
        }
Copy the code

Default-cache.php on the server is as follows:

<? 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); ? >Copy the code

The corresponding request and response headers are as follows (the server is set to cache 60s) :

Of course, it’s a good idea to use caching on the server side, and of course you want to use the default caching policy as much as possible in official design. However, manual cache management is often necessary on the server side for other reasons, or when the client needs to customize the cache policy. For example, if the server does not set the cache expiration time at all, or the server does not know the specific logic of when the user cleans the cache and when to use the cache, the client needs to make its own cache policy.

For NSURLConnnection you can use – (NSCachedURLResponse *)connection:(NSURLConnection *)connection WillCacheResponse :(NSCachedURLResponse *)cachedResponse sets the secondary cache. If this method returns nil, it does not cache. By default, the proxy is not implemented and the default cache policy is followed. Func urlSession(_ session: urlSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> swift.void), the use is similar to NSURLConnection, except dataTask(with URL: url, completionHandler: @escaping (Data? , URLResponse? , Error?) Func urlSession(_ session: urlSession, dataTask: func urlSession(_ session: urlSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse, completionHandler: @escaping (CachedURLResponse?) -> swift.void) is also unusable.

NSURLCache is used by default, whether the URLSession goes through a cache-specific proxy or is called back through the completionHandler. For example, examples 2 and 3 in Demo3 print the default Cache information, but NSURLSession does not use cached data by default if the server does not set caching (cache-control is set in the header). If you set the cache policy to give priority to cache use (for example, using:.returnCacheDataElseload), you can see that the request will not be sent the next time it is requested, as illustrated by Example 4 in Demo3. Once this is set up, however, it becomes difficult to update the cache because the cached data is always there until the cache is empty or the cache limit is exceeded, and it is not cheap to switch the cache policy from one application to another. Therefore, the starting point for proper use of the system’s default cache should be the default network protocol-based cache Settings.

But the cache control logic is rising as the key to solve the problem of cache, such as an API interface design in most cases can be cached, but once the user has modified the part information wants to update with the latest data, but the cache is not expired server-side even know what the client design cannot be forced to update cache, So the client has to control the cache itself. NSURLCache can be forced to use the network protocol cache policy. If the server does not add the cache headers control, it only needs to add the corresponding cache control. Example 5 of Demo3 illustrates this.

import UIKit // demo3 class DemoViewController3: UIViewController { override func viewDidLoad() { super.viewDidLoad() } @IBAction func requestWithServerCache1() { let config = URLSessionConfiguration.default 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) } } 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) } }Copy the code

Cache design

From the previous analysis of URL Loading System, we can see that NSURLProtocol or NSURLCache can be used for client caching, but NSURLProtocol is mostly used for interception processing. Choosing URLSession with NSURLCache gives more control over the interface caller, and NSURLCache has a Cache by default, so we only need to operate on the Cache headers that respond to the Cache response, so the latter is preferred. Since The Touworm client uses Alamofire as the network library, the following is a relatively simple caching scheme combined with Alamofire.

In addition, Alamofire does have a callback port for each URLSessionDataDelegate method. But to see the source code found the entrance dataTaskWillCacheResponse not foreign development, And if in SessionDelegate directly back to transfer into mouth on dataTaskWillCacheResponseWithCompletion callback and unable to control cache each request. Of course, if you could extend a DataTaskDelegate object along this line to leak cache entries, However, the URLSessionDataDelegate must be implemented, and the caching proxy of Swizzle NSURLSession (or inheriting the SessionDelegate delegate switch proxy) should be implemented in the proxy according to the different NSURLDataTask caching. The whole process is not very caller-friendly.

Another approach is to get the cached CachedURLResponse after the Response request ends and modify it (in fact, if the same NSURLRequest is stored in the cache by default, the original cache is updated). NSURLCache itself has an in-memory cache and this process is not too time-consuming. The most important thing in this scenario is to ensure that the response has been processed, so we use the Alamofire chain call to response(Queue: queue, responseSerializer: ResponseSerializer, completionHandler: The completionHandler rerequests the responseSerializer to ensure that the callback is timed in a timely manner. The main code snippet is as follows:

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) } } } }Copy the code

NSURLCache provides a remove method for cache cleanup, but the call to cache cleanup does not immediately take effect. For details, see NSURLCache does not clear stored responses in iOS8. Therefore, Cache expiration Control is implemented with the help of cache-control mentioned above. On the one hand, Cache can be cleaned quickly, and on the other hand, Cache Control can be more precise.

AlamofireURLCache

In order to better cooperate with Alamofire, this code is put on Github in the form of AlamofireURLCache class library. All interface apis should be consistent with the original interface as far as possible, so as to facilitate the secondary encapsulation of Alamofire. In addition, the code also provides manual cache cleaning, automatic cache cleaning after errors, overwriting server side cache configuration and other functions.

AlamofireURLCache adds the refreshCache parameter to the request method for cache refresh. If the refreshCache parameter is set to false or not provided, the cache will not be refreshed and the request will be initiated again only after the last cached data expires.

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)Copy the code

Setting headers on the server is not always optimal. In some cases, the client must control the cache policy by itself. You can use the ignoreServer parameter of AlamofireURLCache to ignore the server configuration and use the maxAge parameter to control the cache duration.

Alamofire.request("https://myapi.applinzi.com/url-cache/default-cache.php",refreshCache:false).responseJSON(completionHa ndler: { 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)Copy the code

In addition, in some cases, it is not necessary to refresh the cache but to clear the cache so that the latest data can be used next time. This can be done by using the cache cleaning API provided by AlamofireURLCache. However, in the case of request error or serialization error, if the cache(maxAge) method is called for caching, the wrong cache data will be used in the next request, and the developer needs to call API to clear the cache according to the return situation. Therefore, the autoClearCache parameter is provided in AlamofireURLCache to handle this situation automatically.

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)Copy the code

The original wiki address

Wiki.bytedance.net/pages/viewp…