iOS网络缓存扫盲篇 - 使用两行代码就能完成80%的缓存需求

2019年11月16日 阅读数:23
这篇文章主要向大家介绍iOS网络缓存扫盲篇 - 使用两行代码就能完成80%的缓存需求,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

目录

  1. 当咱们在谈论缓存的时候,咱们在谈论什么?git

  2. GET网络请求缓存github

    1. 80%的缓存需求:两行代码就可知足数据库

    2. 控制缓存的有效性json

    3. 文件缓存:借助ETag或Last-Modified判断文件缓存是否有效api

      1. Last-Modified数组

      2. ETag浏览器

      3. 总结缓存

    4. 通常数据类型借助 Last-Modified 与 ETag 进行缓存安全

  3. 剩下20%的网络缓存需求--真的有NSURLCache 不能知足的需求?服务器

因为微信、QQ、微博、这类的应用使用缓存很“重”,使通常的用户也对缓存也很是习惯。缓存已然成为必备。

缓存的目的的以空间换时间

这句话在动辄就是 300M、600M 的大应用上,获得了很好的诠释。但能有缓存意识的公司,还在少数。

只有你真正感觉到痛的时候,你才会考虑使用缓存。

这个痛多是:

服务器压力、客户端网络优化、用户体验等等。

当咱们在谈论缓存的时候,咱们在谈论什么?

咱们今天将站在小白用户的角度,给缓存这个概念进行从新的定义。

缓存有不一样的分类方法:

enter image description here

这里所指的缓存,是一个宽泛的概念。

咱们这里主要按照功能进行划分:

enter image description here

第一种 第二种
目的 优化型缓存 功能型缓存
具体描述 出于优化考虑:服务器压力、用户体验、为用户剩流量等等。同时优化型缓存也有内存缓存和磁盘缓存之分。 App离线也能查看,出于功能考虑,属于存储范畴
常见概念 GET网络请求缓存、WEB缓存 离线存储
典型应用 微信首页的会话列表、微信头像、朋友圈、网易新闻新闻列表、 微信聊天记录、
Parse对应的类 PFCachedQueryController PFOfflineStore

重度使用缓存的App: 微信、微博、网易新闻、携程、去哪儿等等。

GET网络请求缓存

概述

首先要知道,POST请求不能被缓存,只有 GET 请求能被缓存。由于从数学的角度来说,GET 的结果是 幂等 的,就好像字典里的 key 与 value 就是幂等的,而 POST 不 幂等 。缓存的思路就是将查询的参数组成的值做为 key ,对应结果做为value。从这个意义上说,一个文件的资源连接,也叫 GET 请求,下文也会这样看待。

80%的缓存需求:两行代码就可知足

设置缓存只须要三个步骤:

第一个步骤:请使用 GET 请求。

第二个步骤:若是你已经使用 了 GET 请求,iOS 系统 SDK 已经帮你作好了缓存。你须要的仅仅是设置下内存缓存大小、磁盘缓存大小、以及缓存路径。甚至这两行代码不设置也是能够的,会有一个默认值。代码以下:

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

第三个步骤:没有第三步!

你只要设置了这两行代码,基本就可知足80%的缓存需求。AFNetworking 的做者 Mattt曾经说过:

无数开发者尝试本身作一个简陋而脆弱的系统来实现网络缓存的功能,却不知 NSURLCache 只要两行代码就能搞定且好上 100 倍。

(AFN 是否是在暗讽 SDWebImage 复杂又蹩脚的缓存机制??)

要注意

  • iOS 5.0开始,支持磁盘缓存,但仅支持 HTTP

  • iOS 6.0开始,支持 HTTPS 缓存

控制缓存的有效性

咱们知道:只要是缓存,总会过时。

那么缓存的过时时间如何控制?

上文中的两行代码,已经给出了一个方法,指定超时时间。但这并也许不能知足咱们的需求,若是咱们对数据的一致性,时效性要求很高,即便1秒钟后数据更改了,客户端也必须展现更改后的数据。这种状况如何处理?

下面咱们将对这种需求,进行解决方案的介绍。顺序是这样的:先从文件类型的缓存入手,引入两个概念。而后再谈下,通常数据类型好比 JSON 返回值的缓存处理。

文件缓存:借助ETag或Last-Modified判断文件缓存是否有效

Last-Modified

服务器的文件存贮,大多采用资源变更后就从新生成一个连接的作法。并且若是你的文件存储采用的是第三方的服务,好比七牛、青云等服务,则必定是如此。

这种作法虽然是推荐作法,但同时也不排除不一样文件使用同一个连接。那么若是服务端的file更改了,本地已经有了缓存。如何更新缓存?

这种状况下须要借助 ETagLast-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

总结下来它的结构以下:

请求 HeaderValue 响应 HeaderValue
Last-Modified If-Modified-Since

若是服务器端的资源没有变化,则自动返回 HTTP 304 (Not Changed.)状态码,内容为空,这样就节省了传输数据量。当服务器端代码发生改变或者重启服务器时,则从新发出资源,返回和第一次请求时相似。从而保证不向客户端重复发出资源,也保证当服务器有变化时,客户端可以获得最新的资源。

判断方法用伪代码表示:

if ETagFromServer != ETagOnClient || LastModifiedFromServer != LastModifiedOnClient
   GetFromServer
else
   GetFromCache

之因此使用

LastModifiedFromServer != LastModifiedOnClient

而非使用:

LastModifiedFromServer > LastModifiedOnClient

缘由是考虑到可能出现相似下面的状况:服务端可能对资源文件,废除其新版,回滚启用旧版本,此时的状况是:

LastModifiedFromServer <= LastModifiedOnClient

但咱们依然要更新本地缓存。

参考连接: What takes precedence: the ETag or Last-Modified HTTP header?

Demo10和 Demo11 给出了一个完整的校验步骤:

并给出了 NSURLConnectionNSURLSession 两个版本:

/*!
 @brief 若是本地缓存资源为最新,则使用使用本地缓存。若是服务器已经更新或本地无缓存则从服务器请求资源。
 
 @details
 
 步骤:
 1. 请求是可变的,缓存策略要每次都从服务器加载
 2. 每次获得响应后,须要记录住 LastModified
 3. 下次发送请求的同时,将LastModified一块儿发送给服务器(由服务器比较内容是否发生变化)
 
 @return 图片资源
 */
- (void)getData:(GetDataCompletion)completion {
    NSURL *url = [NSURL URLWithString:kLastModifiedImageURL];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
    
    //    // 发送 etag
    //    if (self.etag.length > 0) {
    //        [request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
    //    }
    // 发送 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;
        }
        
        // 获取而且纪录 etag,区分大小写
        //        self.etag = httpResponse.allHeaderFields[@"Etag"];
        // 获取而且纪录 LastModified
        self.localLastModified = httpResponse.allHeaderFields[@"Last-Modified"];
        //        NSLog(@"%@", self.etag);
        NSLog(@"%@", self.localLastModified);
        dispatch_async(dispatch_get_main_queue(), ^{
            !completion ?: completion(data);
        });
    }] resume];
}

ETag

ETag 是什么?

HTTP 协议规格说明定义ETag为“被请求变量的实体值” (参见 —— 章节 14.19)。 另外一种说法是,ETag是一个能够与Web资源关联的记号(token)。它是一个 hash 值,用做 Request 缓存请求头,每个资源文件都对应一个惟一的 ETag 值,服务器单独负责判断记号是什么及其含义,并在HTTP响应头中将其传送到客户端,如下是服务器端返回的格式:

ETag: "50b1c1d4f775c61:df3"

客户端的查询更新格式是这样的:

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

其中:If-None-Match - 与响应头的 Etag 相对应,能够判断本地缓存数据是否发生变化

若是ETag没改变,则返回状态304而后不返回,这也和Last-Modified同样。

总结下来它的结构以下:

请求 HeaderValue 响应 HeaderValue
ETag If-None-Match

ETag 是的功能与 Last-Modified 相似:服务端不会每次都会返回文件资源。客户端每次向服务端发送上次服务器返回的 ETag 值,服务器会根据客户端与服务端的 ETag 值是否相等,来决定是否返回 data,同时老是返回对应的 HTTP 状态码。客户端经过 HTTP 状态码来决定是否使用缓存。好比:服务端与客户端的 ETag 值相等,则 HTTP 状态码为 304,不返回 data。服务端文件一旦修改,服务端与客户端的 ETag 值不等,而且状态值会变为200,同时返回 data。

由于修改资源文件后该值会当即变动。这也决定了 ETag 在断点下载时很是有用。
好比 AFNetworking 在进行断点下载时,就是借助它来检验数据的。详见在 AFHTTPRequestOperation 类中的用法:

    //下载暂停时提供断点续传功能,修改请求的HTTP头,记录当前下载的文件位置,下次能够从这个位置开始下载。
- (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];
}

七牛等第三方文件存储商如今都已经支持ETag,Demo8和9 中给出的演示图片就是使用的七牛的服务,见:

static NSString *const kETagImageURL = @"http://ac-g3rossf7.clouddn.com/xc8hxXBbXexA8LpZEHbPQVB.jpg";

下面使用一个 Demo 来进行演示用法,

NSURLConnection 搭配 ETag 为例,步骤以下:

  • 请求的缓存策略使用 NSURLRequestReloadIgnoringCacheData,忽略本地缓存

  • 服务器响应结束后,要记录 Etag,服务器内容和本地缓存对比是否变化的重要依据

  • 在发送请求时,设置 If-None-Match,而且传入 Etag

  • 链接结束后,要判断响应头的状态码,若是是 304,说明本地缓存内容没有发生变化

如下代码详见 Demo08 :

/*!
 @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 的版本见 Demo09:

/*!
 @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];
}

运行效果:

enter image description here

总结

在官方给出的文档中提出 ETag 是首选的方式,优于 Last-Modified 方式。由于 ETag 是基于 hash ,hash 的规则能够本身设置,并且是基于一致性,是“强校验”。 Last-Modified 是基于时间,是弱校验,弱在哪里?好比说:若是服务端的资源回滚客户端的 Last-Modified 反而会比服务端还要新。

虽然 ETag 优于 Last-Modified ,但并不是全部服务端都会支持,而 Last-Modified 则通常都会有该字段。 大多数状况下须要与服务端进行协调支持 ETag ,若是协商无果就只能退而求其次。

Demo 也给出了一个不支持 ETag 的连接,基本随便找一张图片都行:

static NSString *const kLastModifiedImageURL = @"http://image17-c.poco.cn/mypoco/myphoto/20151211/16/17338872420151211164742047.png";

做为通用型的网络请求工具 AFNetworking 对该现状的处理方式是,判断服务端是否包含 ETag ,而后再进行相应处理。可见 AFHTTPRequestOperation 类中的用法,也就是上文中已经给出的断点下载的代码。

在回顾下思路:

  • 为资源分派 hash 值,而后对比服务端与本地缓存是否一致来决定是否须要更新缓存。

这种思路,在开发中常用,好比:处于安全考虑,登录操做通常不会传输帐号密码,而是传输对应的 hash 值-- token ,这里的 token 就能够看作一个 file 资源,若是想让一个用户登录超时时间是三天,只须要在服务端每隔三天更改下 token 值,客户端与服务端值不一致,而后服务端返回 token 过时的提示。

值得注意的一点是:若是借助了 Last-ModifiedETag,那么缓存策略则必须使用 NSURLRequestReloadIgnoringCacheData 策略,忽略缓存,每次都要向服务端进行校验。

若是 GET 中包含有版本号信息

众多的应用都会在 GET 请求后加上版本号:

http://abc.com?my_current_version=v1.0.0

这种状况下,?v1.0?v2.0 两个不一样版本,请求到的 Last-ModifiedETag 会如预期吗?

这彻底取决于公司服务端同事的实现, Last-ModifiedETag 仅仅是一个协议,并无统一的实现方法,而服务端的处理逻辑彻底取决于需求。

你彻底能够要求服务端同事,仅仅判断资源的异同,而忽略掉 ?v1.0?v2.0 两个版本的区别。

参考连接:if-modified-since vs if-none-match

通常数据类型借助 Last-Modified ETag 进行缓存

以上的讨论是基于文件资源,那么对通常的网络请求是否也能应用?

控制缓存过时时间,无非两种:设置一个过时时间;校验缓存与服务端一致性,只在不一致时才更新。

通常状况下是不会对 api 层面作这种校验,只在有业务需求时才会考虑作,好比:

  1. 数据更新频率较低,“万不得已不会更新”---只在服务器有更新时才更新,以此来保证2G 等恶略网络环境下,有较好的体验。好比网易新闻栏目,但相反微博列表、新闻列表就不适合。

  2. 业务数据一致性要求高,数据更新后须要服务端马上展现给用户。客户端显示的数据必须是服务端最新的数据

  3. 有离线展现需求,必须实现缓存策略,保证弱网状况下的数据展现的速度。但不考虑使用缓存过时时间来控制缓存的有效性。

  4. 尽可能减小数据传输,节省用户流量

一些建议:

  1. 若是是 file 文件类型,用 Last-Modified 就够了。即便 ETag 是首选,但此时二者效果一致。九成以上的需求,效果都一致。

  2. 若是是通常的数据类型--基于查询的 get 请求,好比返回值是 data 或 string 类型的 json 返回值。那么 Last-Modified 服务端支持起来就会困难一点。由于好比
    你作了一个博客浏览 app ,查询最近的10条博客, 基于此时的业务考虑 Last-Modified 指的是10条中任意一个博客的更改。那么服务端须要在你发出请求后,遍历下10条数据,获得“10条中是否至少一个被修改了”。并且要保证每一条博客表数据都有一个相似于记录 Last-Modified 的字段,这显然不太现实。

若是更新频率较高,好比最近微博列表、最近新闻列表,这些请求就不适合,更多的处理方式是添加一个接口,客户端将本地缓存的最后一条数据的的时间戳或 id 传给服务端,而后服务端会将新增的数据条数返回,没有新增则返回 nil 或 304。

参考连接: 《(慕课网)imooc iPhone3.3 接口数据缓存》

剩下20%的网络缓存需求

真的有NSURLCache 不能知足的需求?

有人可能要问:

NSURLCache 不是帮咱们作了硬盘缓存么?那咱们为何要本身用数据库作本地缓存啊。为啥不直接用NSURLCache 不是更方便?

系统帮咱们作的缓存,好处是自动,无需咱们进行复杂的设置。坏处也偏偏是这个:不够灵活,不能自定义。只能指定一个缓存的总文件夹,不能分别指定每个文件缓存的位置,更不能为每一个文件建立一个文件夹,也不能指定文件夹的名称。缓存的对象也是固定的:只能是 GET请求的返回值。

下一篇文章咱们将主要围绕这一问题展开讨论下:使用80%的代码来完成剩下的20%的缓存需求 。敬请 star (右上角)持续关注