三、OkHttp_缓存

news2025/2/12 1:00:17

一、OKHttp的缓存逻辑

OKHttp 把重复请求的数据缓存在本地,并设置超时时间,在规定时间内,客户端不再向远程请求数据,而是直接从本地缓存中取数据。

  • 一来提高了响应速度,
  • 二来节省了网络带宽(也就是节省了钱)

二、Http协议的缓存机制

HTTP缓存机制是Web浏览器、代理服务器和Web服务器之间的协议,用于提高Web性能并减少网络带宽的使用。缓存机制能够在再次请求相同资源时,避免网络数据的重复传输,从而提高网络效率和响应速度。

1.1.HTTP缓存机制主要分为两类:

客户端缓存和服务器缓存。

  • 客户端缓存是指在Web浏览器中缓存常用资源,如图片、JavaScript文件和样式表等。
  • 服务器缓存是指在代理服务器或Web服务器上缓存动态生成的页面或页面元素。

1.2.HTTP缓存机制中有两个重要的概念:

缓存有效期和缓存验证。

  • 缓存有效期是指缓存的资源能够被使用的时间长度。如果在有效期内再次请求相同的资源,缓存就会直接使用缓存中的数据,而不必再向服务器请求数据。
  • 缓存验证是指在缓存过期之后,如果缓存仍然需要使用,就必须向服务器发送一个请求,询问服务器是否有更新的数据。

1.3.HTTP缓存机制可以通过HTTP头部信息来控制缓存的行为。

常用的HTTP头部信息有:

  • Cache-Control:用于控制缓存的行为,包括缓存有效期、缓存验证和缓存控制等。
  • Expires:缓存资源的过期时间,是一个固定的时间点,一旦超过这个时间点就认为缓存已经过期。
  • Last-Modified:资源的最后修改时间,用于判断资源是否有更新。
  • ETag:一个标识符,用于判断资源是否有更新。

1.4.Http缓存相关控制的请求头

1.4.1.Cache-Control
  • no-cache:不做缓存
  • max-age:这个参数告诉浏览器将页面缓存多长时间,超过这个时间后才再次向服务器发起请求检查页面是否有更新。对于静态的页面,比如图片、CSS、Javascript,一般都不大变更,因此通常我们将存储这些内容的时间设置为较长的时间,这样浏览器会不会向浏览器反复发起请求,也不会去检查是否更新了。
  • no-store:数据不在硬盘中临时保存,这对需要保密的内容比较重要。
  • max-stale:指示客户机可以接收超出超时期间的响应消息。如果指定- max-stale消息的值,那么客户机可以接收超出超时期指定值之内的响应消息。
1.4.2.Expires
  • Expires指定响应过期的日期和时间,其格式为GMT格式的字符串,例如:
Expires: Wed, 21 Oct 2023 07:28:00 GMT

该响应告知客户端,在过期日期之后,该响应将被视为过时,需要重新请求。过期日期应为服务器当前时间和数据生命周期之和。

Expires头的主要缺点在于,它需要客户端和服务器之间的时间同步。如果两者的时间不同步,就可能导致缓存行为不正确。

通常,更推荐使用Cache-Control头来控制响应的缓存行为,因为它提供了更多的灵活性和精度。在Cache-Control和Expires同时存在的情况下,Cache-Control优先级更高。

1.4.3.Last-Modified/If-Modified-Since

HTTP响应头Last-Modified指示资源的最后修改时间。需要配合Cache-Control使用:

  • Last-Modified:标示这个响应资源的最后修改时间。web服务器在响应请求时,告诉浏览器资源的最后修改时间。
  • If-Modified-Since:当资源过期时(使用Cache-Control标识的max-age),发现资源具有Last-Modified声明,则再次向web服务器请求时带上头 If-Modified-Since,表示请求时间。
    web服务器收到请求后发现有头If-Modified-Since 则与被请求资源的最后修改时间进行比对。
    若最后修改时间较新,说明资源又被改动过,则响应整片资源内容(写在响应消息包体内),HTTP 200;
    若最后修改时间较旧,说明资源无新修改,则响应HTTP 304 (无需包体,节省浏览),告知浏览器继续使用所保存的cache。
Last-Modified: Fri, 29 Oct 2021 08:50:00 GMT

这表示资源在GMT时间2021年10月29日08:50:00进行了最后修改。

1.4.4.Etag/If-None-Match

这个也需要配合Cache-Control使用

  • Etag对应请求的资源在服务器中的唯一标识(具体规则由服务器决定),比如一张图片,它在服务器中的标识为ETag: W/”ACXbWXd1n0CGMtAd65PcoA==”。

  • If-None-Match 如果浏览器在Cache-Control:max-age=60设置的时间超时后,发现消息头中还设置了Etag值。然后,浏览器会再次向服务器请求数据并添加In-None-Match消息头,它的值就是之前Etag值。服务器通过Etag来定位资源文件,根据它是否更新的情况给浏览器返回200或者是304。

Etag机制比Last-Modified精确度更高,如果两者同时设置的话,Etag优先级更高。

1.4.5.Pragma

Pragma头域用来包含实现特定的指令,最常用的是Pragma:no-cache。
在HTTP/1.1协议中,它的含义和Cache- Control:no-cache相同。

三、Http缓存机制流程图

在这里插入图片描述

四、OKHttp设置缓存

参考官方文档

五、CacheInterceptor源码分析

CacheInterceptor 用于处理缓存逻辑。它的主要功能是根据缓存策略决定是否使用缓存的响应,以及是否将新的响应存储到缓存中。

  • 检查是否有缓存:CacheInterceptor首先会检查是否存在与当前请求匹配的缓存响应。如果存在缓存响应并且未过期,将直接返回缓存响应,而不会发送请求到服务器。

  • 发送网络请求:如果没有可用的缓存响应或缓存响应已过期,CacheInterceptor会继续将请求传递给下一个拦截器,发送网络请求到服务器获取新的响应。

  • 处理服务器返回的响应:当服务器返回响应后,CacheInterceptor会根据缓存策略决定是否将该响应存储到缓存中。如果响应满足缓存条件,CacheInterceptor会将其存储到缓存中,以便后续请求可以使用缓存响应。

// 通过构造方法将 Okhttp设置 cache传递到 CacheInterceptor 中
class CacheInterceptor(internal val cache: Cache?) : Interceptor {

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {      
    val call = chain.call()
      // 1、获取缓存候选者。如何获取???待分析
    val cacheCandidate = cache?.get(chain.request())

    val now = System.currentTimeMillis()
    // 按道理讲,接下来应该判断 是否命中缓存,从而决定是直接请求网络数据还是判断缓存是否过期,
    // 但 CacheInterceptor 中并没有这么做,而是构建了一个 CacheStrategy,实际上它是将缓存的合法性、
   // 缓存是否过期等判断全部放到 CacheStrategy 的构建过程中来做了。
   // // 2、根据 当前时间、请求request、以及缓存候选者 获取策略
    val strategy = CacheStrategy.Factory(now, chain.request(), cacheCandidate).compute()
    // 3、获取网络请求Request 和缓存Response
    val networkRequest = strategy.networkRequest
    val cacheResponse = strategy.cacheResponse
    
    cache?.trackResponse(strategy)
    val listener = (call as? RealCall)?.eventListener ?: EventListener.NONE
    // 4、缓存候选不适用。关闭它。
    if (cacheCandidate != null && cacheResponse == null) {
      cacheCandidate.body?.closeQuietly()
    }

    // 5、如果我们被禁止使用网络,但缓存并不存在时,构造一个 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)
          }
    }

    // 6、缓存任然未过期,不需要使用网络请求,直接返回缓存。
    if (networkRequest == null) {
      return cacheResponse!!.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build().also {
            listener.cacheHit(call, it)
          }
    }
   // 当程序运行到这里时,有两种可能:
   // 一是直接发起网络请求,获取原始数据(networkRequest != null && cacheResponse == null);
   // 二是需要进行条件验证(networkRequest != null && cacheResponse != null)。
    if (cacheResponse != null) {
      listener.cacheConditionalHit(call, cacheResponse)
    } else if (cache != null) {
      listener.cacheMiss(call)
    }
    
    var networkResponse: Response? = null
    try {
      // 7、至此需要发起真正的网络请求:即 networkRequset !=null 
      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()
      }
    }

    // 8、发起网络请求后,即 networkRequset !=null 而且  cacheResponse != null
    // 如果服务器返回 304 Not Modified,则表示缓存未修改,任然可用,更新缓存的 header;
    // 否则表示缓存过期,服务器会直接返回原始数据
    if (cacheResponse != null) {
        //  8.1、判断网络请求的响应码是否是304?
      if (networkResponse?.code == HTTP_NOT_MODIFIED) {
          // 8.1.1、如果响应码是 304 ,则 可以使用缓存 
        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()
        // 8.1.1.1、更新缓存
        cache.update(cacheResponse, response)
        return response.also {
          listener.cacheHit(call, it)
        }
      } else {
          // 8.1.2、如果响应码不是 304,则关闭缓存
        cacheResponse.body?.closeQuietly()
      }
    }
    // 9、构建网络响应response
    val response = networkResponse!!.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build()

    if (cache != null) {
        // 10、如果响应可以被缓存的话,保存缓存。
        // 而且 网络响码符合规则 
        // 而且 response 、和networkRequest是 可以缓存的
      if (response.promisesBody() && CacheStrategy.isCacheable(response, networkRequest)) {
        // 10.1、保存、设置、put新的缓存
        val cacheRequest = cache.put(response)
        // 10.2 、写入缓存,并返回 response
        return cacheWritingResponse(cacheRequest, response).also {
          if (cacheResponse != null) {
            // This will log a conditional cache miss only.
            listener.cacheMiss(call)
          }
        }
      }
        // 11、判断请求方法是否是遗弃缓存??? 
        // invalidatesCache(method: String): Boolean = (method == "POST" || method == "PATCH" 
        // || method == "PUT" || method == "DELETE" ||    method == "MOVE") 
      if (HttpMethod.invalidatesCache(networkRequest.method)) {
        try {
            // 这些请求方法 则移除缓存
          cache.remove(networkRequest)
        } catch (_: IOException) {
          // The cache cannot be written.
        }
      }
    }
    // 12 、返回response
    return response
  }

通过对CacheInterceptor的流程梳理,有几个问题需要再次深入研究分析一下:

  • 缓存的获取逻辑
  • 缓存策略构建缓存的过程
  • 缓存更新过程
  • 缓存添加过程
  • 缓存写入过程

5.1.缓存的获取逻辑

val cacheCandidate = cache?.get(chain.request())分析:
Cache - get()方法源码:

internal fun get(request: Request): Response? {
    // 分析一
  val key = key(request.url)
  // 分析二  
  // 表示一个缓存快照,即表示一个缓存条目的快照状态。
    // 在OkHttp的缓存系统中,当从缓存中获取数据时,会返回一个Snapshot对象。
    // Snapshot对象提供了对缓存数据的访问和操作方法,
    // 包括读取缓存数据、获取缓存数据的输入流、获取缓存数据的大小等。
  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
}
分析一、通过请求url获取 存储的key
key = url.toString().encodeUtf8().md5().hex()
分析二、通过key从OkHttp自定义的DiskLruCache获取snapshot:快照

val snapshot: DiskLruCache.Snapshot = try {
cache[key] ?: return null

operator fun get(key: String): Snapshot? {
	//  2.1 初始化日志文件,主要是从journal文件中读取内容 分装到 Entry(对象中)
	//  并将其添加到 lruEntries
    initialize()
	// 2.2 验证key是否符合规范:regex [a-z0-9_-]{1,120}
    validateKey(key)
    // 从lruEntries 对象中读取指定的 entry
    val entry = lruEntries[key] ?: return null
    // 2.4 用于获取缓存条目的快照。
    // 快照是缓存条目的一个副本,它允许我们读取缓存的数据而不影响原始缓存条目。
    val snapshot = entry.snapshot() ?: return null

    redundantOpCount++
    // 2.5 在日志文件中写入,读取的记录
    journalWriter!!.writeUtf8(READ)
        .writeByte(' '.toInt())
        .writeUtf8(key)
        .writeByte('\n'.toInt())
    if (journalRebuildRequired()) {
      cleanupQueue.schedule(cleanupTask)
    }
    return snapshot
  }

分析2.1 initialize

fun initialize() {
    this.assertThreadHoldsLock()
	// 已经初始化 ,则return
    if (initialized) {
      return // Already initialized.
    }
	//2.1.1 备份文件存在就使用
    if (fileSystem.exists(journalFileBackup)) {
      // 如果日志文件也存在,则删除备份文件
      if (fileSystem.exists(journalFile)) {
       // 日志文件存在 删除备份文件
        fileSystem.delete(journalFileBackup)
      } else {
      	//  日志文件不存在,将备份文件命名为日志文件。
        fileSystem.rename(journalFileBackup, journalFile)
      }
    }

    civilizedFileSystem = fileSystem.isCivilized(journalFileBackup)

    // 2.1.2 判断日志文件是否存在
    if (fileSystem.exists(journalFile)) {
      try {
         // 2.1.2.1、读取日志文件, 并将 读取的内容封装到 Entry()对象中
         // 并添加到 存放到 lruEntries中
        readJournal()
        // 2.1.2.2、作为打开缓存的一部分,计算初始大小并收集垃圾。
        // 脏条目被认为是不一致的,将被删除。
        processJournal()
        initialized = true
        return
      } catch (journalIsCorrupt: IOException) {
        Platform.get().log(
            "DiskLruCache $directory is corrupt: ${journalIsCorrupt.message}, removing",
            WARN,
            journalIsCorrupt)
      }
      try {
      // 2.1.3 日志文件不存在,或者发生异常
	 // 缓存已经损坏、尝试删除该目录内容。可以抛出异常,因为存在严重的文件系统问题
        delete()
      } finally {
        closed = false
      }
    }
	// 创建新的日志,如果当前的日志文件不存在 ,则替换
    rebuildJournal()

    initialized = true
  }

分析 2.1.2.1 readJournal

  private fun readJournal() {
    fileSystem.source(journalFile).buffer().use { source ->
        // 读取日志文件的头部信息
        val magic = source.readUtf8LineStrict() // 魔数
        val version = source.readUtf8LineStrict() // 版本号
        val appVersionString = source.readUtf8LineStrict() // 应用程序版本号
        val valueCountString = source.readUtf8LineStrict() // 键值对数量
        val blank = source.readUtf8LineStrict() // 空行

        // 检查日志文件头部的有效性
        if (MAGIC != magic ||
            VERSION_1 != version ||
            appVersion.toString() != appVersionString ||
            valueCount.toString() != valueCountString ||
            blank.isNotEmpty()) {
            throw IOException("日志文件头部异常: [$magic, $version, $valueCountString, $blank]")
        }

        var lineCount = 0
        // 逐行读取日志文件的内容
        while (true) {
            try {
                // 用于解析并处理日志文件中的一行内容。
                // 在DiskLruCache中,日志文件用于记录缓存条目的状态和操作。
                readJournalLine(source.readUtf8LineStrict())
                lineCount++
            } catch (_: EOFException) {
                break // 日志文件结束
            }
        }

        // 计算日志文件中冗余操作的数量
        redundantOpCount = lineCount - lruEntries.size

        // 如果源文件未读取完整,说明最后一行被截断,需要在追加内容之前重新构建日志文件
        if (!source.exhausted()) {
            rebuildJournal()
        } else {
            // 创建一个新的日志文件写入器,用于后续写入操作
            journalWriter = newJournalWriter()
        }
    }
}

分析 2.1.2.2 的方法 processJournal

/**
 * 处理日志文件,清理无效的缓存条目。
 */
private fun processJournal() {
    // 删除临时的日志文件
    fileSystem.delete(journalFileTmp)

    // 遍历缓存条目,清理无效的条目并计算缓存大小
    val iterator = lruEntries.values.iterator()
    while (iterator.hasNext()) {
        val entry = iterator.next()

        // 如果当前缓存条目没有正在进行的编辑操作,则计算其大小
        if (entry.currentEditor == null) {
            for (t in 0 until valueCount) {
                size += entry.lengths[t]
            }
        } else {
            // 如果当前缓存条目有正在进行的编辑操作,则清理相关文件并移除条目
            entry.currentEditor = null
            for (t in 0 until valueCount) {
                fileSystem.delete(entry.cleanFiles[t])
                fileSystem.delete(entry.dirtyFiles[t])
            }
            iterator.remove()
        }
    }
}
分析三、从缓存快照中取出第一个Source,并构建 Cache.Entry

// 读取:url 、requestMethod、varyHeaders、protocol、code、message、sentRequestMillis、receivedResponseMillis 、responseHeaders 、https的相关信息 …
// 就是读取请求信息
val entry: Entry = try {
Entry(snapshot.getSource(ENTRY_METADATA))

 val source = rawSource.buffer()
 url = source.readUtf8LineStrict()
 requestMethod = source.readUtf8LineStrict()
 ...
 
分析四、通过entry的值构建 response

-> val response = entry.response(snapshot)

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()
    }
// 获取构建缓存响应body
-> CacheResponseBody(snapshot, contentType, contentLength)
     // 获得缓存快照source中的第二的文件流
     -> val source = snapshot.getSource(ENTRY_BODY)

六、缓存策略的计算过程

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

-> CacheStrategy.Factory()
// 1、通过读取缓存中的 heads 设置成员变量:
//    Date、expires、lastModified、etag、ageSeconds
	-> init()
	// 2、使用cacherresponse返回一个策略来满足请求。
	-> compute()
		// 2.1 
		-> val candidate = computeCandidate()
		// 2.1.1、没有缓存响应
		-> if (cacheResponse == null)
			-> return CacheStrategy(request, null)
		// 2.1.2、如果缺少必要的握手,则删除缓存的响应。
		-> if (request.isHttps && cacheResponse.handshake == null)
			-> return CacheStrategy(request, null)
		// 2.1.3、如果不应该存储此响应,则永远不应该将其用作响应源。
		// 只要持久性存储运行良好且规则不变,这种检查就应该是多余的。
		-> if (!isCacheable(cacheResponse, request))
			// 如果响应可以存储以供以后服务另一个请求,则返回true。
			-> isCacheable()
				// 通过缓存的响应码判断是否能缓存
				-> when (response.code)
					-> return true/false
				// 其次通过 请求的 和缓存的 cacheControl noStore 字段判断
				-> return !response.cacheControl.noStore && !request.cacheControl.noStore
			// 不满足受用缓存的条件
			-> return CacheStrategy(request, null)
			// 2.1.4、判断 请求的 和缓存的 cacheControl noStore 字段判断
			-> if (requestCaching.noCache || hasConditions(request))
				-> return CacheStrategy(request, null)
			// 2.1.5、超时判断
			-> if (!responseCaching.noCache && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) 
				-> return CacheStrategy(null, builder.build())
			// 2.1.6、找到要添加到请求的条件。如果满足条件,则不会传输响应体。
			// 即将符合条件的请求头添加到请求中
			-> etag != null -> {conditionName = "If-None-Match"  conditionValue = etag }
			-> val conditionalRequest = request.newBuilder().headers(conditionalRequestHeaders.build()).build()
			-> return CacheStrategy(conditionalRequest, cacheResponse)

		// 2.2、我们被禁止使用网络,缓存不足。
		-> if (candidate.networkRequest != null && request.cacheControl.onlyIfCached)
		// 2.3、返回计算出来的CacheStrategy
		-> return candidate

七、缓存更新过程

cache.update(cacheResponse, response)

internal fun update(cached: Response, network: Response) {
	// 分析一
    val entry = Entry(network)
    // 分析二
    val snapshot = (cached.body as CacheResponseBody).snapshot
    var editor: DiskLruCache.Editor? = null
    try {
    // 分析三
      editor = snapshot.edit() ?: return // edit() returns null if snapshot is not current.
      entry.writeTo(editor)
      editor.commit()
    } catch (_: IOException) {
      abortQuietly(editor)
    }
  }

分析1、通过网络请求结果 构建, 初始化Entry 成员变量 url、headers、Method、code、message…

-> val entry = Cache.Entry(network)

分析2、获得 缓存body的快照

-> val snapshot = (cached.body as CacheResponseBody).snapshot

分析3、返回此快照条目的编辑器,如果自此快照创建以来条目发生了更改或正在进行另一项编辑,则返回null。

-> editor = snapshot.edit()
	// 3.1  key:string加密值  ,sequenceNumber:最近提交到此条目的编辑的序列号。
	-> this@DiskLruCache.edit(key, sequenceNumber)
		//  已分析过。
		-> initialize()
		// 3.1.1、获取key对应的文件 .0 .1结尾
		-> var entry: Entry? = lruEntries[key]
		// 3.1.2、快照过期
		-> if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER &&
        (entry == null || entry.sequenceNumber != expectedSequenceNumber))
			-> return null
		// 3.1.3、另一项编辑正在进行中。
		-> if (entry?.currentEditor != null) 
			-> return null
		// 3.1.4、我们无法写入该文件,因为阅读器仍在读取它。
		-> if (entry != null && entry.lockingSourceCount != 0)
			->  return null
		// 3.1.5、如果 日志重建失败,日志写入器将不再活动,这意味着我们将不再活动 能够记录编辑,导致文件泄漏。在这           
		// 两种情况下,我们都希望重试清理 这样我们就能摆脱这种状态了!
		-> if (mostRecentTrimFailed || mostRecentRebuildFailed) 
			-> cleanupQueue.schedule(cleanupTask)
			-> return null
		// 3.1.6、在创建文件之前刷新日志,以防止文件泄漏。
		-> journalWriter.writeUtf8(DIRTY)
		-> journalWriter.flush()
		// 3.1.7、创建缓存文件entry
		-> if (entry == null)
			//  创建包含 缓存文件信息 的 DiskLruCache.Entry
			-> entry = Entry(key)
			-> lruEntries[key] = entry
		// 3.1.8、构建editor
		-> val editor = Editor(entry)
		-> entry.currentEditor = editor
		-> return editor

4、将请求头的信息 METADATA 写入文件 .0文件

-> entry.writeTo(editor)

5、提交此编辑,以便读者可以看到。这将释放编辑锁,以便对同一密钥启动另一个编辑。

-> editor.commit()

八、缓存put存放的过程

 val cacheRequest = cache.put(response)
// 1、通过Http请求方式,验证是否支持缓存
// post、patch、put、move
-> if (HttpMethod.invalidatesCache(response.request.method))
	-> remove(response.request) // 下面分析
// 2、不要缓存非get响应。
// 技术上我们允许缓存HEAD请求和一些POST请求,但是这样做的复杂性很高,好处很低。
-> if (requestMethod != "GET")
	-> return null
// 3、如果Vary标头包含星号,则返回true。这样的响应不能被缓存。
-> if (response.hasVaryAll())
	->  return null
// 4、构造Cache.Entry
-> val entry = Entry(response)
-> editor = cache.edit(key(response.request.url)) ?: return null
	-> DiskLruCache.edit()
		// 初始化 缓存的 Journal 文件,并创建 journalWriter 。一分析过
		-> initialize()
		-> var entry: Entry? = lruEntries[key]
		// 在创建文件之前刷新日志,以防止文件泄漏。记录操作日志
		-> val journalWriter = this.journalWriter!!
		-> journalWriter.writeUtf8(DIRTY)... 
		// 创建DiskLruCache.Entry
		-> if (entry == null)
			-> entry = DiskLruCache.Entry(key)
				-> init()
					// valueCount 为2 ,固定值。即创建以kay.0  key.1的两个缓存文件
					-> val fileBuilder = StringBuilder(key).append('.')
						-> for (i in 0 until valueCount)
								-> fileBuilder.append(i)
								->  cleanFiles += File(directory, fileBuilder.toString())
			// 存放 DiskLruCache.Entry
			-> lruEntries[key] = entry
			// 创建Editor
			-> val editor = Editor(entry)
			// 将 DiskLruCache.Entry 和 editor 绑定
			-> entry.currentEditor = editor
// 5、将 写入请求Header等信息
-> entry.writeTo(editor)
	-> editor.newSink(ENTRY_METADATA)
		// 获取 以key.0结尾的缓存文件
		-> val dirtyFile = entry.dirtyFiles[index]
		-> sink = fileSystem.sink(dirtyFile)
	// 写入头信息
	-> sink.writeUtf8(url).writeByte('\n'.toInt())
-> 
// 6、返回 保存的结果信息
-> return RealCacheRequest(editor)
	// 获取 以key.1结尾的缓存文件
	-> val cacheOut: Sink = editor.newSink(ENTRY_BODY)
	// 关联Body 流
	-> RealCacheRequest.body = ForwardingSink(cacheOut)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/645887.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何使用uni-app开发微信小程序

web前端-基于uniapp的微信小程序项目 起步uni-app简介开发工具下载 HBuilderX安装 HBuilderX安装 scss/sass 编译快捷键方案切换修改编辑器的基本设置新建uni-app项目把项目运行到微信开发者工具 scss语法学习安装相关插件和配置基础格式选择器的嵌套父选择器后面添加内容 &…

麒麟系统在线安装docker(x86/arm)

文章目录 一、查看系统版本&#xff0c;确认版本二、查看系统架构三、下载安装docker-ceX86架构安装aarch64架构 一、查看系统版本&#xff0c;确认版本 [rootlocalhost ~]# cat /etc/kylin-release Kylin Linux Advanced Server release V10 (Sword)二、查看系统架构 [root…

Windows内存诊断工具卡住无响应怎么办?

Windows内存诊断工具是一个有用的程序&#xff0c;用于检查您的RAM是否存在潜在问题。当系统性能降低、频繁冻结或者蓝屏时&#xff0c;用户可以通过内存诊断工具检查和修复内存方面的问题。该工具具有三种测试模式&#xff0c;基本、标准和扩展模式&#xff0c;其中扩展模式下…

在UOS上安装及更新恒辉桌面软件

UOS作为一颗冉冉升起的新星&#xff0c;吸引了众多国内用户及厂商的目光。 而新的系统势必面临着一个问题——软件及应用的适配性。各个软件厂商需要单独为此适配产品。 2022年2月&#xff0c;支持全四路国产CPU&#xff08;x86架构、arm架构、龙芯、申威&#xff09;的数据库…

chatgpt赋能python:Python如何打印结果

Python如何打印结果 Python是一种高级编程语言, 它也是一种解释型语言&#xff0c;也就是说&#xff0c;程序员不必把Python代码编译成机器码或者字节码。它利用缩进来表示块结构&#xff0c;同时使用一些特殊的关键字来定义变量和类等。 在Python中&#xff0c;有多种方式来…

算法课设 戳气球问题实验报告 动态规划

戳气球实验报告 目录 一、题目 二、分析原问题并做调整 三、分析子问题及其递推关系 四、确定dp数组的计算顺序 五、复杂度分析 六、具体实现代码 七、填表示例寻找最优解和最优方案 八、总结 九、致谢 一、题目 有n个气球&#xff0c;编号为0到n-1&#xff0c;每个…

管理类联考——逻辑——知识篇——第三章 三段论(考2题)(以性质命题为基础,最常用推理)

第三章 三段论&#xff08;考2题&#xff09;&#xff08;以性质命题为基础&#xff0c;最常用推理&#xff09; 一、三段论的基本结构 基本结构1&#xff08;最简单&#xff0c;不考&#xff09;&#xff1a; 所有A是B 所有B是C 得&#xff1a;所有A是C 基本结构2&#xff…

网络安全系统教程+学习路线(自学笔记)

一、什么是网络安全 网络安全可以基于攻击和防御视角来分类&#xff0c;我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术&#xff0c;而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 无论网络、Web、移动、桌面、云等哪个领域&#xff0c;都有攻与防两面…

MySQL查询优化大揭秘!看这些关键数据,让你的数据库速度飞起来!

大家好&#xff0c;我是小米&#xff0c;今天给大家分享一些关于MySQL查询优化的干货。在数据库开发和维护中&#xff0c;优化查询是至关重要的一环。通过合理的优化&#xff0c;我们可以让数据库的查询速度事半功倍。那么&#xff0c;在MySQL的查询计划中&#xff0c;有哪些关…

Spring五大类注解和方法注解

1.配置(重要)2.添加五大类注解2.1 Controller&#xff08;控制器存储&#xff09;2.2 Service&#xff08;服务存储&#xff09;2.3 repository&#xff08;仓库存储&#xff09;2.4 Component&#xff08;组件存储&#xff09;2.5 Configuration&#xff08;配置存储&#xff…

【Python】基础内容

简介 面向对象&#xff0c;解释型的编程语言使用缩进作为逻辑层次 运行效率较低 单行注释&#xff1a;以#开头&#xff1a;#注释内容多行注释&#xff1a;以一对三个双引号引起来的内容&#xff1a; “”“注释内容”“” 数据类型 type(被查看类型的数据)&#xff1a;查看…

KETTLE Driver class ‘org.gjt.mm.mysql.Driver‘ could not be found

kettle链接mysql&#xff1a;抛出异常 Driver class org.gjt.mm.mysql.Driver could not be found 这是因为你没有下载对应的mysql驱动程序包&#xff08;DRIVER.jar&#xff09;到你的kettle下&#xff1a; 1 查看你的mysql版本 C:\Users\22077>mysql --version mysql …

快速拼接字符串的新类StringJoiner~

初识StringJoiner类&#xff1a; StringJoiner 是 Java 8 新增的一个类&#xff0c;它不仅提供了一种快速、方便地将多个字符串拼接成一个字符串的方法&#xff0c;并且在拼接之时还可以指定分隔符、前缀和后缀&#xff0c;以及添加多个字符串&#xff0c;最终输出拼接后的字符…

9-基于stm32的MAX31865铂电阻PT100测温全套资料(原理图+教程+程序)

编号: 009 本项目可以通过PT100测温&#xff0c;测温范围为: -200~420C&#xff0c;采用1.8寸OLED显示该资料已经过实物验证&#xff0c;实物中是通过触发GPIO来测量当前的温度&#xff0c;程序注释非常详细&#xff0c;容易上手 经过实验验证&#xff0c;切实可行!配备详细代码…

p5.js 到底怎么设置背景图?

theme: smartblue 本文简介 点赞 关注 收藏 学会了 在 《p5.js 光速入门》 里我们学过加载图片元素&#xff0c;学过过背景色的用法&#xff0c;但当时没提到背景图要怎么使用。 本文就把背景图这部分内容补充完整&#xff0c;并且会提到在 p5.js 里使用背景图的一些注意点。…

森泰克sumtak控制器维修伺服驱动器维修SQ-12

日本森泰克sumtak控制器维修全系列型号。 控制器常见维修故障&#xff1a;短路&#xff0c;模块损坏&#xff0c;带不动负载&#xff0c;主轴准备未绪&#xff0c;驱动器未使能&#xff0c;编码器故障&#xff0c;主轴驱动模块故障&#xff0c;输出电压低&#xff0c;红色灯亮…

Java创建线程的四种方式和线程的生命周期(面试题彻底搞懂)

方式一&#xff1a;继承Thread类的方式&#xff1a; 创建一个继承于Thread类的子类 重写Thread类的run() --> 将此线程执行的操作声明在run()中 创建Thread类的子类的对象 通过此对象调用start()&#xff1a;①启动当前线程 ② 调用当前线程的run() 说明两个问题&#…

百度CDN配置TLS

概述 为了保障您互联网通信的安全性和数据完整性&#xff0c;百度智能云CDN提供TLS版本控制功能。您可以根据不同域名的需求&#xff0c;灵活地配置TLS协议版本。 TLS&#xff08;Transport Layer Security&#xff09;即安全传输层协议&#xff0c;在两个通信应用程序之间提…

关于Dockerfile的优化

如今各个公有镜像仓库中已经包含了成千上万的镜像文件&#xff0c;但并不是所有的镜像都是精简高效的。很多初学者刚开始都习惯使用FROM centos然后RUN 一堆yum install&#xff0c;这样还停留在虚拟机层面的使用&#xff0c;这样创建出来的镜像往往体积比较大。其实我们可以参…

Vmware 设置固定ip地址--桥接模式

前言&#xff1a; 若虚拟机没有设置固定ip地址&#xff0c;每次关机重启后都会更新ip地址。导致连接工具得跟着一起修改&#xff0c;每次修改很烦。 之前使用NAT模式&#xff0c;因为使用此模式后&#xff0c;每次打开网页都会转几秒钟后才会显示网页。所以才使用桥接模式&…