OkHttp3源码解析内部缓存

OkHttp3系列文章

如果有了解过OkHttp的执行流程,可以知道,在拦截器链中有一个缓存拦截器CacheInterceptor,里面决定了是由缓存中获取数据还是通过网络获取。笔者也以这个为入口,展开对OkHttp的缓存分析

1、CacheInterceptor # intercept

public Response intercept(Chain chain) throws IOException {

    //判断是否设置cache
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;

    long now = System.currentTimeMillis();
     
    CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
    Request networkRequest = strategy.networkRequest;
    Response cacheResponse = strategy.cacheResponse;
    
    //如果cache 不为null,从strategy中追踪Response
    //主要是对networkRequest 或cacheResponse 进行计数
    if (cache != null) {
      cache.trackResponse(strategy);
    }

    //如果缓存不适用,则关闭IO流
    if (cacheCandidate != null && cacheResponse == null) {
      closeQuietly(cacheCandidate.body()); 
    }

    // 如果网络被禁止并且无缓存,则返回失败504
    if (networkRequest == null && cacheResponse == null) {
      return new Response.Builder()
          .request(chain.request())
          .protocol(Protocol.HTTP_1_1)
          .code(504)
          .message("Unsatisfiable Request (only-if-cached)")
          .body(Util.EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build();
    }

    // 在无网络时从缓存中获取
    if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }

    Response networkResponse = null;
    try {
      //调用下一个拦截器,访问网络
      networkResponse = chain.proceed(networkRequest);
    } finally {
      // If we're crashing on I/O or otherwise, don't leak the cache body.
      if (networkResponse == null && cacheCandidate != null) {
        closeQuietly(cacheCandidate.body());
      }
    }

    // 如果缓存中已经存在对应的Response的处理
    if (cacheResponse != null) {
      //表示数据未做修改
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        //主要是更新response头部数据
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

    //写入缓存
    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        return cacheWritingResponse(cacheRequest, response);
      }
      
      //移除networkRequest
      if (HttpMethod.invalidatesCache(networkRequest.method())) {
        try {
          cache.remove(networkRequest);
        } catch (IOException ignored) {
          // The cache cannot be written.
        }
      }
    }

    return response;
  }

CacheStrategy内部封装了网络请求对象networkRequest和cacheResponse(实际就是开始获取的候选缓存对象cacheCandidate)。它是OkHttp的缓存策略的核心。

回到上面代码,首先判断是否已设置cache,如果已设置,根据chain.request()返回的request查找cache中对应的response,然后创建一个CacheStrategy对象strategy 。后续再通过对网络状态和缓存状态进行判断,如果是网络获取且未缓存,得到response后,会先更新写入缓存中,再返回。而上面代码的核心都是对cache的操作(get、put、update、remove)。

2、Cache的产生

cache是InternalCache类型的对象,InternalCache是OkHttp的内部缓存接口。它又是怎么实现的呢?如果有了解过【OkHttp3 源码解析执行流程】这篇文章,会发现,在责任链开始执行之前就创建了CacheInterceptor对象,并从OkHttpClient获取了InternalCache的对象,在下面代码注释1处。

 Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    //1
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

client.internalCache()的内部实现

InternalCache internalCache() {
    return cache != null ? cache.internalCache : internalCache;
  }

代码比较简洁,根据cache是否为null,返回相应的internalCache。针对于cache == null 时返回的internalCache,这个是OkHttpClient静态代码块中创建Internal对象重写setCache()方法返回的。看下面代码(省略部分代码):

static {
    Internal.instance = new Internal() {
    ... ...

      @Override 
      public void setCache(OkHttpClient.Builder builder, InternalCache internalCache) {
        builder.setInternalCache(internalCache);
      }

    ... ...
    };
  }

经过查找发现,并没有任何地方调用setCache这个方法。也就是说在初始化OkHttpClient对象时,如果没有通过调用Cache()方法进行配置缓存,client.internalCache()将返回空,即无缓存。那么主要关注Cache()之后会发生什么。下面一个简单的调用Cache()配置缓存。

 private void doOkHttp(){
        //缓存路径
        String path = Environment.getExternalStorageDirectory().getPath()+"OkHttpCache";
        //最大缓存空间
        long size = 1024*1024*50;
        
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cache(new Cache(new File(path),size)) //配置缓存
                .build();
    }

这时在创建CacheInterceptor对象时调用client.internalCache()自然就返回了cache.internalCache。

Cache类的构造方法:

 public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }

  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
  }

在Cache类的构造方法中创建了DiskLruCache类型的cache 实例,这里的FileSystem.SYSTEM是FileSystem(文件系统接口)的实现,其内部是基于Okio的sink/source对缓存文件进行流操作。在DiskLruCache.Entry内部维护了两个数组,保存每个url请求对应文件的引用。然后通过DiskLruCache.Editor操作DiskLruCache.Entry中的数组,并为Cache.Entry提供Sink/source,对文件流进行操作。这点会在后续分析Cache的put和get中证实。

3、Cache的操作

经过上面的分析,在配置了缓存的情况下,最终返回cache.internalCache,进入Cache类内部的internalCache。

final InternalCache internalCache = new InternalCache() {
    @Override 
    public Response get(Request request) throws IOException {
      return Cache.this.get(request);
    }

    @Override 
    public CacheRequest put(Response response) throws IOException {
      return Cache.this.put(response);
    }

    @Override 
    public void remove(Request request) throws IOException {
      Cache.this.remove(request);
    }

    @Override 
    public void update(Response cached, Response network) {
      Cache.this.update(cached, network);
    }

    @Override 
    public void trackConditionalCacheHit() {
      Cache.this.trackConditionalCacheHit();
    }

    @Override 
    public void trackResponse(CacheStrategy cacheStrategy) {
      Cache.this.trackResponse(cacheStrategy);
    }
  };

到这里,可以发现,经过internalCache最终回调了Cache本身的put、get、update等方法进行操作。这里主要分析下put和get中做了什么事。

Cache.put()

CacheRequest put(Response response) {
    //获取请求方法
    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;
    }

    //如果不是GET请求时返回的response,则不进行缓存
    if (!requestMethod.equals("GET")) {    
      return null;
    }

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

    //把response封装在Cache.Entry中,调用DiskLruCache的edit()返回editor
    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      //把url进行 md5(),并转换成十六进制格式
      //将转换后的key作为DiskLruCache内部LinkHashMap的键值
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }

      //用editor提供的Okio的sink对文件进行写入
      entry.writeTo(editor);
      //利用CacheRequestImpl写入body
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }

根据上面的代码发现,OkHttp只针对GET请求时返回的response进行缓存。官方解释:非GET请求下返回的response也可以进行缓存,但是这样做的复杂性高,且效益低。 在获取DiskLruCache.Editor对象editor后,调用writeTo()把url、请求方法、响应首部字段等写入缓存,然后返回一个CacheRequestImpl实例,在CacheInterceptor的intercept()方法内部调用cacheWritingResponse()写入body,最后调用CacheRequestImpl的close()完成提交(实际内部调用了Editor # commit() )。
首先进入DiskLruCache的edit()方法。

  public @Nullable Editor edit(String key) throws IOException {
    return edit(key, ANY_SEQUENCE_NUMBER);
  }

  synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    //内部主要是利用FileSystem处理文件,如果这里出现了异常,
    //在最后会构建新的日志文件,如果文件已存在,则替换
    initialize();
    //检测缓存是否已关闭
    checkNotClosed();
    //检测是否为有效key
    validateKey(key);
    //lruEntries是LinkHashMap的实例,先查找lruEntries是否存在
    Entry entry = lruEntries.get(key);
     
    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
        || entry.sequenceNumber != expectedSequenceNumber)) {
      return null; // Snapshot is stale.
    }

    //如果有Editor在操作entry,返回null
    if (entry != null && entry.currentEditor != null) {
      return null; 
    }
    //如果需要,进行clean操作
    if (mostRecentTrimFailed || mostRecentRebuildFailed) {    
      executor.execute(cleanupRunnable);
      return null;
    }

    // 把当前key在对应文件中标记DIRTY状态,表示正在修改,
    //清空日志缓冲区,防止泄露
    journalWriter.writeUtf8(DIRTY).writeByte(' ').writeUtf8(key).writeByte('\n');
    journalWriter.flush();

    if (hasJournalErrors) {
      return null; // 如果日志文件不能编辑
    }
    
    //为请求的url创建一个新的DiskLruCache.Entry实例
    //并放入lruEntries中
    if (entry == null) {
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }
    
    Editor editor = new Editor(entry);
    entry.currentEditor = editor;
    return editor;
  }

在最后一步创建DiskLruCache.Entry实例entry ,这个entry本身是不存放任何数据的,主要是维护key(请求url)对应的文件列表,且内部currentEditor不为null,表示当前entry处于编辑状态。这一步返回editor后,前面已经了解到会调用Cache.Entry的writeTo()对返回的editor进行操作。

Cache.Entry # writeTo()

public void writeTo(DiskLruCache.Editor editor) throws IOException {
      BufferedSink sink = Okio.buffer(editor.newSink(ENTRY_METADATA));

      sink.writeUtf8(url)
          .writeByte('\n');
      sink.writeUtf8(requestMethod)
          .writeByte('\n');
      sink.writeDecimalLong(varyHeaders.size())
          .writeByte('\n');
     
      //... ...省略,都是利用sink进行写入操作
      sink.close();
    }

在上面的代码,通过editor.newSink()为上层Cache.Entry提供了一个sink ,然后进行文件写入操作。这里只是把url、请求方法、首部字段等写入缓存,并未写入reponse的body内容。到这里,put()方法已经结束。那么,对于response的body的写入在哪里?前面也提到过,在CacheInterceptor的intercept()方法内部调用cacheWritingResponse()写入body。

@Override 
public Response intercept(Chain chain) throws IOException {
    ... ... //省略代码

    if (cache != null) {
      if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        CacheRequest cacheRequest = cache.put(response);
        //在内部实现了response的body的写入
        return cacheWritingResponse(cacheRequest, response);
      }
    ... ... //省略代码
)
put过程.png

Cache.get()

  @Nullable 
  Response get(Request request) {
    //把url转换成key
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;
    try {
      //通过DiskLruCache的get()根据具体的key获取DiskLruCache.Snapshot实例
      snapshot = cache.get(key);
      if (snapshot == null) {
        return null;
      }
    } catch (IOException e) {
      // Give up because the cache cannot be read.
      return null;
    }

    try {
      //通过snapshot.getSource()获取一个Okio的Source
      entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    } catch (IOException e) {
      Util.closeQuietly(snapshot);
      return null;
    }
    
    //根据snapshot获取缓存中的response
    Response response = entry.response(snapshot);

    if (!entry.matches(request, response)) {
      Util.closeQuietly(response.body());
      return null;
    }

    return response;
  }

相比于put过程,get过程相对简单点。DiskLruCache.Snapshot是DiskLruCache.Entry的一个快照值,内部封装了DiskLruCache.Entry对应文件的Source,简单的说:根据条件从DiskLruCache.Entry找到相应的缓存文件,并生成Source,封装在Snapshot内部,然后通过snapshot.getSource()获取Source,对缓存文件进行读取操作。

//DiskLruCache # get()
public synchronized Snapshot get(String key) throws IOException {
    initialize();

    checkNotClosed();
    validateKey(key);
    //从lruEntries查找entry,
    Entry entry = lruEntries.get(key);
    if (entry == null || !entry.readable) return null;
    
    //得到Entry的快照值snapshot
    Snapshot snapshot = entry.snapshot();
    if (snapshot == null) return null;

    redundantOpCount++;
    journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');

    //如果redundantOpCount超过2000,且超过lruEntries的大小时,进行清理操作
    if (journalRebuildRequired()) {
      executor.execute(cleanupRunnable);
    }

    return snapshot;
  }

//DiskLruCache.Entry # snapshot()
Snapshot snapshot() {
      if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();

      Source[] sources = new Source[valueCount];
      // Defensive copy since these can be zeroed out.
      long[] lengths = this.lengths.clone(); 
      try {
         //遍历已缓存的文件,生成相应的sources
        for (int i = 0; i < valueCount; i++) {
          sources[i] = fileSystem.source(cleanFiles[i]);
        }
        //创建Snapshot并返回
        return new Snapshot(key, sequenceNumber, sources, lengths);
      } catch (FileNotFoundException e) {
        // A file must have been deleted manually!
        for (int i = 0; i < valueCount; i++) {
          if (sources[i] != null) {
            Util.closeQuietly(sources[i]);
          } else {
            break;
          }
        }
        // Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache
        // size.)
        try {
          removeEntry(this);
        } catch (IOException ignored) {
        }
        return null;
      }
    }

Cache.Entry # response()

public Response response(DiskLruCache.Snapshot snapshot) {
      String contentType = responseHeaders.get("Content-Type");
      String contentLength = responseHeaders.get("Content-Length");
      Request cacheRequest = new Request.Builder()
          .url(url)
          .method(requestMethod, null)
          .headers(varyHeaders)
          .build();
      return new Response.Builder()
          .request(cacheRequest)
          .protocol(protocol)
          .code(code)
          .message(message)
          .headers(responseHeaders)
          .body(new CacheResponseBody(snapshot, contentType, contentLength))
          .handshake(handshake)
          .sentRequestAtMillis(sentRequestMillis)
          .receivedResponseAtMillis(receivedResponseMillis)
          .build();
    }

总结:

经过分析OkHttp源码,可以知道:Cache只是一个上层的执行者,内部真正的缓存是由DiskLruCache实现的。在DiskLruCache里面通过FileSystem,基于Okio的Sink/Source对文件进行流操作。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335

推荐阅读更多精彩内容