作者简介:00后,22年刚刚毕业,一枚在鹅厂搬砖的程序员。

前置任务:本篇是CacheInterceptor的补充文章,重点讲解OkHttp的缓存类,读者最好也了解下,虽然不太影响后续文章的阅读,也希望读者在阅读之前已经对其进行了简单研究。

学习目标:学习OkHttp的缓存策略,以及缓存类的增删改查方法的细节。

创作初衷:学习OkHttp的原理,阅读Kotlin框架源码,提高自己对Kotlin代码的阅读能力。为了读代码而读代码,笔者知道这是不对的,但作为应届生,提高阅读源码的能力笔者认为还是很重要的。


文章目录

  • OkHttp第三篇-Cache缓存类详解
    • DiskLruCache的初始化
      • 日志格式分析
      • 日志文件的解析
      • DiskLruCache.Entry
      • 总结
      • 输入流的装饰者结构
      • 响应体文件的写入
        • 编码格式的确认
        • 数据的真正读取
        • 最内层的输入流
        • 写入头数据
        • 写入正文数据
      • 总结
      • Snapshot的创建
      • Cache.Entry的创建
      • 响应的创建
      • 响应体文件数据读取
    • 文章开头问题的答案
    • 初衷

OkHttp第三篇-Cache缓存类详解

此篇文章重点分析OkHttp如何实现的缓存.

研究内容:

  • OkHttp的缓存策略是什么?
  • 缓存日志是什么?
  • 缓存文件是如何生成的?
  • 多个缓存文件类型是如何转换的?
  • 缓存文件是如何读取的?
  • 缓存的增删改查是如何运行的?

抛开拦截器和协议内容,开始分析。

若想使用缓存需要在创建Client时指定cache

创建方式如下:

val okHttpClient = OkHttpClient().newBuilder().cache(Cache(File(this.getExternalCacheDir(), "okhttpcache"), 10 * 1024 * 1024)) //看下分析Cache的构造.build()

当创建完Cache,在后续创建缓存拦截器时,会将此Cache传递过去,增删改查则使用此Cache

第二步指定Request的缓存策略,在Cache的注释中包含了各种各样的场景和用法,读者可自行查阅。

下面开始分析Cache

Cache

class Cache internal constructor(directory: File,maxSize: Long,fileSystem: FileSystem
) : Closeable, Flushable {// 缓存最最重要的属性,看名字也知道缓存策略为硬盘LRU缓存,下述分析DiskLruCacheinternal val cache = DiskLruCache(fileSystem = fileSystem,          // 主机的本地文件系统,内部使用OKIO实现IO操作directory = directory,            // 缓存目录appVersion = VERSION,              // 版本valueCount = ENTRY_COUNT,         // 缓存文件的类型,此处是2,因为一条缓存映射两个实体文件头和体maxSize = maxSize,                  // 缓存大小taskRunner = TaskRunner.INSTANCE  // 日志追踪线程池)constructor(directory: File, maxSize: Long) : this(directory, maxSize, FileSystem.SYSTEM)//文章开头的创建Cache的方式调用的则是此构造, 最终会调用到主构造...//省略增删改查的具体代码,在下个小节分析}

DiskLruCache的初始化

本节研究的目标则是DiskLruCache的初始化, 以及日志文件的解析。

先分析DiskLruCache的构造和内部的一些属性

DiskLruCache

class DiskLruCache internal constructor(internal val fileSystem: FileSystem, // 参数的含义在Cache中已经分析val directory: File,private val appVersion: Int,internal val valueCount: Int,maxSize: Long,taskRunner: TaskRunner
) : Closeable, Flushable {...private val journalFile: File              //journal开头属性都与日志相关private val journalFileTmp: Fileprivate val journalFileBackup: Fileprivate var size: Long = 0L              private var journalWriter: BufferedSink? = nullinternal val lruEntries = LinkedHashMap<String, Entry>(0, 0.75f, true) //一个Entry则为一条缓存,使用LinkedHashMap来保存缓存...init {require(maxSize > 0L) { "maxSize <= 0" }require(valueCount > 0) { "valueCount <= 0" }this.journalFile = File(directory, JOURNAL_FILE)  //文件名在companion object中定义this.journalFileTmp = File(directory, JOURNAL_FILE_TEMP)this.journalFileBackup = File(directory, JOURNAL_FILE_BACKUP)}...companion object {@JvmField val JOURNAL_FILE = "journal"    // 日志文件1 下文中统一称为journal@JvmField val JOURNAL_FILE_TEMP = "journal.tmp"  // 日志文件2 下文中统一称为日志tmp@JvmField val JOURNAL_FILE_BACKUP = "journal.bkp"  // 日志文件3 下文中统一称为bkp@JvmField val MAGIC = "libcore.io.DiskLruCache"@JvmField val VERSION_1 = "1"@JvmField val ANY_SEQUENCE_NUMBER: Long = -1@JvmField val LEGAL_KEY_PATTERN = "[a-z0-9_-]{1,120}".toRegex()@JvmField val CLEAN = "CLEAN"@JvmField val DIRTY = "DIRTY"@JvmField val REMOVE = "REMOVE"@JvmField val READ = "READ"}
}

DiskLruCache的初始化是懒加载,在真正操作DiskLruCache时才会被初始化,比如在添加数据时就会触发初始化,初始化方法如下:

DiskLruCache#initialize

@Synchronized @Throws(IOException::class)
fun initialize() {this.assertThreadHoldsLock()// 若已经初始化则直接返回if (initialized) {return // Already initialized.}// 日志文件存在三种// bkp文件若存在,则直接使用此文件if (fileSystem.exists(journalFileBackup)) {// 若journal也存在则删除bkp文件,若不存在则将bkp改为journal,这里透露一下为何这么做,bkp文件一般是空的(后续分析为何是空),反正都是要创建journal的,不如直接由bkp直接改名,省去了创建的操作if (fileSystem.exists(journalFile)) {fileSystem.delete(journalFileBackup)} else {fileSystem.rename(journalFileBackup, journalFile)}}// 此处是唯一对bkp文件进行操作的地方,civilizedFileSystem是布尔类型,代表当前平台是否可以同时读写,看下1.FileSystem#isCivilized  and  FileSystem#sink 分析civilizedFileSystem = fileSystem.isCivilized(journalFileBackup)// 若存在journal则需要读取journal之前的记录if (fileSystem.exists(journalFile)) {try {// 在真正读取前先学习下日志的格式,看下面格式分析小节// 下述方法解析看日志文件的解析小节中1.DiskLruCache#readJournalreadJournal()// 下述方法解析看本节下2.DiskLruCache#processJournalprocessJournal() initialized = truereturn} catch (journalIsCorrupt: IOException) {Platform.get().log("DiskLruCache $directory is corrupt: ${journalIsCorrupt.message}, removing",WARN,journalIsCorrupt)}// The cache is corrupted, attempt to delete the contents of the directory. This can throw and// we'll let that propagate out as it likely means there is a severe filesystem problem.try {delete()} finally {closed = false}}// 不存在journal时才会重建日志// 在日志解析小节已经分析过此方法,详情请看看日志文件的解析小节中3.DiskLruCache#rebuildJournalrebuildJournal() initialized = true
}

1.FileSystem#isCivilized and FileSystem#sink

此方法中则会引出OKIOOKIOSquare公司开发的更高效的IO,目前不对其进行深入研究。

sink 则类比似FileOutputStream即可

source 则类比似FileInputStream即可

其使用装饰着模式也会有类似于BufferOutputStreamBufferedInputStream的存在,等后续遇到在说。

fun FileSystem.isCivilized(file: File): Boolean {//sink方法下面分析,此处打开输出流的时候删除文件,若此操作可以成功则返回为true,若失败则返回为false,标志当前平台是否可以同时读写文件,windows一般是不可以的,inode 的系统(如 Mac、Unix 和 Linux)一般是可以的sink(file).use {try {delete(file) //删除文件return true} catch (_: IOException) {}}delete(file)return false
}@Throws(FileNotFoundException::class)
override fun sink(file: File): Sink {return try {file.sink() //打开输出流} catch (_: FileNotFoundException) {// Maybe the parent directory doesn't exist? Try creating it first.file.parentFile.mkdirs() // 文件夹不存在则先生成目录file.sink()}
}

2.DiskLruCache#processJournal

此方法主要对lruEntries中元素的合法性进行判断和修正

@Throws(IOException::class)
private fun processJournal() {// 删除tmp文件fileSystem.delete(journalFileTmp)// 遍历lruEntries mapval i = lruEntries.values.iterator()while (i.hasNext()) {val entry = i.next()// null 则代表clean数据if (entry.currentEditor == null) {// valueCount是初始化DiskLruCache传入的,为2,为什么是2在之前也讲述过// 加上此缓存的缓存头和体,标志当前Cache使用了多大的空间for (t in 0 until valueCount) {size += entry.lengths[t]}} else {// 若是dirty,则认为为脏数据,将currentEditor制空entry.currentEditor = null// 并删除脏文件for (t in 0 until valueCount) {fileSystem.delete(entry.cleanFiles[t])fileSystem.delete(entry.dirtyFiles[t])}i.remove()}}
}

日志格式分析

调用下述方法两次

fun get() {//创建OkHttpClientval okHttpClient = OkHttpClient().newBuilder().cache(Cache(File(this.getExternalCacheDir(), "okhttpcache"), 10 * 1024 * 1024)).build()val url = "https://www.wanandroid.com"val request = Request.Builder().cacheControl(CacheControl.Builder().maxAge(0, TimeUnit.SECONDS).build()).url(url).build()val call = okHttpClient.newCall(request)call.enqueue(object : Callback {override fun onFailure(call: Call, e: IOException) {Log.d("onFailure", "onFailure")}override fun onResponse(call: Call, response: Response) {val responseBody = response.body;if (responseBody != null)  {Log.d("onResponse", responseBody.string())}}})
}

最终在下述目录生成此类文件:

打开journal文件,日志内容为

libcore.io.DiskLruCache
1
201105
2DIRTY 7369f7cffbcf64f8f27ae32d8bcbc430
CLEAN 7369f7cffbcf64f8f27ae32d8bcbc430 5498 13971
READ 7369f7cffbcf64f8f27ae32d8bcbc430
DIRTY 7369f7cffbcf64f8f27ae32d8bcbc430
CLEAN 7369f7cffbcf64f8f27ae32d8bcbc430 5498 13971

日志的前五行构成标题

它们分别是:

  1. 常量字符串“libcore.io.DiskLruCache
  2. 磁盘缓存的版本
  3. 应用程序的版本
  4. 值计数
  5. 空行

其余的行数代表对缓存的操作

总共有四种,每种后边都会跟随一个Key,此Key是由请求的URL生成的,也是缓存的文件名

  • DIRTY 代表正在创建的,或者创建好并未使用过的缓存,DIRTY必须配合一个CLEAN或者REMOVE,成对出现,否则代表此缓存是tmp文件需要删除
  • CLEAN 代表可用的缓存,其后除包含一个Key,还存在两个值,一个代表响应头的长度,一个代表响应体的长度
  • READ 读取了此缓存
  • REMOVE 删除一个缓存

日志文件的解析

1.DiskLruCache#readJournal

此方法解析Journal文件,将日志中的信息转换成Entry对象,并存储在lruEntries属性中

@Throws(IOException::class)
private fun readJournal() {// 解析journal文件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("unexpected journal header: [$magic, $version, $valueCountString, $blank]")}// 开始解析正文var lineCount = 0while (true) {try {// 解析每一行,下2分析readJournalLine(source.readUtf8LineStrict())// 计数器lineCount++} catch (_: EOFException) {break // End of journal.}}redundantOpCount = lineCount - lruEntries.size// If we ended on a truncated line, rebuild the journal before appending to it.                        // 若source关闭则重建日志if (!source.exhausted()) {rebuildJournal() //下3分析} else {// 若没有关闭则创建BufferSink指向journal文件journalWriter = newJournalWriter()}}
}

此方法分析结束则返回DiskLruCache的初始化小节DiskLruCache#initialize继续向下分析

2.DiskLruCache#readJournalLine

解析一行日志的正文

@Throws(IOException::class)
private fun readJournalLine(line: String) {// 不存在空格则抛出IOE异常val firstSpace = line.indexOf(' ')if (firstSpace == -1) throw IOException("unexpected journal line: $line")val keyBegin = firstSpace + 1// 找到第二个空格出现的位置val secondSpace = line.indexOf(' ', keyBegin)val key: String// 若没有找到第二个空格if (secondSpace == -1) {// 截断字符串 比如“DIRTY 7369f7cffbcf64f8f27ae32d8bcbc430”,截断后key 为7369f7cffbcf64f8f27ae32d8bcbc430key = line.substring(keyBegin)// 如果是remove命令,则移除lruEntries的属性,以key为键if (firstSpace == REMOVE.length && line.startsWith(REMOVE)) {lruEntries.remove(key)return}} else {// 若存在第二个空格,则为clean指令,比如"CLEAN 7369f7cffbcf64f8f27ae32d8bcbc430 5498 13971"// 截断第一个空格到第二个空格之间,则可拿到key值key = line.substring(keyBegin, secondSpace)}// 若是clean,dirty,read则会创建新的Enrty并添加进lruEntriesvar entry: Entry? = lruEntries[key]if (entry == null) {// 创建Entry,先看下面Entry小节分析entry = Entry(key)lruEntries[key] = entry}when {// 若是CLEAN指令secondSpace != -1 && firstSpace == CLEAN.length && line.startsWith(CLEAN) -> {// 在第二个空格处隔断// 比如"CLEAN 7369f7cffbcf64f8f27ae32d8bcbc430 5498 13971"// line.substring(secondSpace + 1) = "5498 13971"// 执行split(' ') 则为分割为数组["5498", "13971"]val parts = line.substring(secondSpace + 1).split(' ')// 标志当前缓存可读entry.readable = trueentry.currentEditor = null// 设置长度数组entry.setLengths(parts)}// 若是DIRTY指令secondSpace == -1 && firstSpace == DIRTY.length && line.startsWith(DIRTY) -> {// 创建Editor,Editor会提供真正的输出流和输入流// 在解析日志文件过程中,一定要记得只要是DIRTY的entry,currentEditor属性不为null,后续会根据此属性来判断当前entry是否是DIRTYentry.currentEditor = Editor(entry)}// 若是READ指令,无需操作secondSpace == -1 && firstSpace == READ.length && line.startsWith(READ) -> {// This work was already done by calling lruEntries.get().}else -> throw IOException("unexpected journal line: $line")}
}

现在举个例子加深Entry对创建规则的理解,日志如下(DiskLruCache类中官方注释的日志):

CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

存储Entry的数据据结构为LinkedHashMap,相同的Keyvalue会覆盖之前的value

此条日志最终会在Entry内部的cleanFilesdirtyFiles分别各创建两个File对象

Key分析,只分析最下行的key即可

3400330d1dfc7f3f7f4b8d4d803dfcf6

最后一条指令为READ,因此只创建一个简单的Entry

Entry中只有一步操作,,执行Entryinit域被,初始化cleanFilesdirtyFiles

等号右边为文件名

  • cleanFiles[0] = 3400330d1dfc7f3f7f4b8d4d803dfcf6.0 响应头文件
  • cleanFiles[1] = 3400330d1dfc7f3f7f4b8d4d803dfcf6.1 响应体文件
  • dirtyFiles[0] = 3400330d1dfc7f3f7f4b8d4d803dfcf6.0.tmp 脏响应头文件
  • dirtyFiles[1] = 3400330d1dfc7f3f7f4b8d4d803dfcf6.1.tmp 脏响应体文件

要注意: 创建File对象不等于创建文件, 后续调用sink()方法, 打开输出流才会真正创建文件.

335c4c6028171cfddfbaae1a9c313c52

最后一条指令也为READ, 效果与上述一样

我们假设为DIRTY,上述照样会被执行,还会初始化Entry对象的currentEditor属性

1ab96a171faeeee38496d8b330771a7a

最后一条为CLEAN,上述效果也会被执行,还会将readable属性置为true表示可使用,另外还会调用setLengths()方法,设置长度

readJournalLine()方法总结: 一句话, 根据不同的日志指令创建响应的Entry, 并添加进Map

3.DiskLruCache#rebuildJournal

此方法有三种情况被执行

  • 增删元素时,超过2000个元素,且缓存使用空间超过DiskLruCache设置的最大空间时
  • 初始化DiskLruCacheJournal文件不存在,则重建Journal文件
  • 解析Journal文件时,由于其他原因导致输入流关闭则也需要重建Journal文件
@Synchronized @Throws(IOException::class)
internal fun rebuildJournal() {// 关闭指向journal文件的输出流journalWriter?.close()// 向tmp文件输出数据fileSystem.sink(journalFileTmp).buffer().use { sink ->// 前五行标题数据sink.writeUtf8(MAGIC).writeByte('\n'.toInt())sink.writeUtf8(VERSION_1).writeByte('\n'.toInt())sink.writeDecimalLong(appVersion.toLong()).writeByte('\n'.toInt())sink.writeDecimalLong(valueCount.toLong()).writeByte('\n'.toInt())sink.writeByte('\n'.toInt())// 遍历lruEntriesfor (entry in lruEntries.values) {// 存在Editor说明是DIRTY数据,写入DIRTYif (entry.currentEditor != null) {sink.writeUtf8(DIRTY).writeByte(' '.toInt())sink.writeUtf8(entry.key)sink.writeByte('\n'.toInt())} else {// 其他情况写入CLEANsink.writeUtf8(CLEAN).writeByte(' '.toInt())sink.writeUtf8(entry.key)entry.writeLengths(sink)sink.writeByte('\n'.toInt())}}}// 若存在journal文件则将其改为bkp文件if (fileSystem.exists(journalFile)) {fileSystem.rename(journalFile, journalFileBackup)}// 再将新的tmp文件改为journal文件fileSystem.rename(journalFileTmp, journalFile)// 删除bkp文件,也就是旧的journal文件fileSystem.delete(journalFileBackup)// 获取指向journal文件的输出流journalWriter = newJournalWriter()hasJournalErrors = falsemostRecentRebuildFailed = false
}

在创建时先创建tmp文件,最终再将此文件改为journal文件,到这里则可弄清楚tmpbkpjournal三个日志文件的作用。

  • bkp文件主要用于判断当前平台是否可以同时读写文件。
  • tmp文件是journal文件的前身,journal最初是由tmp文件改名而来的。
  • journal文件是正式的可以使用的日志文件。

DiskLruCache.Entry

DiskLruCache中存储的缓存实体对象,一个Entry对象代表一条缓存。

internal inner class Entry internal constructor(internal val key: String
) {// 长度数组,下标0代表头文件大小,下标1代表响应体内容文件大小internal val lengths: LongArray = LongArray(valueCount) // 像日志一样,分为tmp和正式文件,真正的缓存也是如此internal val cleanFiles = mutableListOf<File>() //正式缓存文件集合internal val dirtyFiles = mutableListOf<File>()   //脏缓存文件集合// 当前缓存是否可用internal var readable: Boolean = false// 当前编辑或读取完成时必须删除此条目,则为真。internal var zombie: Boolean = false// 正在进行的编辑,如果此条目未被编辑,则为 null。将此设置为 null 时,则该条目是僵尸条目,必须删除该条目。internal var currentEditor: Editor? = null// 此条缓存打开的输入流的总数。当将此值递减为零时,则该条目是僵尸条目,必须删除该条目。internal var lockingSourceCount = 0/** The sequence number of the most recently committed edit to this entry. */internal var sequenceNumber: Long = 0init {// 以key为文件名创建File对象val fileBuilder = StringBuilder(key).append('.')val truncateTo = fileBuilder.length// valueCount为2 循环两次for (i in 0 until valueCount) {fileBuilder.append(i)cleanFiles += File(directory, fileBuilder.toString())fileBuilder.append(".tmp")dirtyFiles += File(directory, fileBuilder.toString())// 删除后缀fileBuilder.setLength(truncateTo)}}...
}

回到日志文件的解析2.DiskLruCache#readJournalLine在创建Entry处继续向下分析。

总结

初始化DiskLruCache最最重要的则是初始化内部的lruEntries属性, 此属性又是由解析日志文件得来的, 因此重点在日志文件.

日志文件有三个类型, 在日志文件的解析小节3.DiskLruCache#rebuildJournal末尾进行了作用说明, 忘记的读者可以往前翻一翻.

按照是否存在日志文件会出现两种执行过程

  • 存在日志文件则按照日志命令的不同创建Entry并添加进lruEntries.
  • 不存在则执行DiskLruCache#rebuildJournal(), 创建tmp文件, 并最终改为journal文件.

下面分析CRUD

此小节主要分析往Cache放数据, 重点方法则是Cache#put()

Cache#put

// 添加数据
internal fun put(response: Response): CacheRequest? {val requestMethod = response.request.method// 如果请求方法为POST,PATCH,PUT,DELETE,MOVE,则移除此缓存if (HttpMethod.invalidatesCache(response.request.method)) {try {remove(response.request)} catch (_: IOException) {// The cache cannot be written.}return null}// 只允许get方法缓存if (requestMethod != "GET") {// Don't cache non-GET responses. We're technically allowed to cache HEAD requests and some// POST requests, but the complexity of doing so is high and the benefit is low.return null}// 如果 Vary 响应头中包含星号,也无法缓存此响应// vary 详情请看https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Vary// vary简单理解则是记录下来需要缓存的响应头的值if (response.hasVaryAll()) {return null}// 创建Entry,此Entry是Cache.Entry 非 DiskLruCache.Entry, 看下述1.Cache.Entry的构造val entry = Entry(response)var editor: DiskLruCache.Editor? = nulltry {// 调用DiskLruCache的edit(), 此方法则会根据传入的参数获取DiskLruCache.Entry, 看下述2.DiskLruCache#edit中的解析editor = cache.edit(key(response.request.url)) ?: return null// 执行写入操作,看下3.Cache.Entry#writeToentry.writeTo(editor)// 返回RealCacheRequest,此时会创建响应体文件, 看下5.RealCacheRequestreturn RealCacheRequest(editor)} catch (_: IOException) {abortQuietly(editor)return null}
}

1.Cache.Entry

没什么特殊操作, 只是将response的值取出来

constructor(response: Response) {this.url = response.request.url.toString()this.varyHeaders = response.varyHeaders()this.requestMethod = response.request.methodthis.protocol = response.protocolthis.code = response.codethis.message = response.messagethis.responseHeaders = response.headersthis.handshake = response.handshakethis.sentRequestMillis = response.sentRequestAtMillisthis.receivedResponseMillis = response.receivedResponseAtMillis
}

2.DiskLruCache#edit

@Synchronized @Throws(IOException::class)
@JvmOverloads
fun edit(key: String, expectedSequenceNumber: Long = ANY_SEQUENCE_NUMBER): Editor? {// 此方法DiskLruCache的初始化小节分析过,作用就是初始化DiskLruCacheinitialize()// 检测缓存有没有关闭,若关闭则抛出异常checkNotClosed()// 判断key是否有效validateKey(key)// 获取lruEntries中key的value, 第一次则一定为nullvar entry: Entry? = lruEntries[key]// 若是添加操作则expectedSequenceNumber使用默认值ANY_SEQUENCE_NUMBER,if不会命中// 若是更新操作才有可能命中此ifif (expectedSequenceNumber != ANY_SEQUENCE_NUMBER &&(entry == null || entry.sequenceNumber != expectedSequenceNumber)) {return null // Snapshot is stale.}// 若entry绑定了Editor则也返回nullif (entry?.currentEditor != null) {return null // Another edit is in progress.}// 第一次添加不会命中,因为entry为null, lockingSourceCount是当前正在打开的输入流的总数if (entry != null && entry.lockingSourceCount != 0) {return null // 无法写入此文件,因为读者仍在阅读它}// 在清理过程中, 超过设置的缓存大小且移除Entry失败时会使mostRecentTrimFailed = true, 若需要重建journal, 日志文件创建失败会使mostRecentRebuildFailed = true, 这两种情况下会命中ifif (mostRecentTrimFailed || mostRecentRebuildFailed) {// The OS has become our enemy! If the trim job failed, it means we are storing more data than// requested by the user. Do not allow edits so we do not go over that limit any further. If// the journal rebuild failed, the journal writer will not be active, meaning we will not be// able to record the edit, causing file leaks. In both cases, we want to retry the clean up// so we can get out of this state!// 翻译如下: 操作系统已成为我们的敌人!如果修剪作业失败,则意味着我们存储的数据多于用户请求的数据。不允许编辑,因此我们不会进一步超出该限制。如果日志重建失败,日志写入器将不会处于活动状态,这意味着我们将无法记录编辑,从而导致文件泄漏。在这两种情况下,我们都想重试清理,以便摆脱这种状态!cleanupQueue.schedule(cleanupTask)return null}// Flush the journal before creating files to prevent file leaks.// 翻译:在创建文件之前刷新日志以防止文件泄漏// 先在journal文件中写入DIRTYval journalWriter = this.journalWriter!!journalWriter.writeUtf8(DIRTY).writeByte(' '.toInt()).writeUtf8(key).writeByte('\n'.toInt())journalWriter.flush()// 若写入journal文件失败则hasJournalErrors = trueif (hasJournalErrors) {return null // Don't edit; the journal can't be written.}// 创建Entry,此Entry为DiskLruCache.Entryif (entry == null) {// 具体的创建过程在DiskLruCache.Entry小节已经分析,此处不再赘述entry = Entry(key)lruEntries[key] = entry}// 创建Editor,并返回,读者没忘记Editor的作用吧,其作用就是具体的输入流和输出流的操作者val editor = Editor(entry)entry.currentEditor = editorreturn editor
}

回到小节的开头的Cache#put方法中,继续向下分析

3.Cache.Entry#writeTo

@Throws(IOException::class)
fun writeTo(editor: DiskLruCache.Editor) {// 按照响应头的报文格式写入文件,newSink()方法看下4.DiskLruCache.Edit#newSink, ENTRY_METADATA为0代表响应头文件// 看完newSink()的解析回到这,buffer()方法则是将当前的流再包装一层,此时流的结构为RealBufferedSink(FaultHidingSink(sink()))editor.newSink(ENTRY_METADATA).buffer().use { sink ->sink.writeUtf8(url).writeByte('\n'.toInt())sink.writeUtf8(requestMethod).writeByte('\n'.toInt())sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.toInt())// 写入Varyfor (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())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())}}
}

以请求https://www.wanandroid.com为例, 最终生成的文件内容如下:

回到Cache#put继续向下分析

4.DiskLruCache.Edit#newSink

根据传入的index返回相应的输出流, 在DiskLruCache.Entry小节讲述过Entry内部有两个数组, cleanFiles , dirtyFiles, 每个数组有两个元素, 下标0代表响应头文件, 下标1代表响应体文件, 此处的index则会根据01返回响应的文件的输出流.

fun newSink(index: Int): Sink {synchronized(this@DiskLruCache) {check(!done)// 若绑定的输出流不正确,则会输出一个新的输出流if (entry.currentEditor != this) {return blackholeSink()}// 若entry不可读,则标志当前的问政正在写if (!entry.readable) {written!![index] = true}// 获取到当前entry中dirtyFiles的文件val dirtyFile = entry.dirtyFiles的文件[index]val sink: Sinktry {// 通过fileSystem创建输出流,fileSystem为FileSystem专门管理文件的类sink = fileSystem.sink(dirtyFile)} catch (_: FileNotFoundException) {// 出现错误则返回一个无关的输出流return blackholeSink()}// 因为okio也使用装饰者模式,将sink包装一层为FaultHidingSink,类似于BufferOutputStream(FileOutputStream())return FaultHidingSink(sink) {synchronized(this@DiskLruCache) {detach()}}}
}

回到3.Cache.Entry#writeTo中继续向下分析

5.RealCacheRequest

private inner class RealCacheRequest(private val editor: DiskLruCache.Editor
) : CacheRequest {// 初始化输出流则会创建文件,newSink()方法已经分析过,此时传入的ENTRY_BODY为1,意味者此流指向的是响应体文件private val cacheOut: Sink = editor.newSink(ENTRY_BODY)private val body: Sinkvar done = falseinit {// 给body赋值, 包装原始sink,包装结构为ForwardingSink(FaultHidingSink(sink()))this.body = object : ForwardingSink(cacheOut) {@Throws(IOException::class)override fun close() {synchronized(this@Cache) {if (done) returndone = truewriteSuccessCount++}super.close()// 当此次文件操作结束则会调用commit(),稍后再分析editor.commit()}}}
}

此时添加结束, 但是有个问题, 若上层业务并没有调用response bodystring()方法, 则会出现下边情况

下边分析响应体文件啥时候写入数据的, 且临时文件如何成为正式文件的,在分析之前先学习输入流的装饰者结构,因为最终写入数据一定是某一层装饰去做的, 功能都是层层装饰上去的

输入流的装饰者结构

输入流即SourceSourceResponseBody的第三个属性,分析输入流结构则绕不开ResponseBody的创建,现在开始分析

接下来需要结合CallServerInterceptorCacheInterceptor的一些内容进行分析

由于责任链模式,最早创建ResponseBody的是CallServerInterceptor,第二拿到ResponseCacheInterceptor,第三个为BridgeInterceptor

ConnectInterceptorRetryAndFollowUpInterceptor并没有对响应体做其他包装,因此不分析

从响应的获取开始分析:

1.CallServerInterceptor#intercept

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {...response.newBuilder().body(exchange.openResponseBody(response)) //看下2分析.build()...return response
}

2.Exchange#openResponseBody

@Throws(IOException::class)
fun openResponseBody(response: Response): ResponseBody {try {val contentType = response.header("Content-Type")val contentLength = codec.reportedContentLength(response)val rawSource = codec.openResponseBodySource(response)  // 指向socket的输入流,此处是最原始的输入流,用于拿取网络数据val source = ResponseBodySource(rawSource, contentLength) //构建ResponseBodySourcereturn RealResponseBody(contentType, contentLength, source.buffer()) //装饰者模式,第三个参数source的装饰结构为RealBufferedSource(ResponseBodySource)} catch (e: IOException) {eventListener.responseFailed(call, e)trackFailure(e)throw e}
}

紧接着CacheInterceptor获取CallServerInterceptor返回的响应,并触发缓存机制则会将Response再次封装

3.CacheInterceptor#intercept

默认触发缓存

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {...// 添加过程只是cache.put(response)此行代码的解析val cacheRequest = cache.put(response)// 此方法会返再次封装Response,其内部会再次对输入流进行包装,此处传入的cacheRequest参数则包含了输出流,看下述分析4.CacheInterceptor#cacheWritingResponsereturn cacheWritingResponse(cacheRequest, response).also {if (cacheResponse != null) {// This will log a conditional cache miss only.listener.cacheMiss(call)}}...return response
}

4.CacheInterceptor#cacheWritingResponse

@Throws(IOException::class)
private fun cacheWritingResponse(cacheRequest: CacheRequest?, response: Response): Response {// Some apps return a null body; for compatibility we treat that like a null cache request.if (cacheRequest == null) return response// body在上述增小节中的5.RealCacheRequest中已经分析val cacheBodyUnbuffered = cacheRequest.body()// 这里多提一嘴,若是网络请求返回的Response则body是在CallServerInterceptor的intercept()赋值的,理解为Socket的输入流即可,若是缓存中赋值的则是在Cache类中赋值的,理解为文件输入流即可val source = response.body!!.source() //body的真实类型为// buffer()则将文件输出流再次封装,结构如下:RealBufferedSink(ForwardingSink(FaultHidingSink(sink())))val cacheBody = cacheBodyUnbuffered.buffer()// 真正的读会在其内部实现,也是真正响应体缓存文件的真实操作者,其read()方法是真正的核心,在调用响应体的string()方法后则会调用到read(),完成拿取数据的操作,并写入缓存文件。val cacheWritingSource = object : Source {...@Throws(IOException::class)override fun read(sink: Buffer, byteCount: Long): Long {...}...}val contentType = response.header("Content-Type")val contentLength = response.body.contentLength()// 返回response,body的真实类型为RealResponseBody,构建body的第三个参数要注意是上面的cacheWritingSource的buffer()方法返回的,,buffer()方法返回的真实类型为RealBufferedSource,装饰者包装,包装结构为RealBufferedSource(cacheWritingSource)return response.newBuilder().body(RealResponseBody(contentType, contentLength, cacheWritingSource.buffer())).build()
}

由于责任链模式,在BridgeInterceptor会再次对source进行包装,看下述5.BridgeInterceptor#intercept分析

5.BridgeInterceptor#intercept

@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {...// 内部构造又会对Source进行封装,看下4中的分析val gzipSource = GzipSource(responseBody.source())val strippedHeaders = networkResponse.headers.newBuilder().removeAll("Content-Encoding").removeAll("Content-Length").build()responseBuilder.headers(strippedHeaders)val contentType = networkResponse.header("Content-Type")// 再次封装最终的Source结构为RealBufferedSource(GzipSource(RealBufferedSource(RealBufferedSource(cacheWritingSource))))responseBuilder.body(RealResponseBody(contentType, -1L, gzipSource.buffer()))..return responseBuilder.build()
}

4.GzipSource

class GzipSource(source: Source) : Source {...private val source = RealBufferedSource(source)...
}

总结:最终请求体的真实类型为RealResponseBody,输入流的包装结构为RealBufferedSource(GzipSource(RealBufferedSource(RealBufferedSource(cacheWritingSource))))

cacheWritingSource又会去操作网络或者文件输入流

说实话,笔者觉得快包成粽子了,层次很不明朗。

注意:上述的输入流的结构只是一种情况,是网络传输的数据且需要缓存的情况,若此次请求直接命中了缓存,没有发送网络请求,则此输入流是指向缓存文件的文件输入流的并不是Socket的网络输入流了,此种情况笔者可以自行分析,只是少装饰一层流而已, 就是上述4.CacheInterceptor#cacheWritingResponse中的匿名内部类生成的对象cacheWritingSource,为什么是读者可以自行分析一下。

接下来开始真正分析 响应体文件啥时候写入数据的, 且临时文件如何成为正式文件的

响应体文件的写入

在使用OkHttp发起请求成功后会回调CallBackonResponse()方法,如下:

override fun onResponse(call: Call, response: Response) {val responseBody = response.body;if (responseBody != null)  {// 调用string()方法获取数据, 此处responseBody的真实类型为RealResponseBody,为何是此类型,在上面输入流的装饰者结构小节已经分析过Log.d("onResponse data", responseBody.string())  //responseBody.string()方法看下述ResponseBody#string分析,string()的作用就是把网络输入流的数据或者文件输入流的数据读进内存}
}

ResponseBody#string

@Throws(IOException::class)
fun string(): String = source().use { source ->source.readString(charset = source.readBomAsCharset(charset())) //重点就在于source.readBomAsCharset(charset())这行代码,会真正往响应体文件写入数据,source在上面输入流的装饰者结构小节已经分析,结构为RealBufferedSource(GzipSource(RealBufferedSource(RealBufferedSource(cacheWritingSource)))),readBomAsCharset()方法用于获取编码方法看下述编码格式的确认小节中的分析,readString()则会将数据真正写入读到内存中,看下述数据的真正读取小节
}

编码格式的确认

1.BufferedSource#readBomAsCharset

@Throws(IOException::class)
fun BufferedSource.readBomAsCharset(default: Charset): Charset {// 返回编码方式,select()方法需要继承实现,在输入流的装饰者结构小节已经分析了Source的具体结构,此处的source为RealBufferedSource,select()方法看下述2.RealBufferedSource#selectreturn when (select(UNICODE_BOMS)) {0 -> UTF_81 -> UTF_16BE2 -> UTF_16LE3 -> UTF_32BE4 -> UTF_32LE-1 -> defaultelse -> throw AssertionError()}
}

2.RealBufferedSource#select

override fun select(options: Options): Int = commonSelect(options) //看下述3.RealBufferedSource#commonSelect

3.RealBufferedSource#commonSelect

此方法的逻辑比较巧妙,先读一些数据,再判断这些数据的编码方式。

internal inline fun RealBufferedSource.commonSelect(options: Options): Int {check(!closed) { "closed" }while (true) {// 第一次循环一定为-2,后续的循环中就可以判断出文件的编码val index = buffer.selectPrefix(options, selectTruncated = true)when (index) {-1 -> {return -1}-2 -> {// 读一些数据到buffer中,此处的source为GzipSource,现在先不分析此方法此方法if (source.read(buffer, Segment.SIZE.toLong()) == -1L) return -1}else -> {// We matched a full byte string: consume it and return it.val selectedSize = options.byteStrings[index].sizebuffer.skip(selectedSize.toLong())return index}}}
}

编码格式的确认小节分析完毕,就一句话先读一部分数据再判断编码方式,回到响应体文件的写入小节中的ResponseBody#string。

数据的真正读取

RealBufferedSource#readString

override fun readString(charset: Charset): String {buffer.writeAll(source) //此方法会将全部的数据读到内存,若内部不够则会oom,在读取的过程中如果流有缓存的装饰者,则会将数据写到文件中,看下1.Buffer.writeAllreturn buffer.readString(charset) // 此方法不再分析只是根据编码创建String对象
}

1.Buffer.writeAll

@Throws(IOException::class)
override fun writeAll(source: Source): Long = commonWriteAll(source)internal inline fun Buffer.commonWriteAll(source: Source): Long {var totalBytesRead = 0L// 循环读取,直到数据读完毕,此处的source则为GzipSource,看下述2.GzipSourcewhile (true) {val readCount = source.read(this, Segment.SIZE.toLong())if (readCount == -1L) breaktotalBytesRead += readCount}return totalBytesRead
}

2.GzipSource

class GzipSource(source: Source) : Source {// Gzip使用分段读取,先读头,在读体,最后读尾,因此初始值为头private var section = SECTION_HEADER// 下层包装private val source = RealBufferedSource(source)/** The inflater used to decompress the deflated body. */private val inflater = Inflater(true)// 负责在压缩源和解压缩的接收缓冲区之间移动数据private val inflaterSource = InflaterSource(this.source, inflater)// 校验和用于检查 GZIP 标头和解压缩的正文private val crc = CRC32()@Throws(IOException::class)override fun read(sink: Buffer, byteCount: Long): Long {require(byteCount >= 0L) { "byteCount < 0: $byteCount" }if (byteCount == 0L) return 0L// 读头操作,此时会将前置数据写入文件,所有的读取操作由于装饰者模式一定会调用到最里层的输入流的读方法,因此先分析最里层的读方法,先看下面最内层的输入流小节if (section == SECTION_HEADER) {consumeHeader() // 写入响应体文件头的重要方法, 看下述写入头数据小节的分析section = SECTION_BODY}// 读取正文if (section == SECTION_BODY) {val offset = sink.sizeval result = inflaterSource.read(sink, byteCount) // 重点方法,看下述写入正文数据小节的分析...section = SECTION_TRAILER}// 读取尾if (section == SECTION_TRAILER) {consumeTrailer()section = SECTION_DONE...}return -1}
}

最内层的输入流

最内层的输入流则是cacheWritingSource,是匿名内部类声明的对象,在输入流的装饰者结构小节中4.CacheInterceptor#cacheWritingResponse中进行过分析.

略过中间的多层装饰,直接分析最里层,最终的读操作一定会调用到此处read()方法:

val cacheWritingSource = object : Source {private var cacheRequestClosed = false@Throws(IOException::class)override fun read(sink: Buffer, byteCount: Long): Long {val bytesRead: Long// 将网络的数据或者说磁盘的数据读取到sink中,此sink是外部装饰者传递的缓存数组,// source可能有两种情况网络输入流或者文件输入流,只需要知道是执行IO操作读取数据到sink缓存中bytesRead = source.read(sink, byteCount)...//读取完毕则需要关闭流if (bytesRead == -1L) {if (!cacheRequestClosed) {cacheRequestClosed = true// body在读取完毕后则会调用close(),cacheBody的具体结构为RealBufferedSink(ForwardingSink(FaultHidingSink(sink()))),由于装饰者模式最终也会调到FaultHidingSink的close()方法,还记得FaultHidingSink是什么时候创建的吗,在上述增小节的5.RealCacheRequest处创建,其创建的FaultHidingSink的close()方法中最终会调用editor.commit(),看本节下述1.Edit.commitcacheBody.close() // The cache response is complete!}return -1}// 将缓存数组的数据copy到cacheBody的缓存中sink.copyTo(cacheBody.buffer, sink.size - bytesRead, bytesRead)// 看本节下述5.RealBufferedSink.commonEmitCompleteSegmentscacheBody.emitCompleteSegments()return bytesRead}...}

1.Edit.commit

fun commit() {synchronized(this@DiskLruCache) {check(!done)if (entry.currentEditor == this) {// 完成此次编辑,看本节下2.DiskLruCache#completeEditcompleteEdit(this, true)}done = true}
}

2.DiskLruCache#completeEdit

@Synchronized @Throws(IOException::class)
internal fun completeEdit(editor: Editor, success: Boolean) {val entry = editor.entrycheck(entry.currentEditor == editor)...// 遍历dirtyFiles数组,进行改名for (i in 0 until valueCount) {val dirty = entry.dirtyFiles[i]if (success && !entry.zombie) {if (fileSystem.exists(dirty)) {val clean = entry.cleanFiles[i]fileSystem.rename(dirty, clean)val oldLength = entry.lengths[i]val newLength = fileSystem.size(clean)entry.lengths[i] = newLengthsize = size - oldLength + newLength}} else {fileSystem.delete(dirty)}}entry.currentEditor = null// 需要移除,自移除节点if (entry.zombie) {removeEntry(entry)return}redundantOpCount++// 不需要移除且此条缓存编辑成功则写入CLEAN日志,否则写入REMOVE日志journalWriter!!.apply {if (entry.readable || success) {entry.readable = truewriteUtf8(CLEAN).writeByte(' '.toInt())writeUtf8(entry.key)entry.writeLengths(this)writeByte('\n'.toInt())if (success) {entry.sequenceNumber = nextSequenceNumber++}} else {lruEntries.remove(entry.key)writeUtf8(REMOVE).writeByte(' '.toInt())writeUtf8(entry.key)writeByte('\n'.toInt())}flush()}// 超过大小则需对缓存进行清理if (size > maxSize || journalRebuildRequired()) {// 异步执行,cleanupTask看本节下3.cleanupTaskcleanupQueue.schedule(cleanupTask)}
}

经过上述分析可得只有在数据读取完毕,才会将tmp文件转换为正式文件。

3.cleanupTask

private val cleanupTask = object : Task("$okHttpName Cache") {override fun runOnce(): Long {synchronized(this@DiskLruCache) {if (!initialized || closed) {return -1L // Nothing to do.}try {// 对大小进行修正如果超出缓存则根据LRU策略去掉响应的缓存trimToSize()} catch (_: IOException) {mostRecentTrimFailed = true}try {// 看下4分析journalRebuildRequired()if (journalRebuildRequired()) {rebuildJournal()redundantOpCount = 0}} catch (_: IOException) {mostRecentRebuildFailed = truejournalWriter = blackholeSink().buffer()}return -1L}}
}

4.DiskLruCache#journalRebuildRequired

private fun journalRebuildRequired(): Boolean {val redundantOpCompactThreshold = 2000// 缓存条目超过2000,且硬盘使用大小超过最初设置的大小,则需要重建日志return redundantOpCount >= redundantOpCompactThreshold &&redundantOpCount >= lruEntries.size
}

5.RealBufferedSink#commonEmitCompleteSegments

internal inline fun RealBufferedSink.commonEmitCompleteSegments(): BufferedSink {check(!closed) { "closed" }// 存在数据val byteCount = buffer.completeSegmentByteCount()// 将数据写到文件上if (byteCount > 0L) sink.write(buffer, byteCount)return this
}

到这最内层的输入流小节分析完毕回到响应体文件的写入小节中5.GzipSource继续向下分析

写入头数据

GzipSource#consumeHeader

@Throws(IOException::class)
private fun consumeHeader() {// Read the 10-byte header. We peek at the flags byte first so we know if we// need to CRC the entire header. Then we read the magic ID1ID2 sequence.// We can skip everything else in the first 10 bytes.// +---+---+---+---+---+---+---+---+---+---+// |ID1|ID2|CM |FLG|     MTIME     |XFL|OS | (more-->)// +---+---+---+---+---+---+---+---+---+---+// 翻译还是很简单的source.require(10) // 最重要的方法,此方法会写入10个字节,按照之前的分析结构source为RealBufferedSource,看下本节1.RealBufferedSource#requireval flags = source.buffer[3].toInt()val fhcrc = flags.getBit(FHCRC)if (fhcrc) updateCrc(source.buffer, 0, 10)val id1id2 = source.readShort()checkEqual("ID1ID2", 0x1f8b, id1id2.toInt())source.skip(8)  // 此方法会跳过8个字节...
}

1.RealBufferedSource#require

override fun require(byteCount: Long): Unit = commonRequire(byteCount)internal inline fun RealBufferedSource.commonRequire(byteCount: Long) {if (!request(byteCount)) throw EOFException()
}override fun request(byteCount: Long): Boolean = commonRequest(byteCount)internal inline fun RealBufferedSource.commonRequest(byteCount: Long): Boolean {require(byteCount >= 0) { "byteCount < 0: $byteCount" }check(!closed) { "closed" }while (buffer.size < byteCount) {// 最终的最终会调用到cacheWritingSource内名内部类的read()方法,在最内层的输入流小节中已经分析过if (source.read(buffer, Segment.SIZE.toLong()) == -1L) return false}return true
}

方法执行完毕后,查看文件发现写了10个字节到响应体文件中:

读完头,该读正文了,回到上述响应体文件的写入小节中的5.GzipSource继续向下分析

写入正文数据

InflaterSource#read

override fun read(sink: Buffer, byteCount: Long): Long {while (true) {// val bytesInflated = readOrInflate(sink, byteCount)if (bytesInflated > 0) return bytesInflatedif (inflater.finished() || inflater.needsDictionary()) return -1L// exhausted()则将剩余的内容全部读取完毕, 看本节下1.RealBufferedSource#exhaustedif (source.exhausted()) throw EOFException("source exhausted prematurely") }
}

1.RealBufferedSource#exhausted

override fun exhausted(): Boolean = commonExhausted()internal inline fun RealBufferedSource.commonExhausted(): Boolean {check(!closed) { "closed" }// 读取数据,read()方法又会执行到上述最内层的输入流小节的read方法,则又继续向文件中写入数据return buffer.exhausted() && source.read(buffer, Segment.SIZE.toLong()) == -1L
}

每执行一次,都会像响应体文件中输出512个字节,最初已经有10个字节的头

执行一次:

执行两次:

直到数据完全读完完毕,则会将tmp去掉变成正式缓存文件,在上述最内层的输入流小节已经分析过。

总结

缓存的一次完整的增操作相对来说比较复杂,put时会先创建tmp的临时缓存文件,此时会先将头文件的数据进行写入,等后续在业务层调用string()后,才会将响应体数据真正写入文件,其中GizpSource装饰层会进行分段,一段一段进行写入,总的过程就是将网络的输入流的数据读到内存再写入缓存文件,等数据完全读取完毕则会将文件改名为正式文件,结束此次操作。

此小节分析获取操作,重点在于Cacheget()方法

Cache#get

// 根据Request获取缓存中的Response
internal fun get(request: Request): Response? {// 根据请求的url生成key,之前再添加时我们知道DiskLruCache存在一个map,此key就用于获取map中的valueval key = key(request.url)val snapshot: DiskLruCache.Snapshot = try { cache[key] ?: return null //cache[key]调用DiskLruCache的get方法,下Snapshot的创建分析} catch (_: IOException) {return null // Give up because the cache cannot be read.}// 创建Cache.Entry,看下Cache.Entry的创建小节val entry: Entry = try {Entry(snapshot.getSource(ENTRY_METADATA)) //ENTRY_METADATA为0,snapshot.getSource()则是返回下标为0的文件输入流,代表着响应头数据} catch (_: IOException) {snapshot.closeQuietly()return null}// 返回一个响应,看下响应的创建小节val response = entry.response(snapshot)if (!entry.matches(request, response)) {response.body?.closeQuietly()return null}return response
}

Snapshot的创建

在调用Cacheget()方法则会取调用DiskLruCacheget()方法返回一个Snapshot,下面分析此方法

1.DiskLruCache#get

@Synchronized @Throws(IOException::class)
operator fun get(key: String): Snapshot? {//若没初始化DiskLruCache,则进行初始化,目的就是解析日志文件初始化lruEntriesinitialize()checkNotClosed()// 判断key是否有效validateKey(key)// 获取key响应的valueval entry = lruEntries[key] ?: return null// 获取Snapshot,下2.DiskLruCache.Entry#snapshot分析val snapshot = entry.snapshot() ?: return null// 获取Snapshot成功,则将日志数目进行++redundantOpCount++// 写入READ日志journalWriter!!.writeUtf8(READ).writeByte(' '.toInt()).writeUtf8(key).writeByte('\n'.toInt())// 此方法在增小节的最内层的输入流小节5.DiskLruCache#journalRebuildRequired进行了分析if (journalRebuildRequired()) {cleanupQueue.schedule(cleanupTask)}return snapshot
}

2.DiskLruCache.Entry#snapshot

internal fun snapshot(): Snapshot? {this@DiskLruCache.assertThreadHoldsLock()if (!readable) return null// 平台不允许文件同时读写,而且存在编辑器或者需要删除则返回null,不允许读取此缓存if (!civilizedFileSystem && (currentEditor != null || zombie)) return null// 输入流集合val sources = mutableListOf<Source>()// 长度集合,size为2,下标0代表响应头文件的长度,下标1代表响应体的长度val lengths = this.lengths.clone() // Defensive copy since these can be zeroed out.try {// valueCount为2,一个响应分为头和体两个文件,此变量的最早来源就是创建DiskLruCache的时候for (i in 0 until valueCount) {// newSource()方法看下3.DiskLruCache.Entry#newSourcesources += newSource(i)}// 返回Snapshot,其构造就是简单的赋值不在分析,所以此类只是整合数据,四个属性(key,此次操作的序列号,文件输入流集合,文件长度集合)return Snapshot(key, sequenceNumber, sources, lengths)} catch (_: FileNotFoundException) {// A file must have been deleted manually!for (source in sources) {source.closeQuietly()}// Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache// size.)try {removeEntry(this)} catch (_: IOException) {}return null}
}

3.DiskLruCache.Entry#newSource

返回cleanFiles数组中的文件输入流

private fun newSource(index: Int): Source {// 通过文件系统获取到响应文件的输入流val fileSource = fileSystem.source(cleanFiles[index])// 平台允许同时读写则直接返回if (civilizedFileSystem) return fileSource// 将正在使用的source数目进行++lockingSourceCount++// 对输入流进行包装,不允许同时读写才会返回此包装类型,主要是close时进行加锁,装饰结构为ForwardingSource(fileSource)return object : ForwardingSource(fileSource) {private var closed = falseoverride fun close() {super.close()if (!closed) {closed = truesynchronized(this@DiskLruCache) {lockingSourceCount--// 无人使用且需要删除则删除此缓存条目if (lockingSourceCount == 0 && zombie) {removeEntry(this@Entry)}}}}}
}
}

Cache.Entry的创建

获取输入流,将响应头的数据解析出来,此方法没有好解释的,读者简单看一下就行,只需要记得Cache.Entry这个对象里包含了响应头的数据即可。

Cache.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.protocolcode = statusLine.codemessage = statusLine.messageval responseHeadersBuilder = Headers.Builder()val responseHeaderLineCount = readInt(source)for (i in 0 until responseHeaderLineCount) {responseHeadersBuilder.addLenient(source.readUtf8LineStrict())}val sendRequestMillisString = responseHeadersBuilder[SENT_MILLIS]val receivedResponseMillisString = responseHeadersBuilder[RECEIVED_MILLIS]responseHeadersBuilder.removeAll(SENT_MILLIS)responseHeadersBuilder.removeAll(RECEIVED_MILLIS)sentRequestMillis = sendRequestMillisString?.toLong() ?: 0LreceivedResponseMillis = receivedResponseMillisString?.toLong() ?: 0LresponseHeaders = responseHeadersBuilder.build()if (isHttps) {val blank = source.readUtf8LineStrict()if (blank.isNotEmpty()) {throw IOException("expected \"\" but was \"$blank\"")}val cipherSuiteString = source.readUtf8LineStrict()val cipherSuite = CipherSuite.forJavaName(cipherSuiteString)val peerCertificates = readCertificateList(source)val localCertificates = readCertificateList(source)val tlsVersion = if (!source.exhausted()) {TlsVersion.forJavaName(source.readUtf8LineStrict())} else {TlsVersion.SSL_3_0}handshake = Handshake.get(tlsVersion, cipherSuite, peerCertificates, localCertificates)} else {handshake = null}} finally {rawSource.close()}
}

响应的创建

Cache.Entry#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)) //最重要的代码,决定了响应体文件的解析,看下1.CacheResponseBody.handshake(handshake).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(receivedResponseMillis).build()
}

1.CacheResponseBody

private class CacheResponseBody(val snapshot: DiskLruCache.Snapshot,private val contentType: String?,private val contentLength: String?
) : ResponseBody() {private val bodySource: BufferedSourceinit {// ENTRY_BODY为1,获取数组中的第二个输入流,指向响应体文件,source的装饰结构为ForwardingSource(fileSource)val source = snapshot.getSource(ENTRY_BODY)// 将source再次包装,RealBufferedSource(ForwardingSource(ForwardingSource(fileSource)))// 在BridgeInterceptor中又会在包装一层GzipSourcebodySource = object : ForwardingSource(source) {@Throws(IOException::class)override fun close() {// 此装饰层主要是关闭输入流snapshot.close()super.close()}}.buffer()}
}

响应体文件数据读取

响应头文件在创建Cache.Entry时已经读取了,而响应体的数据还未读取,真正将响应体数据读取进内存的是ResponseBodystring()方法,此方法在上述小节已经分析过。

修改缓存的方法则是Cacheupdate()方法,更新主要是对头数据进行更新,具体更新啥数据本篇文章不进行过多的讲述,只分析对文件的操作。

Cache#update

传入接旧的Response和新的有效的Response,需要理解的是旧的Response一定是通过查操作拿到的

internal fun update(cached: Response, network: Response) {// 根据新的Response创建Cache.Entry,在增小节的1.Cache.Entry已经分析val entry = Entry(network)// 拿到旧的Response的snapshot,snapshot内部包含旧的缓存文件的流,在查小节的Snapshot的创建中已经进行分析val snapshot = (cached.body as CacheResponseBody).snapshotvar editor: DiskLruCache.Editor? = nulltry {// 获取新的编辑器,看下述1.DiskLruCache#Snapshot.edit分析editor = snapshot.edit() ?: return // edit() returns null if snapshot is not current.entry.writeTo(editor) // 写入数据,在增小节的3.Cache.Entry#writeTo已经分析过editor.commit() // 完毕编辑,在增小节的最内层的输入流小节中的1.Edit.commit已经分析过} catch (_: IOException) {abortQuietly(editor)}
}

1.DiskLruCache#Snapshot.edit

@Throws(IOException::class)
fun edit(): Editor? = this@DiskLruCache.edit(key, sequenceNumber) // 会调用到DiskLruCache.edit()方法,在增小节的2.DiskLruCache#edit已经分析过

总结: 总体来说比较简单,很多方法在上述都已经分析过,改的过程是,利用旧的响应获取到原先的文件流,然后调用entry.writeTo(editor)将新的响应写入到tmp文件,最后调用editor.commit(),将tmp文件转换成正式文件。

注意:转换文件是通过调用SystemFileSystemrename()方法,此方法是先删掉原先的文件然后再改名,因此不会造成文件名字冲突。

Cache#remove

@Throws(IOException::class)
internal fun remove(request: Request) {cache.remove(key(request.url)) //看下1.DiskLruCache#remove
}

1.DiskLruCache#remove

@Synchronized @Throws(IOException::class)
fun remove(key: String): Boolean {initialize() // 在DiskLruCache的初始化小节已经分析过checkNotClosed()validateKey(key) // 检测key是否有效val entry = lruEntries[key] ?: return false // 获取当前的Entryval removed = removeEntry(entry)  // 删除此条缓存if (removed && size <= maxSize) mostRecentTrimFailed = falsereturn removed
}

2.DiskLruCache#removeEntry

@Throws(IOException::class)
internal fun removeEntry(entry: Entry): Boolean {// 不允许同时读写文件,则进行标记if (!civilizedFileSystem) {// 还有打开的输入流if (entry.lockingSourceCount > 0) {// 将此条数据标记成DIRTY,让其失效journalWriter?.let {it.writeUtf8(DIRTY)it.writeByte(' '.toInt())it.writeUtf8(entry.key)it.writeByte('\n'.toInt())it.flush()}}// 修改zombie删除标记位为true,为zombie则标记此缓存无效,且后续会被删除if (entry.lockingSourceCount > 0 || entry.currentEditor != null) {entry.zombie = truereturn true}}// 干掉编辑器entry.currentEditor?.detach() // Prevent the edit from completing normally.// 删除响应的clean文件for (i in 0 until valueCount) {fileSystem.delete(entry.cleanFiles[i])size -= entry.lengths[i]entry.lengths[i] = 0}// 日志条数++redundantOpCount++// 写入REMOVE日志journalWriter?.let {it.writeUtf8(REMOVE)it.writeByte(' '.toInt())it.writeUtf8(entry.key)it.writeByte('\n'.toInt())}// 移除此EntrylruEntries.remove(entry.key)if (journalRebuildRequired()) {cleanupQueue.scheule(cleanupTask)}return true
}

总结:根据传入的key,先判断平台是否支持同时读写文件,若支持则直接删除文件,写入REMOVE日志并移除Entry,若不支持则再判断是否有正在打开的流,若有则标记下来此缓存,并修改日志为DIRTY,修改删除标志位等后续再删除此条缓存。

分析到文章开头的问题旧就得出答案了。

文章开头问题的答案

OkHttp的缓存策略是什么?

硬盘LRU缓存

缓存日志是什么?

记录对缓存的操作,具体有四个指令DIRTYCLEANREADREMOVE,具体作用可以回顾日志格式分析小节

缓存文件是如何生成的?

获取Editor时写入DIRTY日志,然后根据响应头创建key.0.tmp文件并写入响应头数据,然后创建空的响应体文件key.1.tmp,等后续上层业务调用string()方法后才会将具体数据写入响应体文件,等全部数据写入完毕则会将tmp后缀去掉,并写入CLEAN指令,标志此缓存文件可用

多个日志文件类型是如何转换的?

有三种类型的缓存文件tmp,bkp,journal,三个文件的作用在日志文件的解析小节的最后进行了分析

缓存文件是如何读取的?

有指向响应头和响应体缓存文件的流,在读取缓存时,只是通过流对其中的数据进行了读取

缓存的增删改查是如何运行的?

上述增删改查四个小节中总结则是这个问题的答案

初衷

读者肯定认为笔者代码读的太细了,明显就是为了读源码而读源码,笔者和大众的观念是一样的不能为了读而读,但是笔者22届毕业,2022年6月14号刚到了自己的本科毕业证,作为即将踏入职场的新人,总感觉到了公司会看不懂公司的代码,与其惶惶不可终日,不如现在抓住机会锻炼一下阅读源码的能力。

下篇文章分析OkHttp的缓存机制

✨ 原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下} 原创不易,还希望各位大佬支持一下

OkHttp原理第五篇-Cache缓存类详解相关推荐

  1. 《深入理解mybatis原理》 MyBatis的一级缓存实现详解 及使用注意事项

    MyBatis是一个简单,小巧但功能非常强大的ORM开源框架,它的功能强大也体现在它的缓存机制上.MyBatis提供了一级缓存.二级缓存 这两个缓存机制,能够很好地处理和维护缓存,以提高系统的性能.本 ...

  2. Java Cache 缓存方案详解及代码-Ehcache

    一.Spring缓存概念 Spring从3.1开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManage ...

  3. 【JAVA基础篇】String类详解

    昨天参加了一场机试,发现自己居然对String类的api不熟了,所以今天来总结一下(基于JDK1.8). 1.父类和实现的接口 没有父类,或者说父类是Object 接口:Serializable.Co ...

  4. .net System.Web.Caching.Cache缓存类使用详解(转载)

    转自:http://www.cnblogs.com/virusswb/articles/1681561.html net System.Web.Caching.Cache缓存类使用详解 System. ...

  5. Redis五种基本数据类型底层详解(原理篇)

    Redis五种基本数据类型底层详解 详细介绍Redis用到的数据结构 简单动态字符串 SDS和C字符串的区别 总结 链表 字典 哈希表 字典 哈希算法 解决键冲突 rehash(重点) 渐进式reha ...

  6. OKHTTP之缓存配置详解

    前言 在Android开发中我们经常要进行各种网络访问,比如查看各类新闻.查看各种图片.但有一种情形就是我们每次重复发送的网络请求其实返回的内容都是一样的.比如一个电影类APP,每一次向服务器申请某个 ...

  7. InheritableThreadLocal类原理简介使用 父子线程传递数据详解 多线程中篇(十八)...

    上一篇文章中对ThreadLocal进行了详尽的介绍,另外还有一个类: InheritableThreadLocal 他是ThreadLocal的子类,那么这个类又有什么作用呢? 测试代码 publi ...

  8. [NewLife.XCode]实体类详解

    NewLife.XCode是一个有10多年历史的开源数据中间件,由新生命团队(2002~2019)开发完成并维护至今,以下简称XCode. 整个系列教程会大量结合示例代码和运行日志来进行深入分析,蕴含 ...

  9. HTTP缓存机制详解

    HTTP缓存机制详解 一. 前言 二. 缓存的介绍 什么是缓存? 为什么要使用缓存? 1. 减少冗余的数据传输 2. 缓解带宽瓶颈 3. 破坏瞬间拥塞 4. 降低距离时延 三. 缓存有效性 命中和未命 ...

最新文章

  1. 厉害了!单点登录系统用 8 张漫画就解释了。。。
  2. Spotify敏捷模式详解三部曲第二篇:研发过程
  3. 算法小记 · 字符串翻转
  4. 《Linux From Scratch》第一部分:介绍 第一章:介绍-1.3. 更新日志
  5. Linux 监控命令之 netstat
  6. [Java] 蓝桥杯ADV-175 算法提高 三个整数的排序
  7. stl sort分析
  8. [linux]centos7.4上升级python2版本到python3.6.5 【安装双版本,默认python3】
  9. 计算机专业期末试卷分析,计算机期末试卷分析
  10. x86架构应用如何向Arm架构低成本迁移
  11. authorized_key 不生效。
  12. MySQL无法启动 系统发生1058错误
  13. 优测云服务平台分享开源自动化测试框架,快快get起来
  14. Delphi名称的由来(原作:Borland公司Danny Thorpe)
  15. Gson:GitHub 标星 18K 的 JSON 解析器,Google 出品的 Java JSON 解析器,强烈推荐!
  16. JS 特效学习 002:图片渐显
  17. tiptop自定义发送邮件
  18. 男主龙失忆java_不容错过的3本男主失忆文:我忘记了以前的所有,却从未忘记爱你...
  19. CAN转串口智能模块/CAN转232 CAN转485
  20. 配置IIS服务器,支持sis、SISX、3GP、APK,CAB、flv等文件下载

热门文章

  1. 基于Tensorflow的图像特效合成算法研究
  2. 火星坐标(gcj02)、国测局坐标(GPS)和百度坐标(bd0911)互转
  3. .net链接sql情况下实现增删查改
  4. 三目运算符引起的NPE
  5. 分布式消息中间件设计
  6. Python生成+识别二维码
  7. Java毕设项目住房公积金筹集子系统的网站系统计算机(附源码+系统+数据库+LW)
  8. Java毕设项目滁州市住房公积金管理中心网站计算机(附源码+系统+数据库+LW)
  9. 使用yum命令报错 except KeyboardInterrupt, e: SyntaxError: invalid syntax
  10. 互联网凛冬,看大厂HR怎么说~