文档章节

OkHttp离线缓存实现

叶大侠
 叶大侠
发布于 2016/12/11 23:29
字数 1512
阅读 922
收藏 36
点赞 0
评论 2

场景应用:

OkHttp内部已经支持了标准的Http协议缓存策略,如Last-Modified, Etag, Cache-Control等方式,看起来已经是非常够用了,但是我们还想让缓存在这样的场景下得到使用: 当客户端设备网络中断或者服务端出现了错误,但是本地存在缓存副本的时候,我们依然想取用这部分缓存。

OkHttp保存缓存的时机:

okhttp3.internal.http.HttpEngine:
private void maybeCache() throws IOException {
    InternalCache responseCache = Internal.instance.internalCache(client);
    if (responseCache == null) return;

    // Should we cache this response for this request?
    if (!CacheStrategy.isCacheable(userResponse, networkRequest)) {
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          responseCache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
      return;
    }

    // Offer this request to the cache.
    storeRequest = responseCache.put(stripBody(userResponse));
}

这里我们可以看到OkHttp是否缓存请求结果其中的一个条件:CacheStrategy.isCacheable(), 看看这里面的代码:

okhttp3.internal.http.CacheStrategy:
public static boolean isCacheable(Response response, Request request) {
    // Always go to network for uncacheable response codes (RFC 7231 section 6.1),
    // This implementation doesn't support caching partial content.
    switch (response.code()) {
      case HTTP_OK:
      case HTTP_NOT_AUTHORITATIVE:
      case HTTP_NO_CONTENT:
      case HTTP_MULT_CHOICE:
      case HTTP_MOVED_PERM:
      case HTTP_NOT_FOUND:
      case HTTP_BAD_METHOD:
      case HTTP_GONE:
      case HTTP_REQ_TOO_LONG:
      case HTTP_NOT_IMPLEMENTED:
      case StatusLine.HTTP_PERM_REDIRECT:
        // These codes can be cached unless headers forbid it.
        break;

      case HTTP_MOVED_TEMP:
      case StatusLine.HTTP_TEMP_REDIRECT:
        // These codes can only be cached with the right response headers.
        // http://tools.ietf.org/html/rfc7234#section-3
        // s-maxage is not checked because OkHttp is a private cache that should ignore s-maxage.
        if (response.header("Expires") != null
            || response.cacheControl().maxAgeSeconds() != -1
            || response.cacheControl().isPublic()
            || response.cacheControl().isPrivate()) {
          break;
        }
        // Fall-through.

      default:
        // All other codes cannot be cached.
        return false;
    }

    // A 'no-store' directive on request or response prevents the response from being cached.
    return !response.cacheControl().noStore() && !request.cacheControl().noStore();
}

我们接着回到maybeCache()方法往下看,最后一句responseCache.put(stripBody(userResponse))将会调用下面的方法:

okhttp3.Cache:
private CacheRequest put(Response response) throws IOException {
    String requestMethod = response.request().method();

    if (HttpMethod.invalidatesCache(response.request().method())) {
      try {
        remove(response.request());
      } catch (IOException ignored) {
        // The cache cannot be written.
      }
      return null;
    }
    if (!requestMethod.equals("GET")) {
      // Don't cache non-GET responses. We're technically allowed to cache
      // HEAD requests and some POST requests, but the complexity of doing
      // so is high and the benefit is low.
      return null;
    }

    if (OkHeaders.hasVaryAll(response)) {
      return null;
    }

    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(urlToKey(response.request()));
      if (editor == null) {
        return null;
      }
      //缓存写入文件:
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
}

总的来说就是只要是Get请求,同时请求和响应头部CacheControl没有设置no store就会把请求结果缓存了。

OkHttp保存缓存的格式:

url所对应的缓存文件名规则,不难看出是url的md5值。

Util.md5Hex(request.url().toString())

保存的代码很明显就在entry.writeTo里面啦。

okhttp3.Cache.Entry:
public void writeTo(DiskLruCache.Editor editor) throws IOException {
  //.0结尾的文件
  BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

  sink.writeUtf8(url);
  sink.writeByte('\n');
  sink.writeUtf8(requestMethod);
  sink.writeByte('\n');
  sink.writeDecimalLong(varyHeaders.size());
  sink.writeByte('\n');
  for (int i = 0, size = varyHeaders.size(); i < size; i++) {
	sink.writeUtf8(varyHeaders.name(i));
	sink.writeUtf8(": ");
	sink.writeUtf8(varyHeaders.value(i));
	sink.writeByte('\n');
  }

  sink.writeUtf8(new StatusLine(protocol, code, message).toString());
  sink.writeByte('\n');
  sink.writeDecimalLong(responseHeaders.size());
  sink.writeByte('\n');
  for (int i = 0, size = responseHeaders.size(); i < size; i++) {
	sink.writeUtf8(responseHeaders.name(i));
	sink.writeUtf8(": ");
	sink.writeUtf8(responseHeaders.value(i));
	sink.writeByte('\n');
  }

  if (isHttps()) {
	sink.writeByte('\n');
	sink.writeUtf8(handshake.cipherSuite().javaName());
	sink.writeByte('\n');
	writeCertList(sink, handshake.peerCertificates());
	writeCertList(sink, handshake.localCertificates());
	// The handshake’s TLS version is null on HttpsURLConnection and on older cached responses.
	if (handshake.tlsVersion() != null) {
	  sink.writeUtf8(handshake.tlsVersion().javaName());
	  sink.writeByte('\n');
	}
  }
  sink.close();
}

我们打开相应的缓存目录看一下,果然其中包含了以url的md5值作为文件名的一系列文件,还有一个journal文件,这个文件和DiskLruCache缓存有关,我们这里不展开分析。我们看一下.0和.1文件的内容吧,其实.0里面的东西从上面的代码已经可以知道就是和请求和响应头部的一些信息。

http://www.infoq.com/cn/articles/etags
GET
2
Accept-Encoding: gzip
User-Agent: okhttp/3.2.0
HTTP/1.1 200 OK
15
Date: Sun, 11 Dec 2016 14:50:36 GMT
Server: Apache
Sniply-Options: BLOCK
Set-Cookie: JSESSIONID=7FFBDE344A66525EE343DCCC6DD23692; Path=/
Last-Modified: Sun, 11 Dec 2016 14:50:36 GMT
Vary: Accept-Encoding,User-Agent
Access-Control-Allow-Credentials: true
Accept-Ranges: none
Access-Control-Allow-Origin: http://www.infoq.com
Content-Encoding: gzip
Connection: close
Transfer-Encoding: chunked
Content-Type: text/html;charset=utf-8
OkHttp-Sent-Millis: 1481467841870
OkHttp-Received-Millis: 1481467844412

md5(url).1文件很容易猜到就是response本身的内容了。读者可以自行验证,另外如果response是gzip的格式,那么这里缓存的直接就是gzip后的内容了。

OkHttp离线缓存实现:

通过以上的分析,我们的思路就很清晰了,其实就是一开始就判断是否连接网络,如果是否就直接返回缓存的内容,根据response的头部是否是Content-Encoding: gzip来确定是否需要解压后返回;如果请求服务端失败,那么也返回缓存的内容。

通过分析OkHttp的InterceptorChain实现:

okhttp3.RealCall.ApplicationInterceptorChain:

public Response proceed(Request request) throws IOException {
      // If there's another interceptor in the chain, call that.
      if (index < client.interceptors().size()) {
        Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
        Interceptor interceptor = client.interceptors().get(index);
        Response interceptedResponse = interceptor.intercept(chain);

        if (interceptedResponse == null) {
          throw new NullPointerException("application interceptor " + interceptor
              + " returned null");
        }

        return interceptedResponse;
      }

      // No more interceptors. Do HTTP.
      return getResponse(request, forWebSocket);
}

不难发现只要通过一个Interceptor返回response即可中断http后续的请求动作,如果服务端错误,response将会返回相应的错误码,总结最后的实现代码如下:

final class CacheControlInterceptor implements Interceptor {

    private final String mCacheDir;

    private final ILog mLog;

    CacheControlInterceptor(ILog log, String cacheDir){
        this.mCacheDir = cacheDir;
        this.mLog = log;
    }

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        boolean networkAvail = NetUtils.isNetworkAvailable();
        if (!networkAvail){
            Response cacheResponse = getCacheResponse(request);
            if(cacheResponse != null){
                return cacheResponse;
            }
        }
        Response networkResponse = chain.proceed(request);
        if(networkAvail && !networkResponse.isSuccessful()){
            Response cacheResponse = getCacheResponse(request);
            if(cacheResponse != null){
                return cacheResponse;
            }
        }
        return networkResponse;
    }

    private Response getCacheResponse(Request request) throws FileNotFoundException{

        if(!"get".equalsIgnoreCase(request.method())|| mCacheDir == null){
            return null;
        }

        String urlMd5 = Util.md5Hex(request.url().url().toString());
        File headerCacheFile = new File(new File(mCacheDir), urlMd5 + ".0");
        Response.Builder cacheResponseBuilder = null;
        if (headerCacheFile.exists()) {
            cacheResponseBuilder = new Response.Builder();
            cacheResponseBuilder.request(request);
            readCacheResponseHeaders(headerCacheFile,cacheResponseBuilder);
        }

        if(cacheResponseBuilder != null){
            Response cacheResponse = cacheResponseBuilder.build();
            File bodyCacheFile = new File(new File(mCacheDir), urlMd5 + ".1");
            Source cacheSource;
            if(!"gzip".equalsIgnoreCase(cacheResponse.header("Content-Encoding"))){
                cacheSource = Okio.source(bodyCacheFile);
            }else{
                cacheSource = new GzipSource(Okio.source(bodyCacheFile));
            }
            RealResponseBody responseBody = new RealResponseBody(cacheResponse.headers(),Okio.buffer(cacheSource));
            return cacheResponse.newBuilder().body(responseBody).build();
        }

        return null;
    }

    private void readCacheResponseHeaders(File headerCacheFile,Response.Builder responseBuilder) {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new InputStreamReader(new FileInputStream(headerCacheFile)));
            String lineText;
            int code = 0;
            boolean isHeaderBegin = false;
            while((lineText = reader.readLine()) != null){

                if(isHeaderBegin){
                    int sepIndex = lineText.indexOf(":");
                    String headerName = lineText.substring(0,sepIndex);
                    String headerVal = lineText.substring(sepIndex + 1).trim();
                    responseBuilder.addHeader(headerName,headerVal);
                }

                if(code != 0 && NumberUtils.parseInt(lineText,0) != 0){
                    isHeaderBegin = true;
                    continue;
                }

                if(lineText.startsWith("HTTP/1.1")){
                    code = Integer.valueOf(lineText.substring(lineText.indexOf(" ")).trim());
                    responseBuilder.protocol(Protocol.HTTP_1_1);
                    responseBuilder.code(code);
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        } finally {
            IOUtils.closeSilently(reader);
        }
    }
}

参考资料

  1. http缓存: https://developers.google.cn/web/fundamentals/performance/optimizing-content-efficiency/http-caching?hl=zh-cn
  2. tomcat中对客户端的缓存机制: http://blog.csdn.net/liweisnake/article/details/8524179
  3. Okhttp Interceptors: https://github.com/square/okhttp/wiki/Interceptors

© 著作权归作者所有

共有 人打赏支持
叶大侠

叶大侠

粉丝 56
博文 44
码字总数 67312
作品 5
广州
程序员
加载中

评论(2)

OSC_vlkPzm
OSC_vlkPzm
不明觉厉
J-Fla
J-Fla
不错的文
Android面试有迹可循(一)OkHttp3.9拦截器原理与区别

接上回 传送门 上回我们讲到,OkHttp的请求过程中有个非常重要的东西-“拦截器”,而且拦截器又分为interceptors和networkInterceptors两种,那它们具体有何区别呢?又要怎么来使用?现在来一...

SillyMonkey ⋅ 05/19 ⋅ 0

spring传统xml配置okhttp3

问题 后端服务需要使用http客户端请求其他服务支持,项目中需要将HttpClient换成OKhttp,为啥要换OKhttp?这里不讨论这两者之间的优缺点。这篇文章主要关注与Spring传统xml配置方式集成Okhtt...

亚林瓜子 ⋅ 04/19 ⋅ 0

OkHttp3源码解析内部缓存

OkHttp3系列文章 OkHttp3 源码解析执行流程 OkHttp3 源码解析 连接池的复用 如果有了解过OkHttp的执行流程,可以知道,在拦截器链中有一个缓存拦截器CacheInterceptor,里面决定了是由缓存中...

Gillben ⋅ 05/22 ⋅ 0

Android面试的那些答不上来的问题(一)--- OkHttp的拦截器你到底了解多少(上)

前言 前段时间面试了很多家公司(坐标成都,大大小小加起来得20家吧),有时候有些事做多了,你就会发现它的一些窍门或者规律,面试这件事当然也不例外。其实很多公司问到的问题都大同小异,...

SillyMonkey ⋅ 05/10 ⋅ 0

说说在 Android 中如何发送 HTTP 请求

客户端会向服务器发出一条 HTTP 请求,服务器收到请求后会返回一些数据给客户端,然后客户端再对这些数据进行解析与处理。 1 HttpURLConnection 可以使用 HttpURLConnection(官方推荐) 来发...

deniro ⋅ 06/09 ⋅ 0

Android逆向之旅---爆破一款资讯类应用「最右」防抓包策略原理分析

一、逆向分析 首先感谢王同学提供的样本,因为王同学那天找到我咨询我说有一个应用Fiddler抓包失败,其实对于这类问题,我一般都会这么回答:第一你是否安装Fiddler证书了,他说他安装了。第...

jiangwei0910410003 ⋅ 04/25 ⋅ 0

Retrofit源码分析之OKHttpCall

之前在Retrofit源码初探一文中我们提出了三个问题: 什么时候开始将注解中参数拼装成http请求的信息的? 如何产生发起http请求对象的? 如何将对象转换成我们在接口中指定的返回值的? 其中第...

低情商的大仙 ⋅ 05/13 ⋅ 0

[Tools] [okHttp] 调试笔记 --- 无法找到FormEncodingBuilder类

背景: 网上很多例子在讲okHttp的post和get时会用FormEncodingBuilder来构建RequstBody。 但是在okHttp3上,此接口已经已经变掉了,替代它的是MultipartBuilder。 官方说明: okHttp3所有改动...

kris_fei ⋅ 05/25 ⋅ 0

[工具安装使用] [Websocket] Wesocket Client测试用例

利用okHttp中的WebSocket功能在AndroidStudio测试WebSocketClient, 其中Server是使用okHttp中的moc web server搭建的,所以在同一台机器上测试的(moc只能在本机上测试),如果没有搭server, 可...

kris_fei ⋅ 05/11 ⋅ 0

OkHttp源码学习系列一:总流程和Dispatcher分析

本文为本人原创,转载请注明作者和出处。 OkHttp可以说是目前Android开发中最流行的基础网络框架了。相信你也一定早已学会了它的基本用法,今天我们来进一步学习它的源码,了解其请求原理,学...

业松 ⋅ 05/10 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Springboot2 之 Spring Data Redis 实现消息队列——发布/订阅模式

一般来说,消息队列有两种场景,一种是发布者订阅者模式,一种是生产者消费者模式,这里利用redis消息“发布/订阅”来简单实现订阅者模式。 实现之前先过过 redis 发布订阅的一些基础概念和操...

Simonton ⋅ 28分钟前 ⋅ 0

error:Could not find gradle

一.更新Android Studio后打开Project,报如下错误: Error: Could not find com.android.tools.build:gradle:2.2.1. Searched in the following locations: file:/D:/software/android/andro......

Yao--靠自己 ⋅ 昨天 ⋅ 0

Spring boot 项目打包及引入本地jar包

Spring Boot 项目打包以及引入本地Jar包 [TOC] 上篇文章提到 Maven 项目添加本地jar包的三种方式 ,本篇文章记录下在实际项目中的应用。 spring boot 打包方式 我们知道,传统应用可以将程序...

Os_yxguang ⋅ 昨天 ⋅ 0

常见数据结构(二)-树(二叉树,红黑树,B树)

本文介绍数据结构中几种常见的树:二分查找树,2-3树,红黑树,B树 写在前面 本文所有图片均截图自coursera上普林斯顿的课程《Algorithms, Part I》中的Slides 相关命题的证明可参考《算法(第...

浮躁的码农 ⋅ 昨天 ⋅ 0

android -------- 混淆打包报错 (warning - InnerClass ...)

最近做Android混淆打包遇到一些问题,Android Sdutio 3.1 版本打包的 错误如下: Android studio warning - InnerClass annotations are missing corresponding EnclosingMember annotation......

切切歆语 ⋅ 昨天 ⋅ 0

eclipse酷炫大法之设置主题、皮肤

eclipse酷炫大法 目前两款不错的eclipse 1.系统设置 Window->Preferences->General->Appearance 2.Eclipse Marketplace下载【推荐】 Help->Eclipse Marketplace->搜索‘theme’进行安装 比如......

anlve ⋅ 昨天 ⋅ 0

vim编辑模式、vim命令模式、vim实践

vim编辑模式 编辑模式用来输入或修改文本内容,编辑模式除了Esc外其他键几乎都是输入 如何进入编辑模式 一般模式输入以下按键,均可进入编辑模式,左下角提示 insert(中文为插入) 字样 i ...

蛋黄Yolks ⋅ 昨天 ⋅ 0

大数据入门基础:SSH介绍

什么是ssh 简单说,SSH是一种网络协议,用于计算机之间的加密登录。 如果一个用户从本地计算机,使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码...

董黎明 ⋅ 昨天 ⋅ 0

web3j教程

web3j是一个轻量级、高度模块化、响应式、类型安全的Java和Android类库提供丰富API,用于处理以太坊智能合约及与以太坊网络上的客户端(节点)进行集成。 汇智网最新发布的web3j教程,详细讲解...

汇智网教程 ⋅ 昨天 ⋅ 0

谷歌:安全问题机制并不如你想象中安全

腾讯科技讯 5月25日,如今的你或许已经对许多网站所使用的“安全问题机制”习以为常了,但你真的认为包括“你第一个宠物的名字是什么?”这些问题能够保障你的帐户安全吗? 根据谷歌(微博)安...

问题终结者 ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部