OkHttp3源码解析内部缓存

作者: Gillben | 来源:发表于2018-05-22 22:06 被阅读0次

    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对文件进行流操作。

    相关文章

      网友评论

        本文标题:OkHttp3源码解析内部缓存

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