本文基于OkHttp 4.3.1源碼分析
OkHttp - 官方地址
OkHttp - GitHub代碼地址
預備知識

概述
OkHttp整體流程(本文覆蓋紅色部分)

緩存處理流程

緩存文件夾

緩存日志格式

源碼分析
測試代碼
如果需要緩存機制,那么在構造OkHttpClient的時候需要傳入一個Cache實例。
下面是OkHttp提供的一個CacheResponse的用例,除了傳入一個Cache構造OkHttpClient,其它完全一樣。
OkHttp實現(xiàn)緩存的切入點依舊是攔截器,之前的文章我們知道攔截器處理對象可以接受命令對象并根據(jù)自身情況選擇處理還是不處理,所以接下來就是直接從CacheInterceptor開始分析
public final class CacheResponse {
private final OkHttpClient client;
public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB 大小
Cache cache = new Cache(cacheDirectory, cacheSize); // 路徑
// 要使用緩存功能,需要在構造OkHttpClient的時候傳入 cache (緩存大小,緩存路徑地址)
client = new OkHttpClient.Builder()
.cache(cache)
.build();
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
String response1Body;
try (Response response1 = client.newCall(request).execute()) {
...
}
String response2Body;
try (Response response2 = client.newCall(request).execute()) {
...
}
// 兩次請求結果一致
System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
}
public static void main(String... args) throws Exception {
new CacheResponse(new File("CacheResponse.tmp")).run();
}
}
緩存攔截器邏輯
CacheInterceptor.intercept
override fun intercept(chain: Interceptor.Chain): Response {
//
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
// 緩存追蹤,網絡請求數(shù)、命中緩存數(shù),兩者比值可以查看緩存命中率
cache?.trackResponse(strategy)
if (cacheCandidate != null && cacheResponse == null) {
// The cache candidate wasn't applicable. Close it.
cacheCandidate.body?.closeQuietly()
}
// 異常情況
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()
}
// 如果networkRequest為null,則直接使用緩存數(shù)據(jù),攔截器處理至此終結,開始響應階段
if (networkRequest == null) {
return cacheResponse!!.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build()
}
var networkResponse: Response? = null
try {
// 繼續(xù)執(zhí)行 攔截器鏈處理方法,最終發(fā)送網絡請求,到讀取網絡數(shù)據(jù)
networkResponse = chain.proceed(networkRequest)
} finally {
if (networkResponse == null && cacheCandidate != null) {
cacheCandidate.body?.closeQuietly()
}
}
if (cacheResponse != null) {
if (networkResponse?.code == HTTP_NOT_MODIFIED) { // 304
// 如果網絡數(shù)據(jù)標志性沒有改變,開始返回數(shù)據(jù)
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
} 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)) {
// 對響應數(shù)據(jù)進行緩存
val cacheRequest = cache.put(response)
// 返回一個帶有new Source的body讀取流 的 Response
return cacheWritingResponse(cacheRequest, response)
}
if (HttpMethod.invalidatesCache(networkRequest.method)) {
try {
cache.remove(networkRequest)
} catch (_: IOException) {
// The cache cannot be written.
}
}
}
return response
}
緩存獲取邏輯
Cache.get
- 生成請求對應的唯一標志key
- 通過DiskLruCache獲取對應key的緩存快照數(shù)據(jù)
- 如果有快照,則將快照對應的緩存資源數(shù)據(jù)通過Okio中流讀寫轉換成內存數(shù)據(jù)
- 最后返回構造好的Response
internal fun get(request: Request): Response? {
// 根據(jù)請求url字符串進行md5作為緩存對應的key標識
val key = key(request.url)
// Cache成員變量DiskLruCache,緩存邏輯實現(xiàn)類
// snapshot
val snapshot: DiskLruCache.Snapshot = try {
// 從DiskLruCache中取緩存
cache[key] ?: return null
} catch (_: IOException) {
return null // Give up because the cache cannot be read.
}
// 讀取快照中的緩存資源流 ,構造數(shù)據(jù)Entry
val entry: Entry = try {
Entry(snapshot.getSource(ENTRY_METADATA))
} catch (_: IOException) {
snapshot.closeQuietly()
return null
}
// 取 response
val response = entry.response(snapshot)
if (!entry.matches(request, response)) {
response.body?.closeQuietly() // 響應和請求 不匹配則關閉
return null
}
return response // 返回響應數(shù)據(jù)
}
Cache.Entry 構造
緩存數(shù)據(jù)實例,DiskLruCache中l(wèi)ruEntries存儲了請求key對應的快照,快照里有key對應的本地文件讀取流,根據(jù)讀取流讀取本地數(shù)據(jù),轉換輸出構造Entry實例,最終應用構造出一個緩存的Response
internal constructor(rawSource: Source) {
try {
val source = rawSource.buffer()
// 從緩存文件流中 讀取數(shù)據(jù) ,寫入到Entry中
... // 一堆 讀取流 寫入內存操作
} finally {
rawSource.close()
}
}
DiskLruCache.get
- 初始化或者確認初始化(主要目的是將key和對應緩存本地文件標識存儲到內存Map中,便于查詢)
- 如果找到了就返回一個快照對象,沒有則為null
operator fun get(key: String): Snapshot? {
initialize() // 初始化 || 確認已經初始化
checkNotClosed() // check cache是否關閉
validateKey(key) // 確認key 符合 寫入的規(guī)則
// 找key對應的緩存entry
val entry = lruEntries[key] ?: return null
if (!entry.readable) return null
// 有緩存entry 取對應的快照
val snapshot = entry.snapshot() ?: return null
// 標識一次操作
redundantOpCount++
// 寫入 標記READ 和對應 key 一行
journalWriter!!.writeUtf8(READ)
.writeByte(' '.toInt())
.writeUtf8(key)
.writeByte('\n'.toInt())
// 如果操作>2000次了,則需要重新清理 日志
if (journalRebuildRequired()) {
// 執(zhí)行清理任務
cleanupQueue.schedule(cleanupTask)
}
// 返回快照
return snapshot
}
DiskLruCache.initialize
- 整理日志文件,刪除backup或者重命名bakcup日志文件
- 讀取日志文件,生成lruEntries內存緩存
- 處理日志文件
- 按需是否重新構建日志文件
// 初始化方法
fun initialize() {
this.assertThreadHoldsLock()
// 是否已經初始化 ,內存緩存
if (initialized) {
return // Already initialized.
}
// 開始初始化工作
// 如果存在backup的日志文件,
// 則看是否存在日志文件,如果存在日志文件則刪除backup的,
// 如果沒有則將backup文件轉化為日志文件
if (fileSystem.exists(journalFileBackup)) {
if (fileSystem.exists(journalFile)) {
fileSystem.delete(journalFileBackup)
} else {
fileSystem.rename(journalFileBackup, journalFile)
}
}
// journal文件存在 開始讀取工作
if (fileSystem.exists(journalFile)) {
try {
readJournal()
processJournal()
initialized = true
return
} 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
}
}
rebuildJournal()
initialized = true
}
DiskLruCache.readJournal
- 讀取日志,判斷日志是否可用
- 日志可用,則讀取每行日志,構建Key、Entry的map到內存緩存
- 如果讀取過程中有問題,則根據(jù)tmp日志重新構建日志
// 讀取日志文件,構造[key,entry]內存緩存集合
private fun readJournal() {
fileSystem.source(journalFile).buffer().use { source ->
// 基于Okio讀寫本地數(shù)據(jù)
// libcore.io.DiskLruCache
val magic = source.readUtf8LineStrict()
val version = source.readUtf8LineStrict() // 版本 1
val appVersionString = source.readUtf8LineStrict() // app version
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 = 0
while (true) {
try {
// 見readJournalLine方法分析(讀取key,構造entry)
readJournalLine(source.readUtf8LineStrict())
lineCount++
} catch (_: EOFException) {
break // End of journal.
}
}
// 無用的line數(shù)目
redundantOpCount = lineCount - lruEntries.size
// 如果讀取有無,則重構建日志
if (!source.exhausted()) {
// 通過Okio讀取tmp日志文件重新寫Journal文件
rebuildJournal()
} else {
journalWriter = newJournalWriter() // 創(chuàng)建日志writer
}
}
}
DiskLruCache.readJournalLine
- 讀取每一行日志
- 根據(jù)行日志屬性,CLEAN、DIRTY、REMOVE、READ進行對應的處理
- 緩存到內存緩存LruEntries的Map中
// 讀取每一行日志,存儲到LruEntries Map中
private fun readJournalLine(line: String) {
// 找 第一個空格的 指引
val firstSpace = line.indexOf(' ')
if (firstSpace == -1) throw IOException("unexpected journal line: $line")
// 從第一個空格指引+1位置開始找第二個空格指引
val keyBegin = firstSpace + 1
val secondSpace = line.indexOf(' ', keyBegin)
val key: String
// 如果第二個空格指引沒有找到
if (secondSpace == -1) {
// 截取第一個空格指引到字符串最后的字段,截取的字段賦值key
key = line.substring(keyBegin)
// 如果第一個空格指引大小為Remove長度或者line是以remove開頭
if (firstSpace == REMOVE.length && line.startsWith(REMOVE)) {
// lruEntries 移除key
lruEntries.remove(key)
return
}
} else {
// 如果第二個空格存在,則key賦值為第一個空格和第二個空格之間的字符串值
key = line.substring(keyBegin, secondSpace)
}
// 取key對應的Entry
var entry: Entry? = lruEntries[key]
if (entry == null) {
// 不存砸,則創(chuàng)建Entry并賦值
entry = Entry(key)
lruEntries[key] = entry
}
when {
secondSpace != -1 && firstSpace == CLEAN.length && line.startsWith(CLEAN) -> {
// 示例數(shù)據(jù) CLEAN 4b217e04ba52215f3a6b64d28f6729c6 333 194
// 讀line間隔有多少個 方便存儲對應大小數(shù)據(jù)
val parts = line.substring(secondSpace + 1)
.split(' ')
entry.readable = true // 設置可讀
entry.currentEditor = null // 重置null
entry.setLengths(parts) // 根據(jù)文件個數(shù),設置每個文件大小
}
// 示例數(shù)據(jù):DIRTY 4b217e04ba52215f3a6b64d28f6729c6
secondSpace == -1 && firstSpace == DIRTY.length && line.startsWith(DIRTY) -> {
// 賦值currentEditor為對應Entry的Editor
entry.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")
}
}
DiskLruCache.processJournal
- 計算有效文件的總大小值
- 刪除無效本地緩存文件和內存緩存key
private fun processJournal() {
// 刪除journal.tmp臨時文件
fileSystem.delete(journalFileTmp)
val i = lruEntries.values.iterator()
// 遍歷lruEntries ,計算currentEditor為null對應的文件總大小
while (i.hasNext()) {
val entry = i.next()
if (entry.currentEditor == null) {
for (t in 0 until valueCount) {
size += entry.lengths[t]
}
} else {
entry.currentEditor = null
// 刪除currentEditor文件(對應Dirty文件)
for (t in 0 until valueCount) {
fileSystem.delete(entry.cleanFiles[t])
fileSystem.delete(entry.dirtyFiles[t])
}
i.remove()
}
}
}
緩存使用策略邏輯
緩存使用策略:給定請求和緩存響應,決策出用網絡響應還是緩存響應,或者兩者都有
CacheStrategy.init
- 讀取請求時間戳、響應時間戳
- 讀取緩存策略決策的關鍵Header,Date、Expires、Last-Modified、ETag
init {
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis
val headers = cacheResponse.headers
for (i in 0 until headers.size) {
val fieldName = headers.name(i)
val value = headers.value(i)
when {
// 讀取緩存策略決策的關鍵Header
// Date、Expires、Last-Modified、ETag
fieldName.equals("Date", ignoreCase = true) -> {
servedDate = value.toHttpDateOrNull()
servedDateString = value
}
fieldName.equals("Expires", ignoreCase = true) -> {
expires = value.toHttpDateOrNull()
}
fieldName.equals("Last-Modified", ignoreCase = true) -> {
lastModified = value.toHttpDateOrNull()
lastModifiedString = value
}
fieldName.equals("ETag", ignoreCase = true) -> {
etag = value
}
fieldName.equals("Age", ignoreCase = true) -> {
ageSeconds = value.toNonNegativeInt(-1)
}
}
}
}
}
CacheStrategy.compute
執(zhí)行CacheStrategy.computeCandidate策略,最后返回四種策略結果
- CacheStrategy(request, null); 無緩存,需要取網絡數(shù)據(jù)
- CacheStrategy(null, cacheResponse);直接使用緩存
- CacheStrategy(conditionalRequestHeaders,cacheResponse);構建新的網絡請求(加Header)需要新的網絡請求返回后判斷是否使用哪一個
- CacheStrategy(null, null); 錯誤場景
fun compute(): CacheStrategy {
val candidate = computeCandidate()
// 禁止構建新的請求 且 緩存數(shù)據(jù)有沒有,則返回null,這一般是個錯誤場景
if (candidate.networkRequest != null && request.cacheControl.onlyIfCached) {
return CacheStrategy(null, null)
}
return candidate
}
CacheStrategy.computeCandidate
緩存策略一系列判斷,最后有三種策略返回結果
- CacheStrategy(request, null); 無緩存,需要取網絡數(shù)據(jù)
- CacheStrategy(null, cacheResponse);直接使用緩存
- CacheStrategy(conditionalRequestHeaders,cacheResponse);構建新的網絡請求(加Header)需要新的網絡請求返回后判斷是否使用哪一個
private fun computeCandidate(): CacheStrategy {
// 無緩存
if (cacheResponse == null) {
return CacheStrategy(request, null)
}
// https請求 且無handshake數(shù)據(jù)
if (request.isHttps && cacheResponse.handshake == null) {
return CacheStrategy(request, null)
}
// 不支持緩存
if (!isCacheable(cacheResponse, request)) {
return CacheStrategy(request, null)
}
// 配置了cacheControl字段 說明不需要cache
val requestCaching = request.cacheControl
if (requestCaching.noCache || hasConditions(request)) {
return CacheStrategy(request, null)
}
// 計算!noCache情況Header下,是否過期,是否可以直接使用Cache數(shù)據(jù)
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())
}
// !noCache Header標志,且緩存數(shù)據(jù)未過期,則直接使用
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())
}
// 將header數(shù)據(jù)取出,帶入新的請求header中
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.
}
// 構建新的網絡請求,返回帶有新的網絡請求+cacheReponse數(shù)據(jù)
val conditionalRequestHeaders = request.headers.newBuilder()
conditionalRequestHeaders.addLenient(conditionName, conditionValue!!)
val conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build()
return CacheStrategy(conditionalRequest, cacheResponse)
}
緩存存儲邏輯
Cache.put
?? 對于非GET請求,不做緩存邏輯,原因:POST請求雖然可以做到緩存邏輯,但是實現(xiàn)復雜度和收益比非常低,所以沒有處理非Get的請求緩存
internal fun put(response: Response): CacheRequest? {
// 請求方法
val requestMethod = response.request.method
// 如果是非Get則為true
if (HttpMethod.invalidatesCache(response.request.method)) {
try {
remove(response.request) // 移除緩存
} catch (_: IOException) {
// The cache cannot be written.
}
return null
}
// 對于非GET請求,不做緩存邏輯,原因:POST請求雖然可以做到緩存邏輯,但是實現(xiàn)復雜度和收益比非常低,所以沒有做處理
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
}
// 構造 Entry
val entry = Entry(response)
var editor: DiskLruCache.Editor? = null
try {
// Entry 編輯器,數(shù)據(jù)書寫
editor = cache.edit(key(response.request.url)) ?: return null
entry.writeTo(editor)
// 構造一個 RealCacheRequest返回
return RealCacheRequest(editor)
} catch (_: IOException) {
abortQuietly(editor)
return null
}
}
DiskLruCache.edit
主要是獲取編輯器,編輯器獲取的同時,DiskLruCache內存緩存中會存儲對應的key和Entry,entry會指定對應的Editor,且會寫入一條數(shù)據(jù)到日志中,目前日志key對應的數(shù)據(jù)是DIRTY
fun edit(key: String, expectedSequenceNumber: Long = ANY_SEQUENCE_NUMBER): Editor? {
initialize()
checkNotClosed()
validateKey(key)
// key對應Entry
var entry: Entry? = lruEntries[key]
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER &&
(entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
return null // Snapshot is stale.
}
// 是否正在編輯
if (entry?.currentEditor != null) {
return null // Another edit is in progress.
}
// 是否需要執(zhí)行clean
if (mostRecentTrimFailed || mostRecentRebuildFailed) {
cleanupQueue.schedule(cleanupTask)
return null
}
// 寫入 key 到日志中 ,目前對應是 DIRTY
val journalWriter = this.journalWriter!!
journalWriter.writeUtf8(DIRTY)
.writeByte(' '.toInt())
.writeUtf8(key)
.writeByte('\n'.toInt())
journalWriter.flush()
if (hasJournalErrors) {
return null // Don't edit; the journal can't be written.
}
// 構造Entry,存儲到map中
if (entry == null) {
entry = Entry(key)
lruEntries[key] = entry
}
// 構造編輯器,及Etry賦值當前編輯器
val editor = Editor(entry)
entry.currentEditor = editor
return editor
}
Cache.writeTo
通過Okio將響應數(shù)據(jù)寫入到本地緩存
fun writeTo(editor: DiskLruCache.Editor) {
val sink = editor.newSink(ENTRY_METADATA).buffer()
sink.writeUtf8(url).writeByte('\n'.toInt())
sink.writeUtf8(requestMethod).writeByte('\n'.toInt())
sink.writeDecimalLong(varyHeaders.size.toLong()).writeByte('\n'.toInt())
for (i in 0 until varyHeaders.size) {
sink.writeUtf8(varyHeaders.name(i))
.writeUtf8(": ")
.writeUtf8(varyHeaders.value(i))
.writeByte('\n'.toInt())
}
// 寫入數(shù)據(jù)
...
sink.close()
}