“图”解OKHttp系列文章二、ChcheInterceptor

352 阅读10分钟

“图”解OKHttp这个系列本来的想法是尽量只通过流程图来直观的展示整个OKHttp,遗憾的是准备动笔写CacheInterceptor这个拦截器的时候才发现需要解释的细节太多,紧靠一张图的话很难说明白这个事。于是本篇在最难理解的部分使用的流程图辅助注释的方式,其他容易理解的部分使用了Code加注释的部分。 首先我们先看一下整个intercept过程:

override fun intercept(chain: Interceptor.Chain): Response {

    val call = chain.call()

    val cacheCandidate = cache?.get(chain.request())

 

    val now = System.currentTimeMillis()

 

    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()

    val networkRequest = strategy.networkRequest

    val cacheResponse = strategy.cacheResponse

 

    cache?.trackResponse(strategy)

    val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE

 

    if (cacheCandidate != null && cacheResponse == null) {

      // The cache candidate wasn't applicable. Close it.

      cacheCandidate.body?.closeQuietly()

    }

 

    // If we're forbidden from using the network and the cache is insufficient, fail.

    //没有网络请求也没有缓存(networkRequest == null && cacheResponse == null),直接返回504

    if (networkRequest == null && cacheResponse == null) {

      return Response.Builder()

          .request(chain.request())

          .protocol(Protocol.HTTP_1_1)

          .code(HTTP_GATEWAY_TIMEOUT)

          .message("Unsatisfiable Request (only-if-cached)")

          .body(EMPTY_RESPONSE)

          .sentRequestAtMillis(-1L)

          .receivedResponseAtMillis(System.currentTimeMillis())

          .build().also {

            listener.satisfactionFailure(call, it)

          }

    }

 

    // If we don't need the network, we're done.

    //缓存有效,直接读取缓存

    if (networkRequest == null) {

      return cacheResponse!!.newBuilder()

          .cacheResponse(stripBody(cacheResponse))

          .build().also {

            listener.cacheHit(call, it)

          }

    }

 

    if (cacheResponse != null) {

      listener.cacheConditionalHit(call, cacheResponse)

    } else if (cache != null) {

      listener.cacheMiss(call)

    }

 

    var networkResponse: Response? = 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) {

        cacheCandidate.body?.closeQuietly()

      }

    }

 

    // If we have a cache response too, then we're doing a conditional get.

    if (cacheResponse != null) {

      //如果服务器返回304无修改,那就合并缓存的响应头和网络响应的响应头,并修改发送时间、接收时间等数据后作为本次请求的响应返回

      if (networkResponse?.code == HTTP_NOT_MODIFIED) {

        val 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()

 

        // Update the cache after combining headers but before stripping the

        // Content-Encoding header (as performed by initContentStream()).

        cache!!.trackConditionalCacheHit()

        cache.update(cacheResponse, response)

        return response.also {

          listener.cacheHit(call, it)

        }

      } else {

        cacheResponse.body?.closeQuietly()

      }

    }

 

    //有网络请求,没有缓存(networkRequest != null && cacheResponse == null)。说明缓存不可用 那就使用网络请求的响应

    val response = networkResponse!!.newBuilder()

        .cacheResponse(stripBody(cacheResponse))

        .networkResponse(stripBody(networkResponse))

        .build()

 

    //进行写入缓存

    if (cache != null) {

      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {

        // Offer this request to the cache.

        val cacheRequest = cache.put(response)

        return cacheWritingResponse(cacheRequest, response).also {

          if (cacheResponse != null) {

            // This will log a conditional cache miss only.

            listener.cacheMiss(call)

          }

        }

      }

 

      if (HttpMethod.invalidatesCache(networkRequest.method)) {

        try {

          cache.remove(networkRequest)

        } catch (_: IOException) {

          // The cache cannot be written.

        }

      }

    }

 

    return response

  }

关键的地方我都加了注释,相信你通过注释已经可以对这个缓存的处理过程有了一个大体的了解了。那么解下来我们就来重点分析几个比较复杂的步骤,首先我们来看
val cacheCandidate = cache?.get(chain.request())

这个cacheCandidate 是怎么获得的呢,由于整个流程比较的复杂,这里我画了一张流程图帮助大家理解,流程图还是相当的详细的,配合下面的注释,相信大家对整个过程会有一定的理解。

1.png

流程图中使用的缓存用例的democode我在文末也有给出,大家感兴趣的可以先瞅一眼。

注释1:

我们来看看Entry是怎么只通过一个key值来关联缓存信息的:

init {

      // The names are repetitive so re-use the same builder to avoid allocations.

      val fileBuilder = StringBuilder(key).append('.')

      val truncateTo = fileBuilder.length

      for (i in 0 until valueCount) {

        fileBuilder.append(i)

        cleanFiles += directory / fileBuilder.toString()

        fileBuilder.append(".tmp")

        dirtyFiles += directory / fileBuilder.toString()

        fileBuilder.setLength(truncateTo)

      }

   }

可以看到在构造方法里面初始化了cleanFile和dirtyFile变量,可以看到二者分别有valueCount个,命名的规则为cleanFile:key + “.” + i,dirtyFile:key+”.”+i+”temp”.那么这些文件里面又存了些什么呢,我们以valueCount为默认值2为例,文件的内容上图中已经给出,可以看到*.0文件里面存的是响应头,*.1文件中存的是响应体。这样就把key值和响应缓存对应起来了。而这里的key值又是和request的url是对应的,这样就等于是把url和缓存的响应对应了起来。\

fun key(url: HttpUrl): String = url.toString().encodeUtf8().md5().hex()

注释2:

我们这里先贴一下DiskLruCache中整个get过程源码:

operator fun get(key: String): Snapshot? {

    initialize()

 

    checkNotClosed()

    validateKey(key)

    val entry = lruEntries[key] ?: return null

    val snapshot = entry.snapshot() ?: return null

 

    redundantOpCount++

    journalWriter!!.writeUtf8(READ)

        .writeByte(' '.toInt())

        .writeUtf8(key)

        .writeByte('\n'.toInt())

    if (journalRebuildRequired()) {

      cleanupQueue.schedule(cleanupTask)

    }

 

    return snapshot

  }

这里涉及的到的代码为:

val snapshot = entry.snapshot() ?: return null

我们重点分析一下这个方法:

internal fun snapshot(): Snapshot? {

      this@DiskLruCache.assertThreadHoldsLock()

 

      if (!readable) return null

      if (!civilizedFileSystem && (currentEditor != null || zombie)) return null

 

      val sources = mutableListOf<Source>()

      val lengths = this.lengths.clone() // Defensive copy since these can be zeroed out.

      try {

        for (i in 0 until valueCount) {

          sources += newSource(i)

        }

        return Snapshot(key, sequenceNumber, sources, lengths)

      } catch (_: FileNotFoundException) {

        // A file must have been deleted manually!

        for (source in sources) {

          source.closeQuietly()

        }

        // Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache

        // size.)

        try {

          removeEntry(this)

        } catch (_: IOException) {

        }

        return null

      }

    }

 

    private fun newSource(index: Int): Source {

      val fileSource = fileSystem.source(cleanFiles[index])

      if (civilizedFileSystem) return fileSource

 

      lockingSourceCount++

      return object : ForwardingSource(fileSource) {

        private var closed = false

        override fun close() {

          super.close()

          if (!closed) {

            closed = true

            synchronized(this@DiskLruCache) {

              lockingSourceCount--

              if (lockingSourceCount == 0 && zombie) {

                removeEntry(this@Entry)

              }

            }

          }

        }

      }

}

可以看到这里根据entry中的相关信息构造除了一个Snapshot,而这个获取的信息最重要的就两个,一个是key,这个作为标志变量不用多说,另一个就是sources,这是一个经过封装的输入流,感兴趣的可以研究一下okio这个开源的流框架,这里不细说,只要知道这是一个输入流就可以了。从newSource这个方法可以看出,这里source的输出源就是我们注释2里面提到的两个cleanFile。这里将source传递给Snapshot,实际上就是把缓存信息传递给了Snapshot。

注释3:

终于到了临门一脚了,废话不多说先放上关键代码,Cache类中的get方法:

internal fun get(request: Request): Response? {

    val key = key(request.url)

    val snapshot: DiskLruCache.Snapshot = try {

      cache[key] ?: return null

    } catch (_: IOException) {

      return null // Give up because the cache cannot be read.

    }

 

    val entry: Entry = try {

      Entry(snapshot.getSource(ENTRY_METADATA))

    } catch (_: IOException) {

      snapshot.closeQuietly()

      return null

    }

 

    val response = entry.response(snapshot)

    if (!entry.matches(request, response)) {

      response.body?.closeQuietly()

      return null

    }

 

    return response

  }

我们直接看关键的这两句:

......

Entry(snapshot.getSource(ENTRY_METADATA(值为0)))

......

val response = entry.response(snapshot);

Entry相应的构造方法:

@Throws(IOException::class) constructor(rawSource: Source) {

      rawSource.use {

        val source = rawSource.buffer()

        val urlLine = source.readUtf8LineStrict()

        // Choice here is between failing with a correct RuntimeException

        // or mostly silently with an IOException

        url = urlLine.toHttpUrlOrNull() ?: throw IOException("Cache corruption for $urlLine").also {

          Platform.get().log("cache corruption", WARN, it)

        }

        requestMethod = source.readUtf8LineStrict()

        val varyHeadersBuilder = Headers.Builder()

        val varyRequestHeaderLineCount = readInt(source)

        for (i in 0 until varyRequestHeaderLineCount) {

          varyHeadersBuilder.addLenient(source.readUtf8LineStrict())

        }

        varyHeaders = varyHeadersBuilder.build()

 

        val statusLine = StatusLine.parse(source.readUtf8LineStrict())

        protocol = statusLine.protocol

        code = statusLine.code

        message = statusLine.message

        val responseHeadersBuilder = Headers.Builder()

        val responseHeaderLineCount = readInt(source)

        for (i in 0 until responseHeaderLineCount) {

          responseHeadersBuilder.addLenient(source.readUtf8LineStrict())

        }

        val sendRequestMillisString = responseHeadersBuilder[SENT_MILLIS]

        val receivedResponseMillisString = responseHeadersBuilder[RECEIVED_MILLIS]

        responseHeadersBuilder.removeAll(SENT_MILLIS)

        responseHeadersBuilder.removeAll(RECEIVED_MILLIS)

        sentRequestMillis = sendRequestMillisString?.toLong() ?: 0L

        receivedResponseMillis = receivedResponseMillisString?.toLong() ?: 0L

        responseHeaders = responseHeadersBuilder.build()

 

        if (isHttps) {

          val blank = source.readUtf8LineStrict()

          if (blank.isNotEmpty()) {

            throw IOException("expected \"\" but was \"$blank\"")

          }

          val cipherSuiteString = source.readUtf8LineStrict()

          val cipherSuite = CipherSuite.forJavaName(cipherSuiteString)

          val peerCertificates = readCertificateList(source)

          val localCertificates = readCertificateList(source)

          val tlsVersion = if (!source.exhausted()) {

            TlsVersion.forJavaName(source.readUtf8LineStrict())

          } else {

            TlsVersion.SSL_3_0

          }

          handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates)

        } else {

          handshake = null

        }

      }

    }

首先明确这个source的输入源是什么

snapshot.getSource(ENTRY_METADATA(值为0)

不难知道这个输入源对应的是注释1提到的*.0文件,里面存储的是响应头相关信息。搞清楚了输入源是什么之后,理解后面的代码就变的简单了,通过不断的读取*.0文件中的数据,对与响应头相关的成员变量进行初始化;

继续看第二行:

val response = entry.response(snapshot);

继续贴上response方法:

fun response(snapshot: DiskLruCache.Snapshot): Response {

      val contentType = responseHeaders["Content-Type"]

      val contentLength = responseHeaders["Content-Length"]

      val cacheRequest = Request.Builder()

        .url(url)

        .method(requestMethod, null)

        .headers(varyHeaders)

        .build()

      return Response.Builder()

        .request(cacheRequest)

        .protocol(protocol)

        .code(code)

        .message(message)

        .headers(responseHeaders)

        .body(CacheResponseBody(snapshot, contentType, contentLength))

        .handshake(handshake)

        .sentRequestAtMillis(sentRequestMillis)

        .receivedResponseAtMillis(receivedResponseMillis)

        .build()

    }

可以看到,这个方法构造了一个response,其中响应头相关的变量来自刚刚初始化的成员变量,那么相应体呢,我们看这行:

.body(CacheResponseBody(snapshot, contentType, contentLength))

private class CacheResponseBody(

    val snapshot: DiskLruCache.Snapshot,

    private val contentType: String?,

    private val contentLength: String?

  ) : ResponseBody() {

    private val bodySource: BufferedSource

 

    init {

      val source = snapshot.getSource(ENTRY_BODY(值为1))

      bodySource = object : ForwardingSource(source) {

        @Throws(IOException::class)

        override fun close() {

          snapshot.close()

          super.close()

        }

      }.buffer()

    }

 

    override fun contentType(): MediaType? = contentType?.toMediaTypeOrNull()

 

    override fun contentLength(): Long = contentLength?.toLongOrDefault(-1L) ?: -1L

 

    override fun source(): BufferedSource = bodySource

  }

和我们预想的一样,这里通过val source = snapshot.getSource(ENTRY_BODY(值为1))

获取到了输入源为*.1文件的输入流,而*.1文件正式存储有响应体的文件。这样我们就可以随时通过这个source获取到responseBody了。至此响应头和响应体我们都已经拿到,返回构建完成的response即可。\

历经千辛万苦我们终于拿到了心心恋恋的cacheCandidate,那么是不是这个返回的cacheCandidate就可以直接用了呢,很遗憾答案是否定的,让我们回到梦开始的地方:

val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()

val networkRequest = strategy.networkRequest

val cacheResponse = strategy.cacheResponse

可以看到这里又通过CacheStrategy执行了一系列的缓存策略过滤,这里最终会调用CacheStrategy的computeCandidate方法来执行过滤策略:

private fun computeCandidate(): CacheStrategy {

      // No cached response.

      //如果没有缓存,进行网络请求

      if (cacheResponse == null) {

        return CacheStrategy(request, null)

      }

 

      //判断https请求。如果是https请求,但是没有握手信息,进行网络请求

      //okhttp会保存ssl握手信息 Handshake,如果这次发起的是https请求,但是已缓存的响应中没有握手信息,那么这个缓存不能用,发起网络请求

      //Drop the cached response if it's missing a required handshake.

      if (request.isHttps && cacheResponse.handshake == null) {

        return CacheStrategy(request, null)

      }

 

      // If this response shouldn't have been stored, it should never be used as a response source.

      // This check should be redundant as long as the persistence store is well-behaved and the

      // rules are constant.

      //判断响应码以及响应头。主要是通过响应码以及响应头的缓存控制字段判断响应能不能缓存,不能缓存那就进行网络请求

      if (!isCacheable(cacheResponse, request)) {

        return CacheStrategy(request, null)

      }

 

      val requestCaching = request.cacheControl

      //判断请求头。如果 请求包含:CacheControl:no-cache 需要与服务器验证缓存有效性

      // 或者请求头包含 If-Modified-Since:时间 值为lastModified或者data 如果服务器没有在该头部指定的时间之后修改了请求的数据,服务器返回304(无修改)

      // 或者请求头包含 If-None-Match:值就是Etag(资源标记)服务器将其与存在服务端的Etag值进行比较;如果匹配,返回304

      // 请求头中只要存在三者中任意一个,进行网络请求

      if (requestCaching.noCache || hasConditions(request)) {

        return CacheStrategy(request, null)

      }

 

      val responseCaching = cacheResponse.cacheControl

 

      // 缓存的响应从创建到现在的时间

      val ageMillis = cacheResponseAge()

      // 这个响应有效缓存的时长

      var freshMillis = computeFreshnessLifetime()

 

      if (requestCaching.maxAgeSeconds != -1) {

        //缓存资源, 但是在指定时间(单位为秒)后缓存过期

        freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))

      }

 

      var minFreshMillis: Long = 0

      if (requestCaching.minFreshSeconds != -1) {

        //缓存的资源至少要保持指定时间的新鲜期

        minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())

      }

 

      var maxStaleMillis: Long = 0

      if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {

        //指定时间内, 即使缓存过时, 资源依然有效

        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())

      }

 

      //响应存在的时间+缓存的资源至少要保持指定时间的新鲜期 小于 缓存有效时长+过期后还可以使用的时间

      if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {**

        val builder = cacheResponse.newBuilder()

        if (ageMillis + minFreshMillis >= freshMillis) {

          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")

        }

        val oneDayMillis = 24 * 60 * 60 * 1000L

        if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {

          builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"")

        }

        return CacheStrategy(null, builder.build())

      }

 

      // Find a condition to add to the request. If the condition is satisfied, the response body

      // will not be transmitted.

      //缓存过期了

      val conditionName: String

      val conditionValue: String?

      when {

        etag != null -> {

          conditionName = "If-None-Match"

          conditionValue = etag

        }

 

        lastModified != null -> {

          conditionName = "If-Modified-Since"

          conditionValue = lastModifiedString

        }

 

        servedDate != null -> {

          conditionName = "If-Modified-Since"

          conditionValue = servedDateString

        }

 

        else -> return CacheStrategy(request, null) // No condition! Make a regular request.

      }

 

      //如果设置了 If-None-Match/If-Modified-Since 服务器是可能返回304(无修改)的,这时使用缓存的响应

      val conditionalRequestHeaders = request.headers.newBuilder()

      conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)

 

      val conditionalRequest = request.newBuilder()

          .headers(conditionalRequestHeaders.build())

          .build()

      return CacheStrategy(conditionalRequest, cacheResponse)

}

重点的地方都在code中给了注释,基本上能理解响应存在的时间+缓存的资源至少要保持指定时间的新鲜期 小于 缓存有效时长+过期后还可以使用的时间就能理解整个缓存策略了。因为这里主要和http缓存协议相关,这里就不展开说了,感兴趣的可以搜一下http的缓存策略。 至此我们终于获得了可用的cacheResponse,接下来的流程都不复杂,已在一开始的注释中做了解释。

最后一个比较重要的流程就是将网络响应跟新或者存入缓存的过程,这里如果对缓存读取的流程已经熟悉的化,存入的过程还是好理解的,大体上也是先存响应头,后存响应体的过程,这里就不详细介绍了,大家可以自行阅读源码。

到这里关于OKHttp缓存的相关内容就结束了,总结起来就是就一张图和两段代码。一张图自然是介绍cache获取时的那张流程图,两段代码,一段是ChcheInterceptor类中的intercept()整个流程代码,另一段是CacheStrategy中与缓存策略相关的computeCandidate()方法。大家抓住这一张图两段代码,对ChcheInterceptor这个拦截器就很好理解了。

最后贴上缓存demo code:

String url = "http://publicobject.com/helloworld.txt";

File file = new File(getFilesDir(), "cache_test");

// 缓存大小

int cacheSize = 10 * 1024 * 1024;

OkHttpClient okHttpClient = new OkHttpClient().newBuilder()

        .cache(new Cache(file, cacheSize)) // 配置缓存

        .build();

CacheControl cacheControl = new CacheControl.Builder()

        .maxStale(10, TimeUnit.SECONDS)

        .maxAge(10, TimeUnit.SECONDS)

        .build();

final Request request = new Request.Builder()

        .url(url)

        .cacheControl(cacheControl)

        .build();

Call call = okHttpClient.newCall(request);

call.enqueue(new Callback() {

    @Override

    public void onFailure(Call call, IOException e) {

        e.printStackTrace();

    }



    @Override

    public void onResponse(Call  call, Response response) throws IOException {

        Message cmd = mHandler.obtainMessage(TEXT_UPDATE);

        cmd.obj = response.body().string();

        mHandler.sendMessage(cmd);

    }

});