NSURLCache

作者: iOneWay | 来源:发表于2016-10-10 20:24 被阅读189次

    NSURLCache为你的url请求提供了内存以及磁盘上的综合缓存机制。使用缓存可以减少向服务发送请求的次数,同时提升了离线或低速网络中的使用体验,以及减轻了服务的压力。

    NSURLCache会自动且透明的处理网络缓存:当一个请求完成下载来自服务器的响应,一个缓存的回应将在本地保存。下次同一个请求再次发起时,本地保存的回应就会马上返回,不需要连接服务器。

    使用缓存,我门一般会在Appdelegate中设置,如下:

    - (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有一个cachePolicy属性,根据该属性值来设置缓存行为:

    • NSURLRequestUseProtocolCachePolicy: 默认行为(使用网络协议中实现的缓存逻辑)
    • NSURLRequestReloadIgnoringLocalCacheData: 不使用缓存,每次都从网络下载。
    • NSURLRequestReloadIgnoringLocalAndRemoteCacheData:不仅忽略本地缓存,同时也忽略代理服务器或其他中间介质如:CDN等的缓存。
    • NSURLRequestReturnCacheDataDontLoad: 无论缓存是否过期,先使用本地缓存数据。如果缓存中没有申请所对应的数据,那么从原始地址加载数据。
    • NSURLRequestReturnCacheRevalidatingCacheData:从原始地址确认缓存数据的合法性后,缓存数据就可以使用,否则从原始地址加载。
      其中NSURLRequestReloadIgnoringLocalAndRemoteCacheData 和 NSURLRequestReloadRevalidatingCacheData 根本没有实现。

    最常用的缓存策略是默认行为:NSURLRequestUseProtocolCachePolicy
    它的缓存步骤是:
    1,如果一个Request的NSCacheURLResponse不存在,就去请求网络。
    2,如果一个Request的NSCacheURLResponse存在,就去检查response去决定是否需要重新获取。检查Resopne header的Cache-Control字段是否含有must-revalidated字段(http1.1)
    3, 如果包含must-revalidated字段,就通过HEAD方法请求服务器,判断Response头是否有跟新,如果有则去获取数据,如果没有则直接使用cache资源。
    4,如果不包含must-revalidated字段,就查看Cache-Control是否包含其他字段,比如max-age等等是否过期,如果过期,同3一样使用HEAD方法去检查Response头,是否为最新数据,如果有则去请求服务器,反之取cache。如果没有过期,直接取cache。

    以下是证明设置NSURLRequestUseProtocolCachePolicy后直接使用本地缓存而不在请求网络的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是我从网络上随便找的一个链接,它的响应头Cache-Control字段的值为:31536000. 可以说是相当大的。
    当第一次请求到数据后,断开网络,再次离线请求,依旧可以获取到数据,该数据来自Cache。降低了对网络的依赖,减轻了服务的压力,同时也提升了用户体验。

    有些情况下服务端api并没有设置缓存头:Cache-Control。但是我们又希望能够自动缓存一些数据,则可以实现NSURLSessionDataDelegate 协议中的一个方法:

    - (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缓存策略中,我们从服务器获取Response后,可以找到(如果有)Response中包含Etag或则Last-Modified字段。当我们做第二次重复请求的时候,可以从CachedURLResponse取出来,把相应字段拼接在HTTPRequestHeader中(例如,IMS,If-Modified-Since配合Last_Modified),然后发送请求,服务端收到后,如果客户端的资源是最新的,那么就会返回304为Response,而不返回任何内容。反之,如果客户端资源落后了,则直接返回200,并返回Data给客户端。

    通过Last-Modified来实现缓存:
    通过Last-Modified来确定服务端数据是否已经修改,客户端缓存是否有效。
    Last-Modified顾名思义就是资源的最后修改时间戳,往往与缓存时间进行比较来判断是否过期(比较操作有服务端实现).
    在第一次请求一个URL时候,服务端给出响应,响应头中有一个Last-Modified的属性标记此文件在服务端最后被修改的时间,格式类似这样:
    Last-Modified: Fri, 12 May 2006 18:53:33 GMT

    总结下来它的结构如下:
    响应头:Last-Modified
    请求头: If-Modified-Since

    如果服务器的资源没有变化,则自动返回HTTP304, data为空, 节省了传输数据量。服务端发生变化或者重启服务器时,则重新发出资源,从而保证不向客户端发送重复资源,也保证了当服务发生变化的时候,客户端可以得到最新的资源
    代码如下:

    - (void)getData {
        NSURL *url = [NSURL URLWithString:kLastModifiedImageURL];
        NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
    
        // 发送 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);
            // 类型转换(如果将父类设置给子类,需要强制转换)
            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;
            }
            // 获取并且纪录 LastModified
            self.localLastModified = httpResponse.allHeaderFields[@"Last-Modified"];
            NSLog(@"%@", self.localLastModified);
        }] resume];
    

    通过Etag来确定服务端是否数据有变化
    HTTP协议规定Etag为"被请求变量的实体值",其实就是一个hash值,唯一标记资源。服务器单独负责判断Etag是什么含义,并在HTTP响应头中将其传送到客户端,以下是服务端返回的格式:
    Etag:"50b1c1d4f775c61:df3"
    客户端的查询跟新格式是这样的:
    If-None-Match: W/"50b1c1d4f775c61:df3"
    其中
    If-None-Match 与响应头的Etag相对应,可以判断本地缓存数据是否发生变化。
    如果Etag没有改变,则返回304,data为空。与Last-Modified一样

    总结下来结构如下:
    响应头:Etag
    请求头:If-None-Match

    - (void)getData{
        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);
        }] resume];
    }
    

    由于修改资源后Etag值会立即改变。这也决定了Etag在断点下载时非常有用。比如AFNetworking在进行断点下载时候,就是借助它来检验数据的。祥见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"]) {
            //若请求返回的头部有ETag,则续传时要带上这个ETag,
            //ETag用于放置文件的唯一标识,比如文件MD5值
            //续传时带上ETag服务端可以校验相对上次请求,文件有没有变化,
            //若有变化则返回200,回应新文件的全数据,若无变化则返回206续传。
            [mutableURLRequest setValue:[[self.response allHeaderFields] valueForKey:@"ETag"] forHTTPHeaderField:@"If-Range"];
        }
        //给当前request加Range头部,下次请求带上头部,可以从offset位置继续下载
        [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/

    相关文章

      网友评论

        本文标题:NSURLCache

        本文链接:https://www.haomeiwen.com/subject/rrauyttx.html