NSURLCache

NSURLCache為你的url請(qǐng)求提供了內(nèi)存以及磁盤上的綜合緩存機(jī)制。使用緩存可以減少向服務(wù)發(fā)送請(qǐng)求的次數(shù),同時(shí)提升了離線或低速網(wǎng)絡(luò)中的使用體驗(yàn),以及減輕了服務(wù)的壓力。

NSURLCache會(huì)自動(dòng)且透明的處理網(wǎng)絡(luò)緩存:當(dāng)一個(gè)請(qǐng)求完成下載來自服務(wù)器的響應(yīng),一個(gè)緩存的回應(yīng)將在本地保存。下次同一個(gè)請(qǐng)求再次發(fā)起時(shí),本地保存的回應(yīng)就會(huì)馬上返回,不需要連接服務(wù)器。

使用緩存,我門一般會(huì)在Appdelegate中設(shè)置,如下:

- (BOOL)application:(UIApplication *)application
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
  NSURLCache *URLCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024
                                                       diskCapacity:20 * 1024 * 1024
                                                           diskPath:nil];
  [NSURLCache setSharedURLCache:URLCache];
}

NSURLRequest有一個(gè)cachePolicy屬性,根據(jù)該屬性值來設(shè)置緩存行為:

  • NSURLRequestUseProtocolCachePolicy: 默認(rèn)行為(使用網(wǎng)絡(luò)協(xié)議中實(shí)現(xiàn)的緩存邏輯)
  • NSURLRequestReloadIgnoringLocalCacheData: 不使用緩存,每次都從網(wǎng)絡(luò)下載。
  • NSURLRequestReloadIgnoringLocalAndRemoteCacheData:不僅忽略本地緩存,同時(shí)也忽略代理服務(wù)器或其他中間介質(zhì)如:CDN等的緩存。
  • NSURLRequestReturnCacheDataDontLoad: 無論緩存是否過期,先使用本地緩存數(shù)據(jù)。如果緩存中沒有申請(qǐng)所對(duì)應(yīng)的數(shù)據(jù),那么從原始地址加載數(shù)據(jù)。
  • NSURLRequestReturnCacheRevalidatingCacheData:從原始地址確認(rèn)緩存數(shù)據(jù)的合法性后,緩存數(shù)據(jù)就可以使用,否則從原始地址加載。
    其中NSURLRequestReloadIgnoringLocalAndRemoteCacheData 和 NSURLRequestReloadRevalidatingCacheData 根本沒有實(shí)現(xiàn)。

最常用的緩存策略是默認(rèn)行為:NSURLRequestUseProtocolCachePolicy
它的緩存步驟是:
1,如果一個(gè)Request的NSCacheURLResponse不存在,就去請(qǐng)求網(wǎng)絡(luò)。
2,如果一個(gè)Request的NSCacheURLResponse存在,就去檢查response去決定是否需要重新獲取。檢查Resopne header的Cache-Control字段是否含有must-revalidated字段(http1.1)
3, 如果包含must-revalidated字段,就通過HEAD方法請(qǐng)求服務(wù)器,判斷Response頭是否有跟新,如果有則去獲取數(shù)據(jù),如果沒有則直接使用cache資源。
4,如果不包含must-revalidated字段,就查看Cache-Control是否包含其他字段,比如max-age等等是否過期,如果過期,同3一樣使用HEAD方法去檢查Response頭,是否為最新數(shù)據(jù),如果有則去請(qǐng)求服務(wù)器,反之取cache。如果沒有過期,直接取cache。

以下是證明設(shè)置NSURLRequestUseProtocolCachePolicy后直接使用本地緩存而不在請(qǐng)求網(wǎng)絡(luò)的demo:

- (void)demoGet{
    NSString *aburl = @"http://cdn-qn0.jianshu.io/assets/base-ded41764c207f7ff545c28c670922d25.js";
    NSURL *url = [NSURL URLWithString:aburl];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15.0];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        NSString *result = [[NSString alloc] initWithData:data  encoding:NSUTF8StringEncoding];
        NSLog(@"%@", result);
    }];
}

鏈接url是我從網(wǎng)絡(luò)上隨便找的一個(gè)鏈接,它的響應(yīng)頭Cache-Control字段的值為:31536000. 可以說是相當(dāng)大的。
當(dāng)?shù)谝淮握?qǐng)求到數(shù)據(jù)后,斷開網(wǎng)絡(luò),再次離線請(qǐng)求,依舊可以獲取到數(shù)據(jù),該數(shù)據(jù)來自Cache。降低了對(duì)網(wǎng)絡(luò)的依賴,減輕了服務(wù)的壓力,同時(shí)也提升了用戶體驗(yàn)。

有些情況下服務(wù)端api并沒有設(shè)置緩存頭:Cache-Control。但是我們又希望能夠自動(dòng)緩存一些數(shù)據(jù),則可以實(shí)現(xiàn)NSURLSessionDataDelegate 協(xié)議中的一個(gè)方法:

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

如下:

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler
{
    NSURLResponse *response = proposedResponse.response;
    NSHTTPURLResponse *HTTPResponse = (NSHTTPURLResponse*)response;
    NSDictionary *headers = HTTPResponse.allHeaderFields;
 
    NSCachedURLResponse *cachedResponse;
    if (headers[@"Cache-Control"])
    {
        NSMutableDictionary *modifiedHeaders = headers.mutableCopy;
        [modifiedHeaders setObject:@"max-age=60" forKey:@"Cache-Control"];
        NSHTTPURLResponse *modifiedResponse = [[NSHTTPURLResponse alloc]
                                               initWithURL:HTTPResponse.URL
                                               statusCode:HTTPResponse.statusCode
                                               HTTPVersion:@"HTTP/1.1"
                                               headerFields:modifiedHeaders];
 
        cachedResponse = [[NSCachedURLResponse alloc]
                          initWithResponse:modifiedResponse
                          data:proposedResponse.data
                          userInfo:proposedResponse.userInfo
                          storagePolicy:proposedResponse.storagePolicy];
    }
    else
    {
        cachedResponse = proposedResponse;
    }
    completionHandler(cachedResponse);
}

通用的緩存方案:

HTTP緩存策略中,我們從服務(wù)器獲取Response后,可以找到(如果有)Response中包含Etag或則Last-Modified字段。當(dāng)我們做第二次重復(fù)請(qǐng)求的時(shí)候,可以從CachedURLResponse取出來,把相應(yīng)字段拼接在HTTPRequestHeader中(例如,IMS,If-Modified-Since配合Last_Modified),然后發(fā)送請(qǐng)求,服務(wù)端收到后,如果客戶端的資源是最新的,那么就會(huì)返回304為Response,而不返回任何內(nèi)容。反之,如果客戶端資源落后了,則直接返回200,并返回Data給客戶端。

通過Last-Modified來實(shí)現(xiàn)緩存:
通過Last-Modified來確定服務(wù)端數(shù)據(jù)是否已經(jīng)修改,客戶端緩存是否有效。
Last-Modified顧名思義就是資源的最后修改時(shí)間戳,往往與緩存時(shí)間進(jìn)行比較來判斷是否過期(比較操作有服務(wù)端實(shí)現(xiàn)).
在第一次請(qǐng)求一個(gè)URL時(shí)候,服務(wù)端給出響應(yīng),響應(yīng)頭中有一個(gè)Last-Modified的屬性標(biāo)記此文件在服務(wù)端最后被修改的時(shí)間,格式類似這樣:
Last-Modified: Fri, 12 May 2006 18:53:33 GMT

總結(jié)下來它的結(jié)構(gòu)如下:
響應(yīng)頭:Last-Modified
請(qǐng)求頭: If-Modified-Since

如果服務(wù)器的資源沒有變化,則自動(dòng)返回HTTP304, data為空, 節(jié)省了傳輸數(shù)據(jù)量。服務(wù)端發(fā)生變化或者重啟服務(wù)器時(shí),則重新發(fā)出資源,從而保證不向客戶端發(fā)送重復(fù)資源,也保證了當(dāng)服務(wù)發(fā)生變化的時(shí)候,客戶端可以得到最新的資源
代碼如下:

- (void)getData {
    NSURL *url = [NSURL URLWithString:kLastModifiedImageURL];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];

    // 發(fā)送 LastModified
    if (self.localLastModified.length > 0) {
        [request setValue:self.localLastModified forHTTPHeaderField:@"If-Modified-Since"];
    }
    [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        // NSLog(@"%@ %tu", response, data.length);
        // 類型轉(zhuǎn)換(如果將父類設(shè)置給子類,需要強(qiáng)制轉(zhuǎn)換)
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
        NSLog(@"statusCode == %@", @(httpResponse.statusCode));
        // 判斷響應(yīng)的狀態(tài)碼是否是 304 Not Modified (更多狀態(tài)碼含義解釋: https://github.com/ChenYilong/iOSDevelopmentTips)
        if (httpResponse.statusCode == 304) {
            NSLog(@"加載本地緩存圖片");
            // 如果是,使用本地緩存
            // 根據(jù)請(qǐng)求獲取到`被緩存的響應(yīng)`!
            NSCachedURLResponse *cacheResponse =  [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
            // 拿到緩存的數(shù)據(jù)
            data = cacheResponse.data;
        }
        // 獲取并且紀(jì)錄 LastModified
        self.localLastModified = httpResponse.allHeaderFields[@"Last-Modified"];
        NSLog(@"%@", self.localLastModified);
    }] resume];

通過Etag來確定服務(wù)端是否數(shù)據(jù)有變化
HTTP協(xié)議規(guī)定Etag為"被請(qǐng)求變量的實(shí)體值",其實(shí)就是一個(gè)hash值,唯一標(biāo)記資源。服務(wù)器單獨(dú)負(fù)責(zé)判斷Etag是什么含義,并在HTTP響應(yīng)頭中將其傳送到客戶端,以下是服務(wù)端返回的格式:
Etag:"50b1c1d4f775c61:df3"
客戶端的查詢跟新格式是這樣的:
If-None-Match: W/"50b1c1d4f775c61:df3"
其中
If-None-Match 與響應(yīng)頭的Etag相對(duì)應(yīng),可以判斷本地緩存數(shù)據(jù)是否發(fā)生變化。
如果Etag沒有改變,則返回304,data為空。與Last-Modified一樣

總結(jié)下來結(jié)構(gòu)如下:
響應(yīng)頭:Etag
請(qǐng)求頭:If-None-Match

- (void)getData{
    NSURL *url = [NSURL URLWithString:kETagImageURL];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
    
    // 發(fā)送 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);
        // 類型轉(zhuǎn)換(如果將父類設(shè)置給子類,需要強(qiáng)制轉(zhuǎn)換)
        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
        NSLog(@"statusCode == %@", @(httpResponse.statusCode));
        // 判斷響應(yīng)的狀態(tài)碼是否是 304 Not Modified (更多狀態(tài)碼含義解釋: https://github.com/ChenYilong/iOSDevelopmentTips)
        if (httpResponse.statusCode == 304) {
            NSLog(@"加載本地緩存圖片");
            // 如果是,使用本地緩存
            // 根據(jù)請(qǐng)求獲取到`被緩存的響應(yīng)`!
            NSCachedURLResponse *cacheResponse =  [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
            // 拿到緩存的數(shù)據(jù)
            data = cacheResponse.data;
        }
        // 獲取并且紀(jì)錄 etag,區(qū)分大小寫
        self.etag = httpResponse.allHeaderFields[@"Etag"];
        NSLog(@"%@", self.etag);
    }] resume];
}

由于修改資源后Etag值會(huì)立即改變。這也決定了Etag在斷點(diǎn)下載時(shí)非常有用。比如AFNetworking在進(jìn)行斷點(diǎn)下載時(shí)候,就是借助它來檢驗(yàn)數(shù)據(jù)的。祥見AFHTTPRequestOperation類中的用法:

- (void)pause {
    unsigned long long offset = 0;
    if ([self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey]) {
        offset = [[self.outputStream propertyForKey:NSStreamFileCurrentOffsetKey] unsignedLongLongValue];
    } else {
        offset = [[self.outputStream propertyForKey:NSStreamDataWrittenToMemoryStreamKey] length];
    }
    NSMutableURLRequest *mutableURLRequest = [self.request mutableCopy];
    if ([self.response respondsToSelector:@selector(allHeaderFields)] && [[self.response allHeaderFields] valueForKey:@"ETag"]) {
        //若請(qǐng)求返回的頭部有ETag,則續(xù)傳時(shí)要帶上這個(gè)ETag,
        //ETag用于放置文件的唯一標(biāo)識(shí),比如文件MD5值
        //續(xù)傳時(shí)帶上ETag服務(wù)端可以校驗(yàn)相對(duì)上次請(qǐng)求,文件有沒有變化,
        //若有變化則返回200,回應(yīng)新文件的全數(shù)據(jù),若無變化則返回206續(xù)傳。
        [mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"];
    }
    //給當(dāng)前request加Range頭部,下次請(qǐng)求帶上頭部,可以從offset位置繼續(xù)下載
    [mutableURLRequest setValue:[NSString stringWithFormat:@"bytes=%llu-", offset] forHTTPHeaderField:@"Range"];
    self.request = mutableURLRequest;
    [super pause];
}

參考:http://nshipster.cn/nsurlcache/
http://chesterlee.github.io/blog/2014/08/10/ioszhong-de-urlcacheji-zhi/
http://www.hpique.com/2014/03/how-to-cache-server-responses-in-ios-apps/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容