iOS NSCache & NSURLCache 機制原理探究 (二)

上篇文章 咱們已經針對 NSCache 的底層瞭解了其具體的實現機制和淘汰策略 . 因爲文章太長 , 不利於閱讀 . 那麼這篇文章 , 咱們就 NSURLCache 以及 SDWebImage 中的緩存處理機制進行探究講解.nginx

目錄我就繼續上篇文章的來了 , 以便比較閱讀.算法

2. NSURLCache

2.1 介紹

先把官方文檔奉上 NSURLCache數據庫

首先咱們都知道 , 使用 NSURLCache 進行請求數據的緩存時 , 同時自己默認也會有緩存的處理. 那麼咱們須要作什麼 ? 原生默認作了什麼 ?瀏覽器

The NSURLCache class implements the caching of responses to URL load requests by mapping NSURLRequest objects to NSCachedURLResponse objects. It provides a composite in-memory and on-disk cache, and lets you manipulate the sizes of both the in-memory and on-disk portions. You can also control the path where cache data is stored persistently.緩存

啥意思呢 ? 重點就是 它提供了磁盤緩存和內存緩存 , 並可讓用戶來指定緩存的磁盤和內存的大小 . 至於其餘的何時來清楚這些磁盤或者內存中的緩存內容咱們無需關心.bash

而且它提供給咱們集中不一樣的策略以知足靈活多變的需求.服務器

2.1.1 緩存策略

當使用 NSURLSession 時 , 咱們能夠直接經過 NSMutableURLRequest 來指定 NSURLRequestCachePolicy .網絡

typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
    NSURLRequestUseProtocolCachePolicy = 0, //默認策略

    NSURLRequestReloadIgnoringLocalCacheData = 1, //忽略緩存,必須從遠程地址下載;
    NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented 未實現
    NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,

    NSURLRequestReturnCacheDataElseLoad = 2, // 不管緩存是否過時 , 有就使用緩存 , 沒有就請求數據.
    NSURLRequestReturnCacheDataDontLoad = 3,// 不管緩存是否過時 , 有就使用緩存 , 沒有就請求失敗.

    NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented 未實現
};
複製代碼

在瞭解 NSURLCache 以前 , 先來看看默認的 HTTP 緩存機制. 也就是 NSURLRequestCachePolicy 的默認策略.session

2.1.2 HTTP 緩存策略

先來個默認使用 HTTP 緩存策略的例子.app

- (void)example_1{
    NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/50x50.jpg"];
    //默認使用 HTTP緩存策略來進行緩存
     NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];

    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error) {
            NSLog(@"error warning : %@",error);
        }else{
            //從緩存當中讀取數據!
            NSData *tempData = data;
            NSString *responseStr = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding];
            NSLog(@"response:%@",response);
        }
    }] resume];
    
}
複製代碼

打印結果:

response:<NSHTTPURLResponse: 0x600002ff7380> { URL: http://via.placeholder.com/50x50.jpg } { Status Code: 200, Headers {
    "Accept-Ranges" =     (
        bytes
    );
    "Cache-Control" =     (
        "max-age=604800"
    );
    Connection =     (
        "keep-alive"
    );
    "Content-Length" =     (
        807
    );
    "Content-Type" =     (
        "image/jpeg"
    );
    Date =     (
        "Wed, 18 Sep 2019 06:32:38 GMT"
    );
    Etag =     (
        "\"5d5c8aae-327\""
    );
    Expires =     (
        "Wed, 25 Sep 2019 06:32:38 GMT"
    );
    "Last-Modified" =     (
        "Wed, 21 Aug 2019 00:05:02 GMT"
    );
    Server =     (
        "nginx/1.6.2"
    );
    "X-Cache" =     (
        L1
    );
} }
複製代碼

其實在 HTTP 中,控制緩存開關的字段有兩個:PragmaCache-Control . Pragma 是舊產物,已經逐步拋棄,有些網站爲了向下兼容還保留了這兩個字段. 在此就不介紹了.

Cache-Control

在請求中使用 Cache-Control 時,它可選的值有:

在響應中使用Cache-Control 時,它可選的值有:

緩存校驗

在緩存中,咱們須要一個機制來驗證緩存是否有效。好比服務器的資源更新了,客戶端須要及時刷新緩存;又或者客戶端的資源過了有效期,但服務器上的資源仍是舊的,此時並不須要從新發送。緩存校驗就是用來解決這些問題的,

HTTP 1.1 中,咱們主要關注下 Last-Modifiedetag 這兩個字段。

Last-Modified

服務端在返回資源時,會將該資源的最後更改時間經過 Last-Modified 字段返回給客戶端。客戶端下次請求時經過 If-Modified-Since 或者 If-Unmodified-Since 帶上 Last-Modified ,服務端檢查該時間是否與服務器的最後修改時間一致:

  • 若是一致,則返回 304 狀態碼,不返回資源;
  • 若是不一致則返回 200 和修改後的資源,並帶上新的時間。

If-Modified-SinceIf-Unmodified-Since 的區別是:

If-Modified-Since:告訴服務器若是時間一致,返回狀態碼 304

If-Unmodified-Since:告訴服務器若是時間不一致,返回狀態碼 412

Etag

單純的以修改時間來判斷仍是有缺陷,好比文件的最後修改時間變了,但內容沒變。對於這樣的狀況,咱們可使用 Etag 來處理。

Etag 機制:

服務器經過某個算法對資源進行計算,取得一串值(相似於文件的 hash 值),以後將該值經過 Etag 返回給客戶端,客戶端下次請求時經過 If-None-MatchIf-Match 帶上該值,服務器對該值進行對比校驗:若是一致則不要返回資源。

If-None-MatchIf-Match 的區別是:

  • If-None-Match:告訴服務器若是一致,返回狀態碼 304,不一致則返回資源
  • If-Match:告訴服務器若是不一致,返回狀態碼 412

既生 Last-Modified 何生 Etag ?

你可能會以爲使用 Last-Modified 已經足以讓客戶端知道本地的緩存副本是否足夠新,爲何還須要 Etag(實體標識)呢?HTTP 1.1Etag 的出現主要是爲了解決幾個 Last-Modified 比較難解決的問題:

  1. Last-Modified 標註的最後修改只能精確到秒級,若是某些文件在 1 秒鐘之內,被修改屢次的話,它將不能準確標註文件的修改時間

  2. 若是某些文件會被按期生成,當有時內容並無任何變化,但 Last-Modified 卻改變了,致使文件無法使用緩存

  3. 有可能存在服務器沒有準確獲取文件修改時間,或者與代理服務器時間不一致等情形

Etag 是服務器自動生成或者由開發者生成的對應資源在服務器端的惟一標識符,可以更加準確的控制緩存。Last-ModifiedETag 是能夠一塊兒使用的,服務器會優先驗證 ETag,一致的狀況下,纔會繼續比對 Last-Modified,最後才決定是否返回 304

HTTP 緩存機制總結

緩存開關是: pragma , cache-control.

緩存校驗有:Expires , Last-Modified , etag.

從整個流程來看 , 他們以下圖: ( 圖片源自網絡 : 瀏覽器加載 HTTP 緩存機制 )

  • 第一次請求 :
  • 再次請求 :

2.1.3 HTTP 緩存內容查看

說了這麼多 , 咱們剛剛寫的案例到底緩存了什麼內容呢 . 加個斷點 獲取一下沙盒 , 而後咱們打開文件夾看下 :

  • 獲取路徑
  • 查看沙盒文件
  • 使用 DB 查看工具 , 我這裏用的是 DB Browser for SQLite
  • 選擇第一張表 cfurl_cache_blob_data 瀏覽數據.
  • 選擇第一條 response_object 右邊導出二進制 bin 文件 , 使用終端直接 cat 命令查看文件.

能夠看到 : 本次 HTTP 請求信息都被存儲到這個數據庫表中.

再繼續查看你會發現全部的信息都在各個表中存儲完畢.

2.1.4 使用其餘緩存策略

仍是以前的案例 , 咱們稍微修改一下:

- (void)example_1{
    NSURL *url = [NSURL URLWithString:@"http://via.placeholder.com/50x50.jpg"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
    //默認使用 HTTP緩存策略來進行緩存
// NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];\
    //比對服務,資源是否更新
    if (self.lastModified) {
        [request setValue:self.lastModified forHTTPHeaderField:@"If-Modified-Since"];
    }
// if (self.etag) {
// [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
// }
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (error) {
            NSLog(@"error warning : %@",error);
        }else{
            //從緩存當中讀取數據!
            NSData *tempData = data;
            NSString *responseStr = [[NSString alloc] initWithData:tempData encoding:NSUTF8StringEncoding];
            self.lastModified = [(NSHTTPURLResponse *)response allHeaderFields][@"Last-Modified"];
// self.etag = [(NSHTTPURLResponse *)response allHeaderFields][@"Etag"];
            NSLog(@"response:%@",response);
        }
    }] resume];
    
}

- (IBAction)reloadDataAction:(id)sender {
    [self example_1];
}
複製代碼

運行 , 第一次加載 , 返回 200 , 點擊 reload 再加載一次 . 打印以下:

這就是咱們剛纔所說的 lastModified 或者 etag 的用法.

3. SDWebImage 中 NSURLCache 與自身緩存機制處理

直接打開 SDWebImage 源碼 . 來到 SDWebImageDownloader.m 的 以下方法中.

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
    /*如下省略...*/
}
複製代碼

裏面有這麼一段值得注意的 :

NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;

也就是說 , 除非用戶指定了 optionsUseNSURLCache, 不然 SD 會默認忽略掉 NSURLCache .

這麼作的目的 上面也寫了註釋 , 防止屢次緩存 , 避免 SD 本身實現的緩存和 NSURLCache 屢次緩存形成資源浪費 .

一樣的 , 在 SDWebImageDownloaderOperation.m 裏, NSURLSessionDataDelegate 的代理方法 willCacheResponse 中 也能夠看出來:

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler {
    
    NSCachedURLResponse *cachedResponse = proposedResponse;

    if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
        // Prevents caching of responses
        cachedResponse = nil;
    }
    if (completionHandler) {
        completionHandler(cachedResponse);
    }
}
複製代碼

當設置 NSURLRequestReloadIgnoringLocalCacheData 策略 , 會忽略 NSURLCache 的緩存 .

3.1 SDWebImage中下載選項

typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
    SDWebImageDownloaderLowPriority = 1 << 0,
    SDWebImageDownloaderProgressiveDownload = 1 << 1,

    SDWebImageDownloaderUseNSURLCache = 1 << 2,

    SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,

    SDWebImageDownloaderContinueInBackground = 1 << 4,

    SDWebImageDownloaderHandleCookies = 1 << 5,

    SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,

    SDWebImageDownloaderHighPriority = 1 << 7,

    SDWebImageDownloaderScaleDownLargeImages = 1 << 8,
};
複製代碼

那咱們一樣來搜一下 SDWebImageDownloaderIgnoreCachedResponse 這個 options 時 , SD 作了哪些處理.

  • 第一個搜索結果 : SDWebImageDownloaderOperation.mstart 方法中有這麼一段:
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
    // Grab the cached data for later check
    NSCachedURLResponse *cachedResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:self.request];
    if (cachedResponse) {
        self.cachedData = cachedResponse.data;
    }
}
複製代碼

加載 NSURLCache 的磁盤緩存數據 , 以便下面作判斷用.

  • 第二個搜索結果 : SDWebImageDownloaderOperation.mdidCompleteWithError 代理方法中 有一段:
if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) {
    // call completion block with nil
    [self callCompletionBlocksWithImage:nil imageData:nil error:nil finished:YES];
}
複製代碼

這裏就直接返回了 nil , 也就是說 SDWebImageDownloaderIgnoreCachedResponse 這個 options 的機制就是當 image 是從 NSURLCache 獲取到的時候 , 它會返回 nil.

以上就是關於 NSURLCache 以及 SDWebImageHTTP 的緩存策略分析. 若有錯誤 , 歡迎指正 .

相關文章
相關標籤/搜索