淺談iOS 網絡緩存

最近公司項目安全自查, 就對現有項目詳細的檢查了一下, 其中就包括網絡請求緩存安全這一塊, 所以就查資料詳細的學習了一下這一方面的知識, 這里做個記錄也和想要了解這一塊知識的同學做個分享

初識緩存

緩存的目的的以空間換時間

緩存有不同的分類方法:

按照功能分: 1. 優化型緩存 2. 存儲型緩存
按照形式劃分: 1. 內存型緩存 2. 磁盤型緩存(iOS 5以后支持)

GET網絡請求緩存

POST請求不能被緩存,只有 GET 請求能被緩存。因為從數學的角度來講,GET 的結果是 冪等 的,就好像字典里的 key 與 value 就是冪等的,而 POST 不 冪等 。緩存的思路就是將查詢的參數組成的值作為 key ,對應結果作為value。從這個意義上說,一個文件的資源鏈接,也叫 GET 請求,下文也會這樣看待。

緩存只需要二步

第一個步驟:請使用 GET 請求。

第二個步驟:

如果你已經使用 了 GET 請求,iOS 系統 SDK 已經幫你做好了緩存。你需要的僅僅是設置下內存緩存大小、磁盤緩存大小、以及緩存路徑。甚至這兩行代碼不設置也是可以的,會有一個默認值。代碼如下:

NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:urlCache];

如何控制緩存的有效性

借助 ETag 或 Last-Modified 判斷緩存是否有效。

Last-Modified

Last-Modified 顧名思義,是資源最后修改的時間戳,往往與緩存時間進行對比來判斷緩存是否過期。

在瀏覽器第一次請求某一個URL時,服務器端的返回狀態會是200,內容是你請求的資源,同時有一個Last-Modified的屬性標記此文件在服務期端最后被修改的時間,格式類似這樣:

 Last-Modified: Fri, 12 May 2006 18:53:33 GMT

客戶端第二次請求此URL時,根據 HTTP 協議的規定,瀏覽器會向服務器傳送 If-Modified-Since 報頭,詢問該時間之后文件是否有被修改過:

If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT

如果服務器端的資源沒有變化,則自動返回 HTTP 304 (Not Changed.)狀態碼,內容為空,這樣就節省了傳輸數據量。當服務器端代碼發生改變或者重啟服務器時,則重新發出資源,返回和第一次請求時類似。從而保證不向客戶端重復發出資源,也保證當服務器有變化時,客戶端能夠得到最新的資源。

ETag

HTTP 協議規格說明定義ETag為“被請求變量的實體值” (參見 —— 章節 14.19)。 另一種說法是,ETag是一個可以與Web資源關聯的記號(token)。它是一個 hash 值,用作 Request 緩存請求頭,每一個資源文件都對應一個唯一的 ETag 值,
服務器單獨負責判斷記號是什么及其含義,并在HTTP響應頭中將其傳送到客戶端,以下是服務器端返回的格式:

 ETag: "50b1c1d4f775c61:df3"


    客戶端的查詢更新格式是這樣的:

    If-None-Match: W/"50b1c1d4f775c61:df3"

如果ETag沒改變,則返回狀態304然后不返回,這也和Last-Modified一樣。

ETag 是的功能與 Last-Modified 類似:服務端不會每次都會返回文件資源。客戶端每次向服務端發送上次服務器返回的 ETag 值,服務器會根據客戶端與服務端的 ETag 值是否相等,來決定是否返回 data,同時總是返回對應的 HTTP 狀態碼。客戶端通過 HTTP 狀態碼來決定是否使用緩存。比如:服務端與客戶端的 ETag 值相等,則 HTTP 狀態碼為 304,不返回 data。服務端文件一旦修改,服務端與客戶端的 ETag 值不等,并且狀態值會變為200,同時返回 data。

因為修改資源文件后該值會立即變更。這也決定了 ETag 在斷點下載時非常有用。
比如 AFNetworking 在進行斷點下載時,就是借助它來檢驗數據的

示例代碼:

/*!
 @brief 如果本地緩存資源為最新,則使用使用本地緩存。如果服務器已經更新或本地無緩存則從服務器請求資源。

 @details

 步驟:
 1. 請求是可變的,緩存策略要每次都從服務器加載
 2. 每次得到響應后,需要記錄住 etag
 3. 下次發送請求的同時,將etag一起發送給服務器(由服務器比較內容是否發生變化)

 @return 圖片資源
 */
- (void)getData:(GetDataCompletion)completion {
    NSURL *url = [NSURL URLWithString:kETagImageURL];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];

    // 發送 etag
    if (self.etag.length > 0) {
        [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
    }

    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {

        // NSLog(@"%@ %tu", response, data.length);dd
        // 類型轉換(如果將父類設置給子類,需要強制轉換)
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
        NSLog(@"statusCode == %@", @(httpResponse.statusCode));
        // 判斷響應的狀態碼是否是 304 Not Modified (更多狀態碼含義解釋: https://github.com/ChenYilong/iOSDevelopmentTips)
        if (httpResponse.statusCode == 304) {
            NSLog(@"加載本地緩存圖片");
            // 如果是,使用本地緩存
            // 根據請求獲取到`被緩存的響應`!
            NSCachedURLResponse *cacheResponse =  [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
            // 拿到緩存的數據
            data = cacheResponse.data;
        }

        // 獲取并且紀錄 etag,區分大小寫
        self.etag = httpResponse.allHeaderFields[@"Etag"];

        NSLog(@"etag值%@", self.etag);
        !completion ?: completion(data);
    }];
}

相應的 NSURLSession 搭配 ETag 代碼

 /*!
 @brief 如果本地緩存資源為最新,則使用使用本地緩存。如果服務器已經更新或本地無緩存則從服務器請求資源。

 @details

 步驟:
 1. 請求是可變的,緩存策略要每次都從服務器加載
 2. 每次得到響應后,需要記錄住 etag
 3. 下次發送請求的同時,將etag一起發送給服務器(由服務器比較內容是否發生變化)

 @return 圖片資源
 */
- (void)getData:(GetDataCompletion)completion {
    NSURL *url = [NSURL URLWithString:kETagImageURL];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];

    // 發送 etag
    if (self.etag.length > 0) {
        [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
    }

    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

        // NSLog(@"%@ %tu", response, data.length);
        // 類型轉換(如果將父類設置給子類,需要強制轉換)
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
        NSLog(@"statusCode == %@", @(httpResponse.statusCode));
        // 判斷響應的狀態碼是否是 304 Not Modified (更多狀態碼含義解釋: https://github.com/ChenYilong/iOSDevelopmentTips)
        if (httpResponse.statusCode == 304) {
            NSLog(@"加載本地緩存圖片");
            // 如果是,使用本地緩存
            // 根據請求獲取到`被緩存的響應`!
            NSCachedURLResponse *cacheResponse =  [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
            // 拿到緩存的數據
            data = cacheResponse.data;
        }

        // 獲取并且紀錄 etag,區分大小寫
        self.etag = httpResponse.allHeaderFields[@"Etag"];

        NSLog(@"%@", self.etag);
        dispatch_async(dispatch_get_main_queue(), ^{
            !completion ?: completion(data);
        });
    }] resume];
}

iOSNSURLRequest提供了7種緩存策略

NSURLRequestUseProtocolCachePolicy // 默認的緩存策略(取決于協議)

NSURLRequestReloadIgnoringLocalCacheData // 忽略緩存,重新請求

NSURLRequestReloadIgnoringLocalAndRemoteCacheData // 未實現

NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData // 忽略緩存,
重新請求

NSURLRequestReturnCacheDataElseLoad// 有緩存就用緩存,沒有緩存就重新請求

NSURLRequestReturnCacheDataDontLoad// 有緩存就用緩存,沒有緩存就不發請求,當做請求出錯處理(用于離線模式)

NSURLRequestReloadRevalidatingCacheData // 未實現

使用緩存策略的簡單示例

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 1.創建請求
    NSURL *url = [NSURL URLWithString:@"http://127.0.0.1:8080/YYServer/video"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    
    // 2.設置緩存策略(有緩存就用緩存,沒有緩存就重新請求)
    request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
    
    // 3.發送請求
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (data) {
            NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
            
            NSLog(@"%@", dict);
        }
    }];
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容