OkHttp源码学习之Cache

199 阅读4分钟

前几天做了一个需求是关于打点的缓存以及上报,看起来跟okhttp的缓存使用差不多,我觉得这个需要详细看看OkHttp是怎么做的。

首先看一下构造函数,需要传入以下几个参数。

//directory - 可写目录。
//valueCount - 每个缓存条目的值数量。必须为正数。
//maxSize - 此缓存用于存储的最大字节数。
//fileSystem  -文件系统
constructor(
  fileSystem: FileSystem, directory: Path, maxSize: Long,) : this(
  directory,
  maxSize,
  fileSystem,
  TaskRunner.INSTANCE,
)

什么时候存入缓存?

如果知道缓存是请求的响应体的缓存,那么就知道肯定是在返回响应时,进行缓存,调用put()方法的地方只有一个。

class CacheInterceptor(internal val cache: Cache?) : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    if (cache != null) {
      val cacheNetworkRequest = networkRequest.requestForCache()

      if (response.promisesBody() && CacheStrategy.isCacheable(response, cacheNetworkRequest)) {
        // Offer this request to the cache.
        val cacheRequest = cache.put(response.newBuilder().request(cacheNetworkRequest).build())
        return cacheWritingResponse(cacheRequest, response).also {
          if (cacheResponse != null) {
            // This will log a conditional cache miss only.
            listener.cacheMiss(call)
          }
        }
      }
  }
}

怎么存入缓存?

那是怎么缓存的呢?搜索Cache类里,放入缓存的主要通过put()方法,具体如下:

internal fun put(response: Response): CacheRequest? {
  val requestMethod = response.request.method 

  //判断网络请求方式(get/post/put/move等)
  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
  }

  //以*开头的请求头无法缓存
  if (response.hasVaryAll()) {
    return null
  }

  //生成缓存实体
  val entry = Entry(response)
  var editor: DiskLruCache.Editor? = null
  try {
    //以request.url为缓存名字,写入到缓存中
    editor = cache.edit(key(response.request.url)) ?: return null
    entry.writeTo(editor)
    return RealCacheRequest(editor)
  } catch (_: IOException) {
    abortQuietly(editor)
    return null
  }
}

什么时机写入呢?

class CacheInterceptor(internal val cache: Cache?) : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    if (cache != null) { //首先,检查是否存在缓存对象。如果存在,继续执行缓存逻辑。
      val cacheNetworkRequest = networkRequest.requestForCache()

      //判断HTTP响应是否包含主体  && 检查能否缓存(响应成功码)
      if (response.promisesBody() && CacheStrategy.isCacheable(response, cacheNetworkRequest)) {
        // Offer this request to the cache.
        val cacheRequest = cache.put(response.newBuilder().request(cacheNetworkRequest).build())
        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.
        }
      }
  }
}

可以看到上面的代码中,最重要的除了校验方法,就是缓存方法,稍微看下缓存方法里干了那些事情。

@Throws(IOException::class)
private fun cacheWritingResponse(
  cacheRequest: CacheRequest?,
  response: Response,
): Response {
  // Some apps return a null body; for compatibility we treat that like a null cache request.
  if (cacheRequest == null) return response
  val cacheBodyUnbuffered = cacheRequest.body()

  //获取response的源(body)并将其缓冲化。
  val source = response.body.source()
  val cacheBody = cacheBodyUnbuffered.buffer()

  //创建一个cacheWritingSource对象,它是一个实现了Source接口的匿名对象,用于从response中读取数据并写入cacheRequest
  val cacheWritingSource =
    object : Source {
      private var cacheRequestClosed = false

      @Throws(IOException::class)
      override fun read(
        sink: Buffer,
        byteCount: Long,
      ): Long {
        val bytesRead: Long
        try {
          bytesRead = source.read(sink, byteCount)
        } catch (e: IOException) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true
            cacheRequest.abort() // Failed to write a complete cache response.
          }
          throw e
        }

        if (bytesRead == -1L) {
          if (!cacheRequestClosed) {
            cacheRequestClosed = true
            cacheBody.close() // The cache response is complete!
          }
          return -1
        }

        sink.copyTo(cacheBody.buffer, sink.size - bytesRead, bytesRead)
        cacheBody.emitCompleteSegments()
        return bytesRead
      }

      override fun timeout(): Timeout = source.timeout()

      @Throws(IOException::class)
      override fun close() {
        //close方法在关闭source之前,如果cacheRequest未关闭,它会尝试丢弃并中止cacheRequest
        if (!cacheRequestClosed &&
          !discard(ExchangeCodec.DISCARD_STREAM_TIMEOUT_MILLIS, MILLISECONDS)
        ) {
          cacheRequestClosed = true
          cacheRequest.abort()
        }
        source.close()
      }
    }

  val contentType = response.header("Content-Type")
  val contentLength = response.body.contentLength()
  //使用cacheWritingSource和新的内容类型和内容长度构建一个新的Response对象
  return response.newBuilder()
    .body(RealResponseBody(contentType, contentLength, cacheWritingSource.buffer()))
    .build()
}

什么时候读缓存?

通过源码我们可以看到调用缓存的地方在interceptor,通过intercep接口,调用到具体实现类 CacheInterceptor.

 @Throws(IOException::class)
  override fun proceed(request: Request): Response {
    check(index < interceptors.size)
     "拦截器索引超出拦截器列表范围"
    calls++

    if (exchange != null) {
      check(exchange.finder.routePlanner.sameHostAndPort(request.url)) {
      "网络拦截器必须保留相同的host和port"
      }
      check(calls == 1) {
         "网络拦截器必须恰好调用一次proceed()"
      }
    }

    // Call the next interceptor in the chain.
    val next = copy(index = index + 1, request = request)
    val interceptor = interceptors[index]

    @Suppress("USELESS_ELVIS")
    val response =
      interceptor.intercept(next) ?: throw NullPointerException(
         "拦截器返回了null",
      )

    if (exchange != null) {
      check(index + 1 >= interceptors.size || next.calls == 1) {
        "网络拦截器必须恰好调用一次proceed()"
      }
    }
    return response
  }

怎么读读缓存?

在实现类CacheInterceptor中,首先检查有无缓存。

class CacheInterceptor(internal val cache: Cache?) : Interceptor {
  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    //先检查缓存
    val cacheCandidate = cache?.get(chain.request().requestForCache())

    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.
    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)")
        .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(cacheResponse.stripBody())
        .build().also {
          listener.cacheHit(call, it)
        }
    }

    if (cacheResponse != null) {
      listener.cacheConditionalHit(call, cacheResponse)
    } else if (cache != null) {
      listener.cacheMiss(call)
    }
    ......
  } 
}

get()方法是怎么取缓存的呢?

internal fun get(request: Request): Response? {
  // 使用key函数生成缓存键  url.toString().encodeUtf8().md5().hex()
  val key = key(request.url)
  // 尝试从缓存中获取与键关联的Snapshot
  val snapshot: DiskLruCache.Snapshot =
    try {
      cache[key] ?: return null
    } catch (_: IOException) {
      return null // Give up because the cache cannot be read.
    }
    
  // 使用Snapshot创建Entry对象
  val entry: Entry =
    try {
      Entry(snapshot.getSource(ENTRY_METADATA))
    } catch (_: IOException) {
      snapshot.closeQuietly()
      return null
    }
  // 使用Entry创建Response对象
  val response = entry.response(snapshot)
  // 检查Entry是否与请求和响应匹配
  if (!entry.matches(request, response)) {
    response.body.closeQuietly()
    return null
  }

  return response
}

总结

最后用一张图来总结一下,整个okhttp的缓存逻辑。

image.png