OkHttp 源码阅读笔记(四)

OkHttp 源码阅读笔记(四)

第一篇文章中介绍了 OkHttp 的同步调用和异步调用,Dispatcher 的任务调度器工作方式和 RealInterceptorChain 拦截器链的工作方式:OkHttp 源码阅读笔记(一)

第二篇文章中介绍了 OkHttp 如何从缓存中获取链接,如何创建链接以及 ConnectionPool 的工作原理:OkHttp 源码阅读笔记(二)

第三篇文章中介绍了 OkHttp 中的系统拦截器 RetryAndFollowUpInterceptorBridgeInterceptorOkHttp 源码阅读笔记(三)

本篇文章是系列文章的第四篇,主要介绍剩下没有介绍的系统拦截器。

CacheInterceptor

CacheInterceptor 是用来处理 Http 协议的缓存的,默认情况下 OkHttp 是没有支持缓存的,可以在构建 OkhttpClient 的时候设置一个 Cache 对象就可以支持缓存了,Cache 对象中可以指定缓存文件的目录和缓存文件的最大值。CacheIntercptor 只会缓存 GET 请求同时满足缓存条件控制的 Response

OK,现在来看看 CacheInterceptor 是如何处理缓存的,同样的我们以它的 intercept() 方法作为入口函数:

Kotlin 复制代码
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    // 获取缓存的 Response
    val cacheCandidate = cache?.get(chain.request())

    val now = System.currentTimeMillis()
    
    // 通过 Request 和缓存的 Response 计算出新的请求的 Request 和 缓存的 Response
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    // 新的请求 Request,如果它为空就表示不需要请求网络
    val networkRequest = strategy.networkRequest
    // 处理后的缓存的 Response,如果它为空就表示不能使用缓存
    val cacheResponse = strategy.cacheResponse

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

    // 表示无法使用缓存中的 Response,把其中的 ResponseBody 关闭。
    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.
    // 这种情况表示 Request 中希望只要缓存的数据,但是缓存的数据并没有找到,这里就会构建一个描述这个异常的 Response 直接返回。
    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 {
            // 通知 eventListener 获取缓存失败
            listener.satisfactionFailure(call, it)
          }
    }

    // If we don't need the network, we're done.
    // 这种情况下表示缓存可用而且不需要重新请求网络验证,直接把缓存的 Response 返回
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build().also {
            // 通知 eventListener 命中缓存
            listener.cacheHit(call, it)
          }
    }

    if (cacheResponse != null) {
      // 通知 eventListener 命中缓存,但是需要再次请求网络验证缓存是否可用
      listener.cacheConditionalHit(call, cacheResponse)
    } else if (cache != null) {
      // 通知 eventListener 未命中缓存
      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.
    // 这里表示有等待验证的缓存 Response
    if (cacheResponse != null) {
      // ResponseCode 返回 304 就表示原来的缓存是可以继续使用的,反之不可以使用
      if (networkResponse?.code == HTTP_NOT_MODIFIED) {
        // 通过缓存 Response 构建一个新的 Response
        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 {
          // 通知 eventListener 命中缓存
          listener.cacheHit(call, it)
        }
      } else {
        // 缓存的 Response 不可用,关闭 ResponseBody。
        cacheResponse.body?.closeQuietly()
      }
    }
    
    // 后续的逻辑就是没有缓存和缓存不可用的处理逻辑了。  
    
    val response = networkResponse!!.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build()

    if (cache != null) {
      // 判断当前 Response 是否可以缓存。
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // Offer this request to the cache.
        // 写入到 Cache 中
        val cacheRequest = cache.put(response)
        return cacheWritingResponse(cacheRequest, response).also {
          if (cacheResponse != null) {
            // This will log a conditional cache miss only.
            // 通知 eventListener 未命中缓存
            listener.cacheMiss(call)
          }
        }
      }
      // 验证是否可以缓存
      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
          // 移除缓存
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }

    return response
  }

上面的代码主要逻辑可以分为以下几步:

  1. Cache 中获取缓存的 Response

  2. 通过 Request 和缓存的 Response 计算 CacheStrategyCacheStrategy 非常重要,它来控制后续是否需要网络请求,缓存是否能用等等,我们后面要重点分析它的关键代码。

  3. 如果请求只允许缓存的数据,同时又需要网络验证,就构建一个描述该错误的 Response 返回。

  4. 如果不需要请求网络,同时缓存可用,直接返回缓存的 Response

  5. 请求网络。

  6. 如果有等待验证的缓存,同时服务端返回 304 表示缓存可用,就把缓存的 Response 返回,同时更新 Cache

  7. 后续就是普通没有缓存或者缓存过期的处理逻辑了,判断 Response 是否可以缓存,如果可以缓存到 Cache 中,然后把网络的 Response 返回。

基于上面的代码我们再分析分析 CacheStrategy 关键逻辑和 Cache 的关键逻辑。

CacheStrategy

CacheStrategy 是通过 CacheStrategy.Factory#compute() 方法计算的,我们也以它为入口函数分析:

Kotlin 复制代码
    fun compute(): CacheStrategy {
      val candidate = computeCandidate()

      // We're forbidden from using the network and the cache is insufficient.
      // 没有找到缓存,同时请求也要缓存,就把 networkRequest 和 cacheResponse 都设置为空
      if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
        return CacheStrategy(null, null)
      }

      return candidate
    }

通过 computeCandidate() 方法计算 CacheStrategy,如果获取到的缓存需要网络验证同时又只使用缓存,那么就会把 networkRequestcacheResponse 都设置为空(也就是我们分析 intercept() 方法中的第二点)。

我们继续看 computeCandidate() 方法(以下的方法有许多的 Http 协议缓存控制相关的内容,不熟悉的同学建议网络上找找相关资料):

Kotlin 复制代码
    private fun computeCandidate(): CacheStrategy {
      // No cached response.
      // 没有缓存,直接请求网络
      if (cacheResponse == null) {
        return CacheStrategy(request, null)
      }

      // Drop the cached response if it's missing a required handshake.
      // 有缓存,但是 https 协议的 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.
      // 通过 Response 和 Reqeust 判断,不能够使用缓存,直接请求网络。
      if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }

      val requestCaching = request.cacheControl
      // Request 中的缓存控制如果有 no-cache 或者请求头有缓存控制的相关的条件,直接请求网络。
      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())
      }
      // 如果 Response 中缓存控制没有 no-cache,表示服务器可以接受不验证缓存。同时缓存的年纪没有达到最大的限制。这样就直接使用缓存且不验证服务器。
      if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
        val builder = cacheResponse.newBuilder()
        // 如果时间超过 freshMillis,但是没有超过 maxStaleMillis 限制,添加警告 header
        if (ageMillis + minFreshMillis >= freshMillis) {
          builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"")
        }
        val oneDayMillis = 24 * 60 * 60 * 1000L
        // 如果缓存的时间超过一天,添加警告 header
        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?
      // 计算缓存控制 header,也就是 If-None-Match 和 If-Modified-Since
      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.
      }

      val conditionalRequestHeaders = request.headers.newBuilder()
      conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
      // 添加缓存控制相关的 Request Header
      val conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build()
      // 缓存需要向服务端确认是否可用    
      return CacheStrategy(conditionalRequest, cacheResponse)
    }

这里先简单描述下 CacheStrategynetworkRequestcacheResponse 的空状态分别表示什么意思:

  1. networkRequestcacheResponse 都为空。

    表示没有获取到缓存,同时也无法请求网络,也就是前面分析到的 Request Header 中有 only-if-cache 的,表示只要缓存的数据,但是缓存数据不可用。

  2. networkRequest 不为空,cacheResponse 为空。

    表示缓存不可用,需要直接请求服务器。

  3. networkRequest 为空,cacheResponse 不为空。

    表示缓存可以直接使用,不需要向服务器验证。

  4. networkRequestcacheResponse 都不为空。 表示虽然有缓存,但是需要向服务器验证是否可用。

在了解了 CacheStrategy 的各种状态后,我们再看看源码的处理流程:

  1. 没有缓存,直接请求网络。

  2. 有缓存,但是 https 协议的 handshake 缺失,直接请求网络。

  3. 通过 ResponseReqeust 调用 isCacheable() 方法(后面分析)判断是否能够使用缓存,不能够使用缓存,直接请求网络。

  4. Request 中的缓存控制如果有 no-cache 或者请求头有缓存控制的相关的条件(也就是有 If-None-Match 或者 If-Modified-Since),直接请求网络。

  5. 如果 Response 中缓存控制没有 no-cache(这里注意在 Responseno-cache 表示可以使用缓存但是使用前必须要向服务器再验证一次,no-store 表示不允许使用缓存。这个命名让人有点懵逼。),表示服务器可以接受不验证缓存。同时缓存的年纪没有达到最大的限制。这样就直接使用缓存且不验证服务器。

  6. 剩下的情况就是有缓存且需要向服务器验证,在请求的 Request 中添加验证缓存相关的 HeaderIf-None-Match 或者 If-Modified-Since)。

我们再来看看 isCacheable() 方法的实现:

Kotlin 复制代码
    fun isCacheable(response: Response, request: Request): Boolean {
      // Always go to network for uncacheable response codes (RFC 7231 section 6.1), This
      // implementation doesn't support caching partial content.
      when (response.code) {
        HTTP_OK,
        HTTP_NOT_AUTHORITATIVE,
        HTTP_NO_CONTENT,
        HTTP_MULT_CHOICE,
        HTTP_MOVED_PERM,
        HTTP_NOT_FOUND,
        HTTP_BAD_METHOD,
        HTTP_GONE,
        HTTP_REQ_TOO_LONG,
        HTTP_NOT_IMPLEMENTED,
        StatusLine.HTTP_PERM_REDIRECT -> {
          // These codes can be cached unless headers forbid it.
        }

        HTTP_MOVED_TEMP,
        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) {
            return false
          }
        }

        else -> {
          // 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
    }
  }

上面代码比较简单分为以下情况

  • 200,203,204,300,301,404,405,410,414,501,308

    需要 Request HeaderResponse Header 中没有 no-store 才可以缓存。

  • 302,307

    如果 Response Header 中没有 Expiresmax-age, publicprivate,禁止缓存;其他情况需要 Request HeaderResponse Header 中没有 no-store 才可以缓存。

  • 其他情况禁止缓存

Cache

更加具体的缓存原理我就没有贴代码了,这里只是简单的看看 get()put() 方法。

get()

Kotlin 复制代码
  internal fun get(request: Request): Response? {
    // 通过 url 的 md5 作为 key.
    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
  }

这里有一点比较有意思,Cache 存储 Response 的方式是分两部分存储:除 Response Body 外的部分存储为 ENTRY_METADATA; Response Body 存储为 ENTRY_BODY。在读取的时候 ENTRY_METADATA 是立即读取到 Response 中,而 ENTRY_BODY 就不是,需要等外部调用的地方真正读取的时候才去缓存文件中加载。获取到 ENTRY_METADATA 后,会去检查 Vary Response Header,如果 Vary 值为 *,禁止使用缓存,这个表示该 Response 的结果受 Request 外的参数影响。

put()

Kotlin 复制代码
  internal fun put(response: Response): CacheRequest? {
    val requestMethod = response.request.method

    if (HttpMethod.invalidatesCache(response.request.method)) {
      try {
        remove(response.request)
      } catch (_: IOException) {
        // The cache cannot be written.
      }
      return null
    }
    
    // 只缓存 GET 请求
    if (requestMethod != "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
    }
    
    // 检查 Vary 参数
    if (response.hasVaryAll()) {
      return null
    }

    val entry = Entry(response)
    var editor: DiskLruCache.Editor? = null
    try {
      editor = cache.edit(key(response.request.url)) ?: return null
      entry.writeTo(editor)
      return RealCacheRequest(editor)
    } catch (_: IOException) {
      abortQuietly(editor)
      return null
    }
  }

只是缓存 GET 请求,和 get() 方法一样,ENTRY_METADATA 的数据会被立马写入到本地缓存文件中,而 ENTRY_BODY 是只有在外部读取 Response Body 的时候才会被写入到本地缓存文件中。

最后

我还是低估了 CacheIntercptor 的复杂程度,本篇文章也是够长了,下一篇文章继续介绍系统的最后一个拦截器 CallServerInterceptor

相关推荐
长亭外的少年7 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿9 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神11 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛11 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法11 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter12 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快14 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl14 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
麦田里的守望者江14 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
Dnelic-14 小时前
解决 Android 单元测试 No tests found for given includes:
android·junit·单元测试·问题记录·自学笔记