OKHttp攔截器-緩存攔截器

OKHttp攔截器-緩存攔截器

CacheInterceptor荣暮,OKHttp第三個執(zhí)行的攔截器就是緩存攔截器了,在發(fā)出請求前,判斷是否命中緩存梧油。如果命中則可以不請求,直接使用緩存的響應(只會存在Get請求的緩存)州邢。這里內容比較多儡陨,大家做好心理準備哦~

總體流程

老規(guī)矩,先來看一下攔截器的CacheInterceptor#intercept()方法:

@Throws(IOException::class)
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.
  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) {
    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()
    }
  }

  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
}

先來看一下大體的流程量淌,首先通過CacheStrategy.Factory().compute()方法拿到CacheStrategy對象骗村,再判斷對象里面的兩個成員判斷應該返回的響應結果:

val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
// 請求網絡
val networkRequest = strategy.networkRequest
// 請求緩存
val cacheResponse = strategy.cacheResponse
  • 如果networkRequest == null && cacheResponse == null,那么直接GG呀枢,返回響應碼返回504胚股,

    if (networkRequest == null && cacheResponse == null) {
      return Response.Builder()
          .request(chain.request())
            // 網絡協(xié)議1.1
          .protocol(Protocol.HTTP_1_1)
          // HTTP_GATEWAY_TIMEOUT 504
          .code(HTTP_GATEWAY_TIMEOUT)
          // 錯誤消息
          .message("Unsatisfiable Request (only-if-cached)")
          // 一個空的響應體
          .body(EMPTY_RESPONSE)
          .sentRequestAtMillis(-1L)
          .receivedResponseAtMillis(System.currentTimeMillis())
          .build().also {
            listener.satisfactionFailure(call, it)
          }
    }
    
  • 如果networkRequest == null ,這時候 cacheResponse肯定非空裙秋,直接返回cacheResponse

    if (networkRequest == null) {
          return cacheResponse!!.newBuilder()
              .cacheResponse(stripBody(cacheResponse))
              .build().also {
                listener.cacheHit(call, it)
              }
    }
    
    private fun stripBody(response: Response?): Response? {
          return if (response?.body != null) {
            response.newBuilder().body(null).build()
          } else {
            response
          }
    }
    
  • 判斷cacheResponse != null琅拌,如果條件命中,說明networkRequestcacheResponse都非空摘刑,那么判斷服務器返回的code碼进宝,如果是HTTP_NOT_MODIFIED(304)代表緩存沒有被修改,那么更新緩存時效并返回

    // If we have a cache response too, then we're doing a conditional get.
    if (cacheResponse != null) {
      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非空枷恕,那么直接向服務器發(fā)起請求党晋,獲取到響應之后再進行緩存

    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.
            }
          }
        }
    

    從上 代碼 我們看到,只有cache != null時才會進行緩存,cache怎么來的呢 未玻?還是在我們構造OKHttpClient的時候傳入的

    val client = OkHttpClient.Builder()
        .cookieJar(cookieJar)
        .cache(Cache(File(Environment.DIRECTORY_DOCUMENTS), 1024 * 1024 * 50))
        .retryOnConnectionFailure(false)
        .build()
    

    那我們來總結一下整體的步驟:

    1灾而、從緩存中獲得對應請求的響應緩存

    2、創(chuàng)建 CacheStrategy ,創(chuàng)建時會判斷是否能夠使用緩存扳剿,在 CacheStrategy 中存在兩個成員:networkRequestcacheResponse 旁趟。他們的組合如下:

    networkRequest cacheResponse 說明
    Null Not Null 直接使用緩存
    Not Null Null 向服務器發(fā)起請求
    Null Null 直接gg,okhttp直接返回504
    Not Null Not Null 發(fā)起請求庇绽,若得到響應為304轻庆,則更新緩存并返回

    3、交給下一個責任鏈繼續(xù)處理

    4敛劝、后續(xù)工作余爆,返回304則用緩存的響應;否則使用網絡響應并緩存本次響應(只緩存Get請求的響應)

    緩存攔截器的工作說起來比較簡單夸盟,但是具體的實現(xiàn)蛾方,需要處理的內容很多。在緩存攔截器中判斷是否可以使用緩存上陕,或是請求服務器都是通過 CacheStrategy判斷桩砰。

緩存策略

緩存主要的邏輯就是緩存策略(CacheStrategy)了,首先需要認識幾個請求頭與響應頭

響應頭 說明 舉例
Date 消息發(fā)送的時間 Date: Sat, 18 Nov 2028 06:17:41 GMT
Expires 資源過期的時間 Expires: Sat, 18 Nov 2028 06:17:41 GMT
Last-Modifified 資源最后修改時間 Last-Modifified: Fri, 22 Jul 2016 02:57:17 GMT
ETag 資源在服務器的唯一標識 ETag: "16df0-5383097a03d40"
Age 服務器用緩存響應請求释簿,<br />該緩存從產生到現(xiàn)在經過多長時間(秒) Age: 3825683
Cache-Control 請求控制 no-cache
請求頭 說明 舉例
If-Modified-Since 服務器沒有在指定的時間后修改請求對應資源亚隅,返回304(無修改) If-Modifified-Since: Fri, 22 Jul 2016 02:57:17 GMT
If-None-Match 服務器將其與請求對應資源的 Etag 值進行比較,匹配返回304 If-None-Match: "16df0-5383097a03d40
Cache-Control 請求控制 no-cache

其中 Cache-Control 可以在請求頭存在庶溶,也能在響應頭存在煮纵,對應的value可以設置多種組合:

  1. max-age=[秒] :資源最大有效時間;

  2. public :表明該資源可以被任何用戶緩存,比如客戶端偏螺,代理服務器等都可以緩存資源;

  3. private :表明該資源只能被單個用戶緩存行疏,默認是private。

  4. no-store :資源不允許被緩存

  5. no-cache :(請求)不使用緩存

  6. immutable :(響應)資源不會改變

  7. min-fresh=[秒] :(請求)緩存最小新鮮度(用戶認為這個緩存有效的時長)

  8. must-revalidate :(響應)不允許使用過期緩存

  9. max-stale=[秒] :(請求)緩存過期后多久內仍然有效

這里需要注意一點套像,假設存在max-age=100酿联,min-fresh=20。這代表了用戶認為這個緩存的響應夺巩,從服務器創(chuàng)建響應到能夠緩存使用的時間為100-20=80s贞让。但是如果max-stale=100。這代表了緩存有效時間80s過后柳譬,仍然允許使用100s喳张,可以看成緩存有效時長為180s。

在這里插入圖片描述

詳細流程

至此我們對緩存策略有了一定的了解征绎,現(xiàn)在就可以看看它的詳細流程了蹲姐,首先我們看一下緩存策略是如何構造的

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

class Factory(
    private val nowMillis: Long,
    internal val request: Request,
    private val cacheResponse: Response?
  ) {
   
    // ...........
    
    init {
      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)
            }
          }
        }
      }
    }
}

上面代碼很好理解,只做了一件事情人柿,解析請求頭里面的數(shù)據(jù)并保存到成員變量柴墩,接下來看一下compute()方法

/** Returns a strategy to satisfy [request] using [cacheResponse]. */
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()完成真正的緩存判斷

/** Returns a strategy to use assuming the request can use the network. */
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.
  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
  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.
  }

  val conditionalRequestHeaders = request.headers.newBuilder()
  conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)

  val conditionalRequest = request.newBuilder()
      .headers(conditionalRequestHeaders.build())
      .build()
  return CacheStrategy(conditionalRequest, cacheResponse)
}

CacheStrategy有兩個構造參數(shù):

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?
)

第一個是請求對象,第二個是緩存響應對象凫岖,上面我們分析了江咳,如果cacheResponse為null,那么需要進行網絡請求哥放。以上代碼太多歼指,接下來我們還是分步驟來分析。

1. 緩存是否存在

if (cacheResponse == null) {
    return CacheStrategy(request, null)
}

cacheResponse是從緩存中找到的響應甥雕,如果為null踩身,那就表示沒有找到對應的緩存,創(chuàng)建的CacheStrategy實例對象只存在networkRequest社露,這代表了需要發(fā)起網絡請求挟阻。

2. https請求的緩存

if (request.isHttps && cacheResponse.handshake == null) {
    return CacheStrategy(request, null)
  }

如果本次是https,需要檢測緩存中的握手信息峭弟,如果沒有握手信息附鸽,那么緩存無效,需要從網絡請求瞒瘸。

3. 響應碼和響應頭

if (!isCacheable(cacheResponse, request)) {
  return CacheStrategy(request, null)
}

根據(jù)響應頭和響應碼判斷是否可以拿到緩存坷备,整個邏輯都在isCacheable()中:

/** Returns true if [response] can be stored to later serve another request. */
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,                            // 200
    HTTP_NOT_AUTHORITATIVE,             // 203
    HTTP_NO_CONTENT,                    // 204
    HTTP_MULT_CHOICE,                   // 300 
    HTTP_MOVED_PERM,                    // 301
    HTTP_NOT_FOUND,                     // 404
    HTTP_BAD_METHOD,                    // 405
    HTTP_GONE,                          // 410
    HTTP_REQ_TOO_LONG,                  // 414
    HTTP_NOT_IMPLEMENTED,               // 501
    StatusLine.HTTP_PERM_REDIRECT       // 308
      -> {
      // These codes can be cached unless headers forbid it.
    }

    HTTP_MOVED_TEMP,                    // 302
    StatusLine.HTTP_TEMP_REDIRECT       // 307
      -> {
      // 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的情況下,什么都不做情臭,直接返回!response.cacheControl.noStore && !request.cacheControl.noStore省撑,這兩個條件就是判斷服務器給的響應頭里面有沒有Cache-Control: no-store(資源不可被緩存),如果有俯在,代表緩存不可用丁侄,否則繼續(xù)下一步判斷。

  • 如果響應碼是302,307(重定向)朝巫,則需要進一步判斷是不是存在一些允許緩存的響應頭鸿摇。根據(jù)注解中的給到的文檔<a >http://tools.ietf.org/html/rfc7234#section-3</a>中的描述,如果存在Expires或者Cache-Control的值為:

    1. max-age=[秒] :資源最大有效時間;

    2. public :表明該資源可以被任何用戶緩存劈猿,比如客戶端拙吉,代理服務器等都可以緩存資源;

    3. private :表明該資源只能被單個用戶緩存,默認是private

    同時不存在 Cache-Control: no-store 揪荣,那就可以繼續(xù)進一步判斷緩存是否可用筷黔,否則緩存不可用。

中間總結

以上3步可以總結一下:

1仗颈、響應碼不為 200, 203, 204, 300, 301, 404, 405, 410, 414, 501, 308佛舱,302椎例,307 緩存不可用;

2、當響應碼為302或者307時请祖,未包含某些響應頭订歪,則緩存不可用;

3、當存在 Cache-Control: no-store 響應頭則緩存不可用肆捕。

如果響應緩存可用刷晋,進一步再判斷緩存有效性

4. 用戶的請求配置

經過以上幾個判斷,如果緩存是可用狀態(tài)慎陵,就要繼續(xù)下一步判斷了

val requestCaching = request.cacheControl

if (requestCaching.noCache || hasConditions(request)) {
  return CacheStrategy(request, null)
}

 private fun hasConditions(request: Request): Boolean =
        request.header("If-Modified-Since") != null || request.header("If-None-Match") != null

OkHttp需要先對用戶本次發(fā)起的Request進行判定眼虱,如果用戶指定了Cache-Control: no-cache (不使用緩存)的請求頭或者請求頭包含If-Modified-SinceIf-None-Match (請求驗證),那么就不允許使用緩存席纽。

這意味著如果用戶請求頭中包含了這些內容捏悬,那就必須向服務器發(fā)起請求。但是需要注意的是润梯,OkHttp并不會緩存304的響應邮破,如果是此種情況,即用戶主動要求與服務器發(fā)起請求仆救,服務器返回的304(無響應體)抒和,則直接把304的響應返回給用戶:既然你主動要求,我就只告知你本次請求結果彤蔽。

而如果不包含這些請求頭摧莽,那繼續(xù)判定緩存有效性。

5. 響應的緩存有效期

這里跟OKHttp3.x的版本有些區(qū)別顿痪,在3.x的版本中還判斷了一個Cache-Control: immutable, 代表緩存沒有改變镊辕,這時就可以直接使用緩存了,在kotlin版本中去掉了這個判斷蚁袭。

這一步為進一步根據(jù)緩存響應中的一些信息判定緩存是否處于有效期內征懈。如果滿足條件:

緩存存活時間 < 緩存新鮮度 - 緩存最小新鮮度 + 過期后繼續(xù)使用時長

代表可以使用緩存。其中新鮮度可以理解為有效時間揩悄,而這里的 緩存新鮮度 - 緩存最小新鮮度 就代表了緩存真正有效的時間卖哎。

//  獲取緩存響應CacheControl頭
val responseCaching = cacheResponse.cacheControl
// 1.獲取緩存的響應從創(chuàng)建到現(xiàn)在的時間
val ageMillis = cacheResponseAge()    
// 2.獲取響應有效緩存的時長
var freshMillis = computeFreshnessLifetime()

// 如果請求中指定了 max-age 表示指定了能拿的緩存有效時長,
// 就需要綜合響應有效緩存時長與請求能拿緩存的時長删性,
// 獲得最小的能夠使用響應緩存的時長
if (requestCaching.maxAgeSeconds != -1) {
        freshMillis = minOf(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds.toLong()))
      }
// 3. 請求包含Cache-Control:min-fresh=[秒]能夠使用還未過指定時間的緩存(請求認為的緩存有效時間)
var minFreshMillis: Long = 0 
if (requestCaching.minFreshSeconds != -1) {  
    minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())    
}

// 4. 
// 4.1 Cache-Control:must-revalidate 可緩存但必須再向源服務器進行確認
// 4.2 Cache-Control:max-stale=[秒] 緩存過期后還能使用指定的時長 如果未指定多少秒亏娜,則表示無論過期多長都可以;如果指定了蹬挺,則只要是指定時間內就能使用緩存

// eg: 前者忽略后者维贺,所以判斷了不必須向服務器確認,再獲得請求頭中的max-stale
var maxStaleMillis: Long = 0 
if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {    
    maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())     
}

// 5. 不需要與服務器驗證有效性 && (響應存在的時間 + 請求認為的緩存有效時間) < (緩存有效時長+過期后還可以使用的時間), 條件命中代表可以使用緩存
if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {        
    
    val builder = cacheResponse.newBuilder()    
    // 如果已過期巴帮,但未超過 過期后繼續(xù)使用時長溯泣,那還可以繼續(xù)使用虐秋,只用添加相應的頭部字段
    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())     
}
      
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)    
}
     
val conditionalRequestHeaders = request.headers.newBuilder()   
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)  
val conditionalRequest = request.newBuilder()   
.headers(conditionalRequestHeaders.build()          
.build()     
return CacheStrategy(conditionalRequest, cacheResponse)

1. 緩存到現(xiàn)在存活的時間:ageMillis

首先cacheResponseAge()方法獲得了響應大概存在了多久:

val ageMillis = cacheResponseAge()       

private fun cacheResponseAge(): Long {
      val servedDate = this.servedDate
      //  apparentReceivedAge 代表了客戶端收到響應到服務器發(fā)出響應的一個時間差
      //  seredData 是從緩存中獲得的 Date 響應頭對應的時間(服務器發(fā)出本響應的時間)
      //  receivedResponseMillis 為本次響應對應的客戶端發(fā)出請求的時間
      val apparentReceivedAge = if (servedDate != null) {
        maxOf(0, receivedResponseMillis - servedDate.time)
      } else {
        0
      }

      // receivedAge 是代表了客戶端的緩存,在收到時就已經存在多久了
      // ageSeconds 是從緩存中獲得的 Age 響應頭對應的秒數(shù) (本地緩存的響應是由服務器的緩存返回垃沦,這個緩存在服務器存在的時間)客给。ageSeconds 與上一步計算結果apparentReceivedAge的最大值為收到響應時,這個響應數(shù)據(jù)已經存在多久
      // 假設我們發(fā)出請求時栏尚,服務器存在一個緩存,其中 Data: 0點 只恨。 此時译仗,客戶端在1小時后發(fā)起請求,此時由服務器在緩存中插入 Age: 1小時 并返回給客戶端官觅,此時客戶端計算的 receivedAge 就是1小時纵菌,這就代表了客戶端的緩存在收到時就已經存在多久了。(不代表到本次請求時存在多久了)  
      val receivedAge = if (ageSeconds != -1) {
        maxOf(apparentReceivedAge, SECONDS.toMillis(ageSeconds.toLong()))
      } else {
        apparentReceivedAge
      }

      // responseDuration 是緩存對應的請求休涤,在發(fā)送請求與接收請求之間的時間差
      val responseDuration = receivedResponseMillis - sentRequestMillis
      // residentDuration 是這個緩存接收到的時間到現(xiàn)在的一個時間差
      val residentDuration = nowMillis - receivedResponseMillis
        
      //  receivedAge + responseDuration + residentDuration 所代表的意義就是:
      // 緩存在客戶端收到時就已經存在的時間 + 請求過程中花費的時間 + 本次請求距離緩存獲得的時間咱圆,就是緩存真正存在了多久。
      return receivedAge + responseDuration + residentDuration
    }

2. 緩存新鮮度(有效時間):freshMillis

var freshMillis = computeFreshnessLifetime()
    
private fun computeFreshnessLifetime(): Long {
      val responseCaching = cacheResponse!!.cacheControl
      if (responseCaching.maxAgeSeconds != -1) {
        return SECONDS.toMillis(responseCaching.maxAgeSeconds.toLong())
      }

      val expires = this.expires
      if (expires != null) {
        val servedMillis = servedDate?.time ?: receivedResponseMillis
        val delta = expires.time - servedMillis
        return if (delta > 0L) delta else 0L
      }

      if (lastModified != null && cacheResponse.request.url.query == null) {
        // As recommended by the HTTP RFC and implemented in Firefox, the max age of a document
        // should be defaulted to 10% of the document's age at the time it was served. Default
        // expiration dates aren't used for URIs containing a query.
        val servedMillis = servedDate?.time ?: sentRequestMillis
        val delta = servedMillis - lastModified!!.time
        return if (delta > 0L) delta / 10 else 0L
      }

      return 0L
}

緩存新鮮度(有效時長)的判定會有幾種情況功氨,按優(yōu)先級排列如下:

  1. 緩存響應包含Cache-Control: max-age=[秒資源最大有效時間

  2. 緩存響應包含Expires: 時間 序苏,則通過Data或接收該響應時間計算資源有效時間

  3. 緩存響應包含 Last-Modified: 時間 ,則通過Data或發(fā)送該響應對應請求的時間計算資源有效時間捷凄;并且根據(jù)建議以及在Firefox瀏覽器的實現(xiàn)忱详,使用得到結果的10%來作為資源的有效時間。

3. 緩存最小新鮮度:minFreshMillis

var minFreshMillis: Long = 0
if (requestCaching.minFreshSeconds != -1) {
  minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds.toLong())
}

如果用戶的請求頭中包含Cache-Control: min-fresh=[秒]跺涤,代表用戶認為這個緩存有效的時長匈睁。假設本身緩存新鮮度為: 100秒,而緩存最小新鮮度為:10秒桶错,那么緩存真正有效時間為90秒航唆。

4. 緩存過期后仍然有效時長:maxStaleMillis

var maxStaleMillis: Long = 0
if (!responseCaching.mustRevalidate && requestCaching.maxStaleSeconds != -1) {
  maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds.toLong())
}

如果緩存的響應中沒有包含Cache-Control: must-revalidate (不可用過期資源),獲得用戶請求頭中包含Cache-Control: max-stale=[秒]緩存過期后仍有效的時長院刁。

5. 判定緩存是否有效

if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
  val builder = cacheResponse.newBuilder()
  //  如果已過期糯钙,但未超過過期后仍然有效時長,那還可以繼續(xù)使用退腥,添加Warning響應頭
  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())
}

最后利用上4步產生的值超营,只要緩存的響應未指定 no-cache 忽略緩存,如果:

緩存存活時間+緩存最小新鮮度 < 緩存新鮮度+過期后繼續(xù)使用時長

代表可以使用緩存阅虫。

假設 緩存到現(xiàn)在存活了:100 毫秒; 用戶認為緩存有效時間(緩存最小新鮮度)為:10 毫秒; 緩存新鮮度為: 100毫秒; 緩存過期后仍能使用: 0 毫秒; 這些條件下演闭,首先緩存的真實有效時間為: 90毫秒,而緩存已經過了這個時間颓帝,所以無法使用緩存米碰。不等式可以轉換為: 緩存存活時間 < 緩存新鮮度 - 緩存最小新鮮度 + 過期后繼續(xù)使用時長窝革,即 存活時間 < 緩存有效時間 + 過期后繼續(xù)使用時間

6. 緩存過期處理

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.
}

val conditionalRequestHeaders = request.headers.newBuilder()
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)

val conditionalRequest = request.newBuilder()
    .headers(conditionalRequestHeaders.build())
    .build()
return CacheStrategy(conditionalRequest, cacheResponse)

如果繼續(xù)執(zhí)行,表示緩存已經過期無法使用吕座。此時我們判定緩存的響應中如果存在Etag虐译,則使用If-None-Match交給服務器進行驗證;如果存在Last-Modified或者Data吴趴,則使用If-Modified-Since交給服務器驗證漆诽。服務器如果無修改則會返回304。

這時候注意:由于是緩存過期而發(fā)起的請求(與第4個判斷用戶的主動設置不同)锣枝,如果服務器返回304厢拭,那框架會自動更新緩存,所以此時CacheStrategy既包含networkRequest也包含cacheResponse

總結

至此撇叁,緩存攔截器就算告一段落了供鸠,走完了這些緩存,返回CacheStrategy對象陨闹,接下來就是一開始我們講的總體流程那里了楞捂。

整體總結如下:

1、如果從緩存獲取的 Response 是null趋厉,那就需要使用網絡請求獲取響應寨闹;

2、如果是Https請求君账,但是又丟失了握手信息鼻忠,那也不能使用緩存,需要進行網絡請求杈绸;

3帖蔓、如果判斷響應碼不能緩存且響應頭有 no-store 標識,那就需要進行網絡請求瞳脓;

4塑娇、如果請求頭有 no-cache 標識或者有 If-Modified-Since/If-None-Match ,那么需要進行網絡請求劫侧;

5埋酬、如果響應頭沒有 no-cache 標識,且緩存時間沒有超過極限時間烧栋,那么可以使用緩存写妥,不需要進行網絡請求;

6审姓、如果緩存過期了珍特,判斷響應頭是否設置 Etag/Last-Modified/Date ,沒有那就直接使用網絡請求否則需要考慮服務器返回304魔吐;并且扎筒,只要需要進行網絡請求莱找,請求頭中就不能包含 only-if-cached ,否則框架直接返回504嗜桌!

緩存攔截器本身主要邏輯其實都在緩存策略中奥溺,攔截器本身邏輯非常簡單,如果確定需要發(fā)起網絡請求骨宠,則進行下一個攔截器 ConnectInterceptor

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末浮定,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子层亿,更是在濱河造成了極大的恐慌桦卒,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件棕所,死亡現(xiàn)場離奇詭異闸盔,居然都是意外死亡悯辙,警方通過查閱死者的電腦和手機琳省,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來躲撰,“玉大人针贬,你說我怎么就攤上這事÷5埃” “怎么了桦他?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長谆棱。 經常有香客問我快压,道長,這世上最難降的妖魔是什么垃瞧? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任蔫劣,我火速辦了婚禮,結果婚禮上个从,老公的妹妹穿的比我還像新娘脉幢。我一直安慰自己,他們只是感情好嗦锐,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布嫌松。 她就那樣靜靜地躺著,像睡著了一般奕污。 火紅的嫁衣襯著肌膚如雪萎羔。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天碳默,我揣著相機與錄音,去河邊找鬼。 笑死蜂绎,一個胖子當著我的面吹牛叨叙,可吹牛的內容都是我干的。 我是一名探鬼主播垫竞,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了砸喻?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤蒋譬,失蹤者是張志新(化名)和其女友劉穎割岛,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體犯助,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡癣漆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了剂买。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片惠爽。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖瞬哼,靈堂內的尸體忽然破棺而出婚肆,到底是詐尸還是另有隱情,我是刑警寧澤坐慰,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布较性,位于F島的核電站,受9級特大地震影響结胀,放射性物質發(fā)生泄漏赞咙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一糟港、第九天 我趴在偏房一處隱蔽的房頂上張望攀操。 院中可真熱鬧,春花似錦着逐、人聲如沸崔赌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽健芭。三九已至,卻和暖如春秀姐,著一層夾襖步出監(jiān)牢的瞬間慈迈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留痒留,地道東北人谴麦。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像伸头,于是被迫代替她去往敵國和親匾效。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容