okhttp的缓存机制

okhttp加入缓存

我们可以通过下面的代码进行实现

  File file = new File(DirUtil.getCacheDir() + File.separator + "okhttp");
        int cacheSize = 10 * 1024 * 1024;
        Cache cache = new Cache(file, cacheSize);
        okHttpClient = new OkHttpClient.Builder().cache(cache).addNetworkInterceptor(new NetCacheInterceptor()).build();

上面的代码我们设置了一个缓存目录okhttp,缓存大小为10M,通过okhttpclient的build传入。

Cache是什么?

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
  )

我们可以知道cache用Lru缓存的一个实现

我们看下他的put

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
    }

    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 {
      editor = cache.edit(key(response.request.url)) ?: return null
      entry.writeTo(editor)
      return RealCacheRequest(editor)
    } catch (_: IOException) {
      abortQuietly(editor)
      return null
    }
  }

我们看下

 try {
      editor = cache.edit(key(response.request.url)) ?: return null
      entry.writeTo(editor)
      return RealCacheRequest(editor)
    } catch (_: IOException) {
      abortQuietly(editor)
      return null
    }
    fun key(url: HttpUrl): String = url.toString().encodeUtf8().md5().hex()

 就是计算httpurl的md5值,然后将数据放入lrucache中,以httpurl的md5为key。

HttpMethod.invalidatesCache(response.request.method)

fun invalidatesCache(method: String): Boolean = (method == "POST" ||
      method == "PATCH" ||
      method == "PUT" ||
      method == "DELETE" ||
      method == "MOVE") // WebDAV

通过上面的代码我们可以知道post默认不缓存

如果我想缓存,怎么处理呢?我们模仿缓存拦截器自定义缓存拦截器,插入拦截器队列即可。

我们知道okhttp有个缓存拦截器,我们就分析这个缓存拦截器

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

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

从lrucache当中获取到相应的缓存

我们看下这段代码

val now = System.currentTimeMillis()

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

 /** 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
    }


 

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

这段代码挺长,我们看下

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

如果当前的缓存数据存在可用的缓存(即没有过期),那么就返回

我们回到CacheInterceptor 

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


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

可以知道如果不需要联网,networkRequest就会为null

我们接着看CacheInterceptor

 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

这个就很简单了,如果现在存在一个缓存,同时服务器返回了304,那么就把现在的缓存再存下,更新下现在缓存的header,请求时间、数据返回时间。

如果缓存不存在,那么就判断现在的数据是否能够存储。如果能就存到cache中。

如何判断的呢?

fun Response.promisesBody(): Boolean {
  // HEAD requests never yield a body regardless of the response headers.
  if (request.method == "HEAD") {
    return false
  }

  val responseCode = code
  if ((responseCode < HTTP_CONTINUE || responseCode >= 200) &&
      responseCode != HTTP_NO_CONTENT &&
      responseCode != HTTP_NOT_MODIFIED) {
    return true
  }

  // If the Content-Length or Transfer-Encoding headers disagree with the response code, the
  // response is malformed. For best compatibility, we honor the headers.
  if (headersContentLength() != -1L ||
      "chunked".equals(header("Transfer-Encoding"), ignoreCase = true)) {
    return true
  }

  return false
}
 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
    }
  }
}

我们看到最后有一行代码

      return !response.cacheControl.noStore && !request.cacheControl.noStore

那么这些值从哪里赋值呢

 @get:JvmName("cacheControl") val cacheControl: CacheControl
    get() {
      var result = lazyCacheControl
      if (result == null) {
        result = CacheControl.parse(headers)
        lazyCacheControl = result
      }
      return result
    }
@JvmStatic
    fun parse(headers: Headers): CacheControl {
      var noCache = false
      var noStore = false
      var maxAgeSeconds = -1
      var sMaxAgeSeconds = -1
      var isPrivate = false
      var isPublic = false
      var mustRevalidate = false
      var maxStaleSeconds = -1
      var minFreshSeconds = -1
      var onlyIfCached = false
      var noTransform = false
      var immutable = false

      var canUseHeaderValue = true
      var headerValue: String? = null

      loop@ for (i in 0 until headers.size) {
        val name = headers.name(i)
        val value = headers.value(i)

        when {
          name.equals("Cache-Control", ignoreCase = true) -> {
            if (headerValue != null) {
              // Multiple cache-control headers means we can't use the raw value.
              canUseHeaderValue = false
            } else {
              headerValue = value
            }
          }
          name.equals("Pragma", ignoreCase = true) -> {
            // Might specify additional cache-control params. We invalidate just in case.
            canUseHeaderValue = false
          }
          else -> {
            continue@loop
          }
        }

        var pos = 0
        while (pos < value.length) {
          val tokenStart = pos
          pos = value.indexOfElement("=,;", pos)
          val directive = value.substring(tokenStart, pos).trim()
          val parameter: String?

          if (pos == value.length || value[pos] == ',' || value[pos] == ';') {
            pos++ // Consume ',' or ';' (if necessary).
            parameter = null
          } else {
            pos++ // Consume '='.
            pos = value.indexOfNonWhitespace(pos)

            if (pos < value.length && value[pos] == '\"') {
              // Quoted string.
              pos++ // Consume '"' open quote.
              val parameterStart = pos
              pos = value.indexOf('"', pos)
              parameter = value.substring(parameterStart, pos)
              pos++ // Consume '"' close quote (if necessary).
            } else {
              // Unquoted string.
              val parameterStart = pos
              pos = value.indexOfElement(",;", pos)
              parameter = value.substring(parameterStart, pos).trim()
            }
          }

          when {
            "no-cache".equals(directive, ignoreCase = true) -> {
              noCache = true
            }
            "no-store".equals(directive, ignoreCase = true) -> {
              noStore = true
            }
            "max-age".equals(directive, ignoreCase = true) -> {
              maxAgeSeconds = parameter.toNonNegativeInt(-1)
            }
            "s-maxage".equals(directive, ignoreCase = true) -> {
              sMaxAgeSeconds = parameter.toNonNegativeInt(-1)
            }
            "private".equals(directive, ignoreCase = true) -> {
              isPrivate = true
            }
            "public".equals(directive, ignoreCase = true) -> {
              isPublic = true
            }
            "must-revalidate".equals(directive, ignoreCase = true) -> {
              mustRevalidate = true
            }
            "max-stale".equals(directive, ignoreCase = true) -> {
              maxStaleSeconds = parameter.toNonNegativeInt(Integer.MAX_VALUE)
            }
            "min-fresh".equals(directive, ignoreCase = true) -> {
              minFreshSeconds = parameter.toNonNegativeInt(-1)
            }
            "only-if-cached".equals(directive, ignoreCase = true) -> {
              onlyIfCached = true
            }
            "no-transform".equals(directive, ignoreCase = true) -> {
              noTransform = true
            }
            "immutable".equals(directive, ignoreCase = true) -> {
              immutable = true
            }
          }
        }
      }

      if (!canUseHeaderValue) {
        headerValue = null
      }

      return CacheControl(noCache, noStore, maxAgeSeconds, sMaxAgeSeconds, isPrivate, isPublic,
          mustRevalidate, maxStaleSeconds, minFreshSeconds, onlyIfCached, noTransform, immutable,
          headerValue)
    }

通过代码我们可以知道对缓存的控制有下面这些

Cache-Control
Pragma
 when {
            "no-cache".equals(directive, ignoreCase = true) -> {
              noCache = true
            }
            "no-store".equals(directive, ignoreCase = true) -> {
              noStore = true
            }
            "max-age".equals(directive, ignoreCase = true) -> {
              maxAgeSeconds = parameter.toNonNegativeInt(-1)
            }
            "s-maxage".equals(directive, ignoreCase = true) -> {
              sMaxAgeSeconds = parameter.toNonNegativeInt(-1)
            }
            "private".equals(directive, ignoreCase = true) -> {
              isPrivate = true
            }
            "public".equals(directive, ignoreCase = true) -> {
              isPublic = true
            }
            "must-revalidate".equals(directive, ignoreCase = true) -> {
              mustRevalidate = true
            }
            "max-stale".equals(directive, ignoreCase = true) -> {
              maxStaleSeconds = parameter.toNonNegativeInt(Integer.MAX_VALUE)
            }
            "min-fresh".equals(directive, ignoreCase = true) -> {
              minFreshSeconds = parameter.toNonNegativeInt(-1)
            }
            "only-if-cached".equals(directive, ignoreCase = true) -> {
              onlyIfCached = true
            }
            "no-transform".equals(directive, ignoreCase = true) -> {
              noTransform = true
            }
            "immutable".equals(directive, ignoreCase = true) -> {
              immutable = true
            }
          }

我们通过上面的知识自己来实现一个拦截器,使得所有的内容缓存时间是60s

package com.yuanxuzhen.testandroid.okhttp;

import org.jetbrains.annotations.NotNull;

import java.io.IOException;

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class NetCacheInterceptor implements Interceptor {
    @NotNull
    @Override
    public Response intercept(@NotNull Chain chain) throws IOException {
        Request request = chain.request();
        Response originResponse = chain.proceed(request);

        //设置响应的缓存时间为60秒,即设置Cache-Control头,并移除pragma消息头,因为pragma也是控制缓存的一个消息头属性
        originResponse = originResponse.newBuilder()
                .removeHeader("pragma")
                .header("Cache-Control", "max-age=60")
                .build();

        return originResponse;
    }
}
package com.yuanxuzhen.testandroid.okhttp;

import java.io.File;

import okhttp3.Cache;
import okhttp3.OkHttpClient;

public class OKHttpManager {
    private static volatile OKHttpManager instance;
    private OkHttpClient okHttpClient;
    private OKHttpManager() {
        File file = new File(DirUtil.getCacheDir() + File.separator + "okhttp");
        int cacheSize = 10 * 1024 * 1024;
        Cache cache = new Cache(file, cacheSize);
        okHttpClient = new OkHttpClient.Builder().cache(cache).addNetworkInterceptor(new NetCacheInterceptor()).build();
    }

    public static OKHttpManager getInstance(){
        if(instance == null){
            synchronized (OKHttpManager.class){
                if(instance == null){
                    instance = new OKHttpManager();
                }
            }
        }
        return instance;
    }

    public OkHttpClient getOkHttpClient() {
        return okHttpClient;
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值