探索OkHttp系列 (四) 缓存机制

前言

上一篇文章,我们介绍了BridgeInterceptor,这一篇文章我们就来介绍CacheInterceptor,它与OkHttp的缓存机制有关。

在介绍OkHttp的缓存机制之前,我们先来了解下Http的缓存机制。

Http的缓存机制

缓存主要指代理服务器或客户端的磁盘中保存的资源副本,通过缓存可以减少向源服务器的访问,从而提高效率。

缓存规则

为方便理解,我们认为客户端存在一个缓存数据库,用于存储缓存信息,并且不考虑代理服务器的存在。

在客户端第一次请求数据时,此时缓存数据库中没有对应的缓存数据,需要请求服务器,服务器返回后,将数据存储至缓存数据库中:

image-20211122160654923

我们将Http的缓存规则分为两大类:强制缓存,对比缓存,与缓存规则相关的信息,均包含在报文的Header中。这两类缓存规则可以同时存在,下面分别介绍这两类缓存规则。

强制缓存

假设缓存数据库存在缓存数据,仅基于强制缓存,请求数据的流程如下:

image-20211122161109564

可以看出,强制缓存如果生效,就不需要再和服务器发生交互。

强制缓存的实现依靠于ExpiresCache-Control这两个Header

Expires/Cache-Control

Expires 是 HTTP/1.0 所提供的对缓存的支持,通过这个 Header ,服务端可以告诉客户端缓存的过期时间,表示在过期时间内该资源都不会被更改,可以不用再向自己请求了。

例如:Expires: Mon, 22 Nov 2021 16:21:00 GMT 就标明了缓存的过期时间。

可以发现,它是由服务端生成的一个具体时间,浏览器之类的客户端应用会根据本地的时间与该具体时间对比,而客户端的时间可能跟服务端的时间有误差,这就会导致缓存命中的误差。

由于 Expires 存在上述的问题,因此在 HTTP/1.1 协议中引入了 Cache-Control 机制,通过这个 Header 可以在服务端与客户端之间沟通缓存信息。常见的缓存指令如下

说明
private 客户端可以缓存
public 客户端代理服务器都可以缓存
max-age=xxx 缓存数据在xxx秒后过期
no-cache 需要使用对比缓存来验证缓存数据

其中private是默认值。

Http/1.0版本,如果同时出现了 Cache-Control:max-age= 以及 Expiresmax-age指令会被忽略,以Expires为准;

Http/1.1版本,如果同时出现了 Cache-Control:max-age= 以及 ExpiresExpires会被忽略,以 max-age 为准。

对比缓存

假设缓存数据库存在缓存数据,仅基于对比缓存,请求数据的流程如下:

image-20211122164303152

可以看出,对比缓存不管是否生效,都需要与服务端发生交互。

浏览器第一次请求数据时,服务器会将缓存标识与数据一起返回给客户端,客户端将二者备份至缓存数据库中。
再次请求数据时,客户端将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断,若缓存资源仍有效,服务器会返回304状态码,通知客户端比较成功,可以使用缓存数据。

分为两种标识传递,分别是Last-Modified / If-Modified-SinceEtag / If-None-Match,下面我们分别介绍。

Last-Modified / If-Modified-Since

这两个字段需要配合 Cache-Control 进行使用,Last-Modified 位于响应头,If-Modified-Since 位于请求头。它们的含义分别是:

  • Last-Modified:该响应资源最后的修改时间,服务器在响应请求的时候可以填入该字段。
  • If-Modified-Since:客户端缓存过期时(max-age 到达),发现该资源具有 Last-Modified 字段,可以在 Header 中填入 If-Modified-Since 字段,并填入Last-Modified记录的时间。服务端收到该时间后会与该资源的最后修改时间进行比较。
    • 若该资源已经被修改 ,则会返回状态码200,并对整个资源响应。
    • 否则说明该资源在访问时未被修改,则会响应状态码 304,告知客户端可以使用缓存的资源。

Etag / If-None-Match

同样需要配合 Cache-Control 使用,Etag 位于响应头,If-None-Match 位于请求头。并且它们的优先级高于 Last-Modified/If-Modified-Since

它们的含义分别是:

  • Etag:请求的资源在服务器中的唯一标识,规则由服务器决定。
  • If-None-Match:若客户端在缓存过期时(max-age 到达),发现该资源具有 Etag 字段,就可以添加 If-None-Match Header,并传入 Etag 中的值,服务器收到请求后就会将If-None-Match的值与被请求资源的唯一标识进行比对
    • 若比对不同,说明资源有被改动过,则会返回状态码200,并响应整个资源内容
    • 若比对相同,说明资源没有新的修改,则会返回状态码 304,告知客户端可以继续使用缓存的资源。

总结

强制缓存和对比缓存可以同时存在,强制缓存优先级高于对比缓存,也就是说,当执行强制缓存的规则时,如果缓存生效,直接使用缓存,不再执行对比缓存规则。

当强制缓存和对比缓存同时存在时:

  1. 对于强制缓存,服务端通知客户端一个缓存时间,在缓存时间内客户端可以直接使用缓存的资源,不在缓存时间内,若客户端需要获取数据,则需要执行对比缓存策略。

  2. 对于比较缓存,客户端将缓存信息中的EtagLast-Modified通过请求发送给服务器,由服务器校验,若返回304状态码,则客户端可以使用缓存中的资源。

流程图如下

客户端第一次请求时:

image-20211122170912331

客户端再次请求时:

image-20211122171329923

OkHttp缓存机制

我们开始介绍CacheInterceptor拦截器。

CacheInterceptor

intercept

我们从它的intercept方法开始介绍

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val call = chain.call()
    // 以Request作为Key,获取候选缓存  
    val cacheCandidate = cache?.get(chain.request())

    val now = System.currentTimeMillis()

    // 根据「当前时间、请求、候选缓存」,创建缓存策略  
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    // 如果该请求不需要使用网络,那么networkRequest就为null  
    val networkRequest = strategy.networkRequest
    // 如果不存在该请求对应的缓存,那么cacheResponse为null  
    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.
    // 如果该请求不使用网络,并且没有对应的缓存,那么直接报错,返回状态码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和cacheResponse不能同时为null)  
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build().also {
            listener.cacheHit(call, it)
          }
    }

    /* 经过上面的两个If语句,代码执行到这里,说明networkRequest不为null,也就是该请求要使用网络 */  
      
    if (cacheResponse != null) {
      listener.cacheConditionalHit(call, cacheResponse)
    } else if (cache != null) {
      listener.cacheMiss(call)
    }

    // 表示网络请求的响应  
    var networkResponse: Response? = null
    try {
      // 进行网络请求,获取下一个拦截器返回的Response  
      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,说明之前的缓存数据有效,返回cacheResponse对应
      // 的缓存结果(HTTP_NOT_MODIFIED对应状态码304)
      if (networkResponse?.code == HTTP_NOT_MODIFIED) {
        val response = cacheResponse.newBuilder()
  			// 混合 缓存Response的Header 和 网络获取的Response的Header        
            .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()
      }
    }
      
    /* 在上面的一大段If语句中,若响应码为304,则缓存资源有效,返回缓存资源,若响应码不为304, */  
	/* 则表示缓存资源过期,关闭缓存资源 */
    
    // 读取网络请求的结果  
    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.
        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
  }

我们对它的intercept方法做个总结:

  1. RequestkeyCache中读取候选缓存
  2. 根据「当前时间,Request,候选缓存」构建一个缓存策略,用于判断当前请求是否需要使用网络,是否存在缓存
  3. 根据缓存策略,如果当前请求不使用网络且没有缓存,直接报错并返回状态码504
  4. 根据缓存策略,如果当前请求不使用网络且存在缓存,直接返回缓存数据
  5. 进行网络操作,将请求交给下面的拦截器处理,同时获得返回的Response
  6. 若通过网络返回的Response的状态码为304,混合缓存Response和网络返回的Response的请求头,更新缓存并返回缓存Response
  7. 读取网络返回的Response,判断是否需要缓存,如果需要则对Response进行缓存

缓存策略主要是根据CacheStrategy中的networkRequestcacheResponse来决定的:

networkRequest cacheResponse 对应处理
null null 直接报错,状态码返回504
null non-null 直接返回缓存Response
non-null null 返回网络获取的Response,满足缓存条件则缓存Response
non-null non-null 网络Response状态码若为304,则混合请求头后更新缓存,并返回缓存;若状态码为200,返回网络Response,满足缓存条件则缓存Response

Cache

CacheInterceptor中,有一个Cache对象,intercept方法就是基于这个对象,对Response进行CRUD操作的。

自定义Cache

OkHttp中,默认是不使用Cache的,解释如下:

CacheInterceptorRealCall::getResponseWithInterceptorChain中被构建

interceptors += CacheInterceptor(client.cache)

使用的是OkHttpClientcache对象,在OkHttpClient中,使用的是Buildercache

@get:JvmName("cache") val cache: Cache? = builder.cache

而在OkHttpClient.Builder中,其属性如下

internal var cache: Cache? = null

也就是说,CacheInterceptorcache默认是null的。

我们可以通过下面的方法,自己构建一个Cache,并且在构建OkHttpClient的时候传入

    val cacheFile = File(cachePath)
    val cacheSize = (10 * 1024 * 1024).toLong()
    val cache = Cache(cacheFile, cacheSize)
    val client: OkHttpClient = OkHttpClient.Builder()
                                           .cache(cache)
                                           .build()

这里调用了Cache下面的构造方法

constructor(directory: File, maxSize: Long) : this(directory, maxSize, FileSystem.SYSTEM)

调用了主构造方法

class Cache internal constructor(
  directory: File,
  maxSize: Long,
  fileSystem: FileSystem
) : Closeable, Flushable {
  internal val cache = DiskLruCache(
      fileSystem = fileSystem,
      directory = directory,
      appVersion = VERSION,
      valueCount = ENTRY_COUNT,
      maxSize = maxSize,
      taskRunner = TaskRunner.INSTANCE
  )
  ...
}

可以看出,在这里创建了一个DiskLruCache对象。

put

  internal fun put(response: Response): CacheRequest? {
    // 获取请求的方法  
    val requestMethod = response.request.method

    // 若method是POST、PATCH、PUT、DELETE、MOVE当中的一个,则认为该Response不需要缓存
    if (HttpMethod.invalidatesCache(response.request.method)) {
      try {
        // 移除该请求对应的缓存  
        remove(response.request)
      } catch (_: IOException) {
        // The cache cannot be written.
      }
      // 结束  
      return null
    }

    // 若method不是GET,则不进行Response的缓存
    // 虽然从技术上讲,HEAD的部分POST对应的响应也可以缓存,但是复杂度太高,且效益太低  
    if (requestMethod != "GET") {
      return null
    }

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

    // 根据Response创建Cache.Entry  
    val entry = Entry(response)
    var editor: DiskLruCache.Editor? = null
    try {
      // 尝试获取editor  
      editor = cache.edit(key(response.request.url)) ?: return null
      // 将entry的信息写入editor中
      entry.writeTo(editor)
      // 根据editor获取CacheRequest对象  
      return RealCacheRequest(editor)
    } catch (_: IOException) {
      abortQuietly(editor)
      return null
    }
  }

put方法的执行大致可以分为两部分

  1. 根据Requestmethod,判断是否需要缓存Response,若不需要缓存Response,则return null,执行结束
  2. 根据Response去创建EntryResponse的信息就存放在Entry里面,接着将Entry的信息写入DiskLruCache.Editor中。

另外,在创建DiskLruCache.Editor的时候,调用了key方法,该方法会根据requesturl生成其对应的存储key,如下

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

可以看出,它其实就是将url使用UTF-8编码后,再使用md5加密,再转化成了十六进制。

Cache.Entry

我们看下存储Response信息的Cache.Entry

  private class Entry {
  	...
    constructor(response: Response) {
      // 请求url  
      this.url = response.request.url.toString()
      // Vary响应头  
      this.varyHeaders = response.varyHeaders()
      // 请求方法
      this.requestMethod = response.request.method
      // 协议
      this.protocol = response.protocol
      // Http状态码
      this.code = response.code
      // HTTP状态消息  
      this.message = response.message
      // HTTP响应头  
      this.responseHeaders = response.headers
      // 承载此响应连接的TLS握手,若响应没有TLS则为null  
      this.handshake = response.handshake
      // 发起请求时间戳,若该响应由缓存提供,则这是原始请求的时间戳  
      this.sentRequestMillis = response.sentRequestAtMillis
      // 收到响应时间戳,若该响应由缓存提供,则这是原始响应的时间戳  
      this.receivedResponseMillis = response.receivedResponseAtMillis
    }
    ...  
  }
Cache.Entry::writeTo

Cache.Entry就是调用writeTo这个方法,向editor写入响应Response的信息的,我们查看这个方法

    @Throws(IOException::class)
    fun writeTo(editor: DiskLruCache.Editor) {
      // 获取输出流sink,然后对数据逐个缓存  
      editor.newSink(ENTRY_METADATA).buffer().use { sink ->
        // 缓存请求url                                           
        sink.writeUtf8(url).writeByte('\n'.toInt())
        // 缓存请求方法                                           
        sink.writeUtf8(requestMethod).writeByte('\n'.toInt())
        // 缓存Vary响应头大小                                           
        sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.toInt())
        // 缓存Vary响应头信息                                          
        for (i in 0 until varyHeaders.size) {
          sink.writeUtf8(varyHeaders.name(i))
              .writeUtf8(": ")
              .writeUtf8(varyHeaders.value(i))
              .writeByte('\n'.toInt())
        }

        // 缓存协议、状态码、状态消息                                           
        sink.writeUtf8(StatusLine(protocol, code, message).toString()).writeByte('\n'.toInt())
        // 缓存响应头大小                                           
        sink.writeDecimalLong((responseHeaders.size + 2).toLong()).writeByte('\n'.toInt())
        // 缓存响应头信息                                           
        for (i in 0 until responseHeaders.size) {
          sink.writeUtf8(responseHeaders.name(i))
              .writeUtf8(": ")
              .writeUtf8(responseHeaders.value(i))
              .writeByte('\n'.toInt())
        }
        // 缓存请求发送时间                                           
        sink.writeUtf8(SENT_MILLIS)
            .writeUtf8(": ")
            .writeDecimalLong(sentRequestMillis)
            .writeByte('\n'.toInt())
        // 缓存请求响应时间                                           
        sink.writeUtf8(RECEIVED_MILLIS)
            .writeUtf8(": ")
            .writeDecimalLong(receivedResponseMillis)
            .writeByte('\n'.toInt())

        // 判断是否为HTTPS请求,如果是则记录TLS握手信息                                        
        if (isHttps) {
          sink.writeByte('\n'.toInt())
          sink.writeUtf8(handshake!!.cipherSuite.javaName).writeByte('\n'.toInt())
          writeCertList(sink, handshake.peerCertificates)
          writeCertList(sink, handshake.localCertificates)
          sink.writeUtf8(handshake.tlsVersion.javaName).writeByte('\n'.toInt())
        }
      }
    }

这里将Entry存储的信息写入editor,而Entry存储的是Response的信息,所以这里其实就是将Response的信息写入editor,之后editor会将信息保存在本地,也就实现了Response信息保存在本地的操作。这里主要利用了 okio 库中的 BufferedSink 实现了写入操作。

get

我们接着看Cacheget方法

  internal fun get(request: Request): Response? {
    // 以请求url作为key  
    val key = key(request.url)
    // 在DiskLruCache中查找是否有key对应的缓存,有则返回对应的内存快照,没有则返回null  
    val snapshot: DiskLruCache.Snapshot = try {
      cache[key] ?: return null
    } catch (_: IOException) {
      return null // Give up because the cache cannot be read.
    }

    // 根据内存快照的getSource方法获取输入流,接着在Entry的构造方法中,将输入流中的缓存数据
    // 保存起来,构造出Entry对象  
    val entry: Entry = try {
      Entry(snapshot.getSource(ENTRY_METADATA))
    } catch (_: IOException) {
      snapshot.closeQuietly()
      return null
    }

    // 利用Entry对象中的信息,构造出Response对象  
    val response = entry.response(snapshot)
    // 判断请求与构造的Response是否匹配,不匹配则返回null  
    if (!entry.matches(request, response)) {
      response.body?.closeQuietly()
      return null
    }
	// 返回响应
    return response
  }

get方法的执行流程是,先根据请求的url对应的key,在DiskLruCache查找是否有对应的缓存,如果有缓存,则根据缓存中的信息构造出Response,然后再匹配构造出的Response与请求Request是否匹配,匹配则返回Response,不匹配则返回null

Cache.Entry(Source)

我们查看Entry参数为Source的构造函数,查看它是如何根据Source构造出Entry的,由于代码很多,我们只截取一小部分

    @Throws(IOException::class) constructor(rawSource: Source) {
      try {
        val source = rawSource.buffer()
        url = source.readUtf8LineStrict()
        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
		...
      } finally {
        rawSource.close()
      }
    }

其内部就是读取source中的信息并且保存起来,这里的读取操作也是使用到了okio这个库。

Cache.Entry::response

Entry通过response方法,构建出了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()
    }

可以看出,其实就是利用了Entry保存的信息,构建出了Response对象。

小结

可以发现,写入和读取的过程都有用到 Entry 类,看来 Entry 类就是 OkHttpResponse 缓存的桥梁了。

update

我们查看Cacheupdate方法

  internal fun update(cached: Response, network: Response) {
    // 将网络Response信息保存在Entry中
    val entry = Entry(network)
    // 根据缓存Response,获取内存快照  
    val snapshot = (cached.body as CacheResponseBody).snapshot
    var editor: DiskLruCache.Editor? = null
    try {
      // 获取editor对象  
      editor = snapshot.edit() ?: return // edit() returns null if snapshot is not current.
      // 将entry保存的信息(网络Response的信息),写入editor中  
      entry.writeTo(editor)
      // 提交editor的内容,写入本地  
      editor.commit()
    } catch (_: IOException) {
      abortQuietly(editor)
    }
  }

有了前面的基础,理解update方法就很容易了。

remove

Cacheremove方法如下

  @Throws(IOException::class)
  internal fun remove(request: Request) {
    cache.remove(key(request.url))
  }

它的实现很简单,直接调用了DiskLruCacheremove方法,移除对应的缓存。

CacheStrategy

CacheInterceptordintercept方法中,先调用CacheStrategy.Factory的构造方法,创建了一个Factory对象,然后再调用该对象的compute方法,创建了一个CacheStrategy对象。

CacheStrategy构造方法的参数如下

class CacheStrategy internal constructor(
  /** The request to send on the network, or null if this call doesn't use the network. */
  val networkRequest: Request?,
  /** The cached response to return or validate; or null if this call doesn't use a cache. */
  val cacheResponse: Response?
) {
    ...
}

CacheStrategy.Factory

构造方法

我们查看CacheStrategy.Factory这个类

  class Factory(
    private val nowMillis: Long,
    internal val request: Request,
    private val cacheResponse: Response?
  ) {
    // 服务器提供cacheResponse的时间  
    private var servedDate: Date? = null
    private var servedDateString: String? = null

    // cacheResponse最后修改的时间  
    private var lastModified: Date? = null
    private var lastModifiedString: String? = null

    // cacheResponse过期的时间,如果这个字段和max-age字段都被设置了,max-age字段的优先级更高  
    private var expires: Date? = null

    // 由OkHttp设置的扩展Header,表明cacheResponse对应请求的首次发起时间  
    private var sentRequestMillis = 0L

    // 由OkHttp设置的扩展Header,表明cacheResponse首次被接收到的时间  
    private var receivedResponseMillis = 0L

    // cacheResponse的Etag标识符  
    private var etag: String? = null

    // cacheResponse的存活时间  
    private var ageSeconds = -1

    /**
     * Returns true if computeFreshnessLifetime used a heuristic. If we used a heuristic to serve a
     * cached response older than 24 hours, we are required to attach a warning.
     */
    private fun isFreshnessLifetimeHeuristic(): Boolean {
      return cacheResponse!!.cacheControl.maxAgeSeconds == -1 && expires == null
    }

    init {
      // 若在本地磁盘找得到缓存响应,则将对应信息保存在Factory对象中  
      if (cacheResponse != null) {
        this.sentRequestMillis = cacheResponse.sentRequestAtMillis
        this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis
        val headers = cacheResponse.headers
        for (i in 0 until headers.size) {
          val fieldName = headers.name(i)
          val value = headers.value(i)
          when {
            fieldName.equals("Date", ignoreCase = true) -> {
              servedDate = value.toHttpDateOrNull()
              servedDateString = value
            }
            fieldName.equals("Expires", ignoreCase = true) -> {
              expires = value.toHttpDateOrNull()
            }
            fieldName.equals("Last-Modified", ignoreCase = true) -> {
              lastModified = value.toHttpDateOrNull()
              lastModifiedString = value
            }
            fieldName.equals("ETag", ignoreCase = true) -> {
              etag = value
            }
            fieldName.equals("Age", ignoreCase = true) -> {
              ageSeconds = value.toNonNegativeInt(-1)
            }
          }
        }
      }
    }
      
    fun compute(): CacheStrategy {...}  
    ...  
  }

CacheStrategy.Factory的构造方法中,主要是判断缓存响应cacheResponse是否存在,若存在则将响应对应的信息保存在Factory对象中。

compute

通过调用Factory对象的compute方法,可以创建一个CacheStrategy对象

    /** Returns a strategy to satisfy [request] using [cacheResponse]. */
	// 使用cacheResponse返回一个满足Request的策略
    fun compute(): CacheStrategy {
      val candidate = computeCandidate()

      // We're forbidden from using the network and the cache is insufficient.
      // 我们被禁止使用网络,并且缓存不足
      if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
        return CacheStrategy(null, null)
      }

      return candidate
    }
computeCandidate

上面的compute方法调用了computeCandidate方法,该方法如下

    private fun computeCandidate(): CacheStrategy {
      // 若该请求没有对应的缓存响应,则忽略缓存响应,需要网络请求
      if (cacheResponse == null) { 
        return CacheStrategy(request, null)
      }

      // 若该请求为HTTPS请求,但是缓存响应中没有保存TLS握手相关信息,则忽略缓存响应,需要网络请求
      if (request.isHttps && cacheResponse.handshake == null) {
        return CacheStrategy(request, null)
      }

      // 通过cacheResponse的响应码来判断响应是否允许被缓存,若不允许则忽略缓存响应,需要网络请求 
      // (其实参与判断的还有noStore指令的值) 
      if (!isCacheable(cacheResponse, request)) {
        return CacheStrategy(request, null)
      }

      // CacheControl类:包含来自服务端或客户端的缓存指令,这些指令表明了什么响应可以被存储,
      // 这些存储的响应可以满足哪些需求。
      val requestCaching = request.cacheControl
      // noCache指令说明:noCache指令可以出现在请求、响应当中。若出现在响应的位置,它表明在发布
      // 缓存副本之前,必须向源服务器验证缓存的有效性;若出现在请求中,它表明不要使用一个缓存来回应
      // 该需求。  
      // hasConditions方法:若Request包含If-Modified-Since或If-None-Match其中一个Header,
      // 则该方法返回true。
      // 这里If语句的意思:若请求不允许使用缓存响应,或者请求头有If-Modified-Since/If-None-Match,  	  // 则忽略缓存响应,需要网络请求。(客户端发送的请求自己就带有If-Modified-Since或If-None-Match
      // ,缓存响应也是不会被使用的,OkHttp在下面的代码中是有为请求添加If-Modified-Since或
      // If-None-Match的Header的逻辑的) 
      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())
      }

      // 如果cacheResponse没有noCache指令(发布缓存前不用向源服务器验证),并且cacheResponse
      // 仍然在存活时间内,则不需要进行网络请求,直接使用缓存响应  
      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())
      }

      // 添加 If-None-Match/If-Modified-Since 的Header到请求头中,服务器通过验证这些Header,
      // 来判断客户端的缓存是否还有效,若缓存仍然有效,则返回304,响应中不会包含Response Body  
      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
        }

        // 没有 If-None-Match/If-Modified-Since 的Header可以添加,则忽略缓存响应,需要网络请求   
        else -> return CacheStrategy(request, null) // No condition! Make a regular request.
      }
  
      val conditionalRequestHeaders = request.headers.newBuilder()
      conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)

      // 在原来的Request上面添加包含If-None-Match/If-Modified-Since的Header  
      val conditionalRequest = request.newBuilder()
          .headers(conditionalRequestHeaders.build())
          .build()
      // 返回一个包含网络请求和缓存响应的策略  
      return CacheStrategy(conditionalRequest, cacheResponse)
    }

computeCandidate方法的主要作用是,根据请求和缓存响应(cacheResponse)的信息,去创建并返回一个CacheStrategy对象,该对象表明的情况有下面三种

  • 忽略缓存响应,以网络请求为准
  • 不进行网络请求,直接使用缓存响应
  • 为请求添加If-None-Match/If-Modified-SinceHeader,这时候结合网络请求和缓存响应,若服务端验证缓存响应仍然有效,则返回304,不返回Response Body,若服务端验证缓存响应无效,则返回200,并且返回的响应中包含Response Body

computeCandidate方法的步骤做一个总结:

  1. 该请求没有对应的缓存响应,直接进行网络请求
  2. 该请求为HTTPS请求但是缓存响应中没有保存TLS握手相关数据,忽略缓存,进行网络请求
  3. 该响应是否允许被缓存,若不允许,直接进行网络请求
  4. 若请求头中含有noCache指令或If-Modified-Since/If-None-Match Header,则忽略缓存响应,进行网络请求
  5. 如果缓存响应没有noCache指令,并且缓存响应还未过期,则直接使用缓存响应,不需要进行网络请求
  6. 若缓存响应过期,且没有保存etag/lastModified/servedDate信息,直接进行网络请求
  7. 若缓存响应过期,且缓存响应中保存了etag/Modified/servedDate信息,将信息添加到请求头中,进行网络请求,同时将该缓存响应保存至缓存策略中。

另外注意一点:

  • 在第7步,添加etag/lastModified/servedDate信息,也就是添加If-Modified-Since/If-None-Match Header的时候,If-None-Match信息的添加在If-Modified-Since的前面,这也说明了Etag / If-None-Match的优先级高于 Last-Modified/If-Modified-Since

关系梳理

我们梳理一下CacheStrategy的构建流程,以及它和CacheInterceptor::intercept的关系。

首先在CacheInterceptor::intercept中,会先去构造一个CacheStrategy.Factory对象,并且将候选缓存作为构造参数传入,在Factory的构造方法中会保存候选缓存的信息,接着在拦截器的intercept方法中,调用Factory对象的compute方法,去获取一个CacheStrategy对象,compute方法内部又会调用computeCandidate方法,在computeCandidate方法中,会根据请求、候选缓存的信息,去构建一个CacheStrategy对象,CacheStrategy对象表明是否需要进行网络请求、是否使用缓存响应,intercept方法在获取到CacheStrategy对象后,根据策略中的信息,会选择:

  • 报错
  • 使用缓存
  • 进行网络请求
    • 服务端验证缓存资源有效,使用缓存资源
    • 服务端验证缓存资源无效,使用网络请求结果

总结

OkHttp的缓存主要与两个类有关:Cache类和CacheStrategy类。Cache类负责在本地对Response进行CRUD的操作;CacheStrategy会根据接收到的请求、候选缓存的信息,判断该请求是要进行网络请求,还是直接使用缓存响应,还是让服务器验证缓存资源是否有效。CacheInterceptor拦截器就在它的intercept方法中,根据策略中获取的信息,进行「报错、使用缓存、进行网络请求」等操作。

Cache类对Response进行的CRUD操作,是依靠DiskLruCache实现的,DiskLruCache实现了磁盘的LruCache机制。用户可以对OkHttpClient中的Cache进行配置,实现自定义缓存地址以及缓存空间大小。

大致流程如下:

image-20211124124939616

参考

  1. 彻底弄懂HTTP缓存机制及原理 - 博客园.
posted @ 2021-12-19 00:19  Giagor  阅读(931)  评论(0编辑  收藏  举报