OkHttp3源码详解(四)缓存策略,万分膜拜
//如果当前缓存不符合要求,将其closeif (cacheCandidate != null && cacheResponse == null) {closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.}// 如果不能使用网络,同时又没有符合条件的缓存,直接抛504错误if (networkRequest == null && cacheResponse == null) {return new Response.Builder().request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(Util.EMPTY_RESPONSE).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();}// 如果有缓存同时又不使用网络,则直接返回缓存结果if (networkRequest == null) {return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();}//尝试通过网络获取回复Response networkResponse = 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) {closeQuietly(cacheCandidate.body());}}// 如果既有缓存,同时又发起了请求,说明此时是一个Conditional Get请求if (cacheResponse != null) {// 如果服务端返回的是NOT_MODIFIED,缓存有效,将本地缓存和网络响应做合并if (networkResponse.code() == HTTP_NOT_MODIFIED) {Response 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 {// 如果响应资源有更新,关掉原有缓存closeQuietly(cacheResponse.body());}}Response response = networkResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).networkResponse(stripBody(networkResponse)).build();if (cache != null) {if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {// 将网络响应写入cache中CacheRequest cacheRequest = cache.put(response);return cacheWritingResponse(cacheRequest, response);}if (HttpMethod.invalidatesCache(networkRequest.method())) {try {cache.remove(networkRequest);} catch (IOException ignored) {// The cache cannot be written.}}}return response; ```} 核心逻辑都以中文注释的形式在代码中标注出来了,大家看代码即可。通过上面的代码可以看出,几乎所有的动作都是以CacheStrategy缓存策略为依据做出的,那么接下来看下缓存策略是如何生成的,相关代码实现在CacheStrategy$Factory.get()方法中:\[CacheStrategy$Factory\]```/*** Returns a strategy to satisfy {@code request} using the a cached response {@code response}.*/public CacheStrategy get() {CacheStrategy candidate = getCandidate();if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {// We're forbidden from using the network and the cache is insufficient.return new CacheStrategy(null, null);}return candidate;}/** Returns a strategy to use assuming the request can use the network. */private CacheStrategy getCandidate() {// 若本地没有缓存,发起网络请求if (cacheResponse == null) {return new CacheStrategy(request, null);}// 如果当前请求是HTTPS,而缓存没有TLS握手,重新发起网络请求if (request.isHttps() && cacheResponse.handshake() == null) {return new 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 new CacheStrategy(request, null);}//如果当前的缓存策略是不缓存或者是conditional get,发起网络请求CacheControl requestCaching = request.cacheControl();if (requestCaching.noCache() || hasConditions(request)) {return new CacheStrategy(request, null);}//ageMillis:缓存agelong ageMillis = cacheResponseAge();//freshMillis:缓存保鲜时间long freshMillis = computeFreshnessLifetime();if (requestCaching.maxAgeSeconds() != -1) {freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));}long minFreshMillis = 0;if (requestCaching.minFreshSeconds() != -1) {minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());}long maxStaleMillis = 0;CacheControl responseCaching = cacheResponse.cacheControl();if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());}//如果 age + min-fresh >= max-age && age + min-fresh < max-age + max-stale,则虽然缓存过期了, //但是缓存继续可以使用,只是在头部添加 110 警告码if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {Response.Builder builder = cacheResponse.newBuilder();if (ageMillis + minFreshMillis >= freshMillis) {builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");}long oneDayMillis = 24 * 60 * 60 * 1000L;if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");}return new CacheStrategy(null, builder.build());}// 发起conditional get请求String conditionName;String conditionValue;if (etag != null) {conditionName = "If-None-Match";conditionValue = etag;} else if (lastModified != null) {conditionName = "If-Modified-Since";conditionValue = lastModifiedString;} else if (servedDate != null) {conditionName = "If-Modified-Since";conditionValue = servedDateString;} else {return new CacheStrategy(request, null); // No condition! Make a regular request.}Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);Request conditionalRequest = request.newBuilder().headers(conditionalRequestHeaders.build()).build();return new CacheStrategy(conditionalRequest, cacheResponse);} ```可以看到其核心逻辑在getCandidate函数中。基本就是HTTP缓存协议的实现,核心代码逻辑已通过中文注释说明,大家直接看代码就好。3. DiskLruCache Cache内部通过DiskLruCache管理cache在文件系统层面的创建,读取,清理等等工作,接下来看下DiskLruCache的主要逻辑:public final class DiskLruCache implements Closeable, Flushable {final FileSystem fileSystem; final File directory; private final File journalFile; private final File journalFileTmp; private final File journalFileBackup; private final int appVersion; private long maxSize; final int valueCount; private long size = 0; BufferedSink journalWriter; final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<>(0, 0.75f, true);// Must be read and written when synchronized on ‘this’. boolean initialized; boolean closed; boolean mostRecentTrimFailed; boolean mostRecentRebuildFailed;/\*\** To differentiate between old and current snapshots, each entry is given a sequence number each* time an edit is committed. A snapshot is stale if its sequence number is not equal to its* entry’s sequence number. \*/ private long nextSequenceNumber = 0;/\*\* Used to run ‘cleanupRunnable’ for journal rebuilds. \*/ private final Executor executor; private final Runnable cleanupRunnable = new Runnable() { public void run() { … } }; … } 3.1 journalFile DiskLruCache内部日志文件,对cache的每一次读写都对应一条日志记录,DiskLruCache通过分析日志分析和创建cache。日志文件格式如下:```libcore.io.DiskLruCache11002CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054DIRTY 335c4c6028171cfddfbaae1a9c313c52CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342REMOVE 335c4c6028171cfddfbaae1a9c313c52DIRTY 1ab96a171faeeee38496d8b330771a7aCLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234READ 335c4c6028171cfddfbaae1a9c313c52READ 3400330d1dfc7f3f7f4b8d4d803dfcf6前5行固定不变,分别为:常量:libcore.io.DiskLruCache;diskCache版本;应用程序版本;valueCount(后文介绍),空行接下来每一行对应一个cache entry的一次状态记录,其格式为:[状态(DIRTY,CLEAN,READ,REMOVE),key,状态相关value(可选)]:- DIRTY:表明一个cache entry正在被创建或更新,每一个成功的DIRTY记录都应该对应一个CLEAN或REMOVE操作。如果一个DIRTY缺少预期匹配的CLEAN/REMOVE,则对应entry操作失败,需要将其从lruEntries中删除- CLEAN:说明cache已经被成功操作,当前可以被正常读取。每一个CLEAN行还需要记录其每一个value的长度- READ: 记录一次cache读取操作- REMOVE:记录一次cache清除 ```日志文件的应用场景主要有四个:DiskCacheLru初始化时通过读取日志文件创建cache容器:lruEntries。同时通过日志过滤操作不成功的cache项。相关逻辑在DiskLruCache.readJournalLine,DiskLruCache.processJournal 初始化完成后,为避免日志文件不断膨胀,对日志进行重建精简,具体逻辑在DiskLruCache.rebuildJournal 每当有cache操作时将其记录入日志文件中以备下次初始化时使用 当冗余日志过多时,通过调用cleanUpRunnable线程重建日志 3.2 DiskLruCache.Entry 每一个DiskLruCache.Entry对应一个cache记录:private final class Entry { final String key;```/** Lengths of this entry's files. */final long[] lengths;final File[] cleanFiles;final File[] dirtyFiles;/** True if this entry has ever been published. */boolean readable;/** The ongoing edit or null if this entry is not being edited. */Editor currentEditor;/** The sequence number of the most recently committed edit to this entry. */long sequenceNumber;Entry(String key) {this.key = key;lengths = new long[valueCount];cleanFiles = new File[valueCount];dirtyFiles = new File[valueCount];// The names are repetitive so re-use the same builder to avoid allocations.StringBuilder fileBuilder = new StringBuilder(key).append('.');int truncateTo = fileBuilder.length();for (int i = 0; i < valueCount; i++) {fileBuilder.append(i);cleanFiles[i] = new File(directory, fileBuilder.toString());fileBuilder.append(".tmp");dirtyFiles[i] = new File(directory, fileBuilder.toString());fileBuilder.setLength(truncateTo);}}.../*** Returns a snapshot of this entry. This opens all streams eagerly to guarantee that we see a* single published snapshot. If we opened streams lazily then the streams could come from* different edits.*/Snapshot snapshot() {if (!Thread.holdsLock(DiskLruCache.this)) throw new AssertionError();Source[] sources = new Source[valueCount];long[] lengths = this.lengths.clone(); // Defensive copy since these can be zeroed out.try {for (int i = 0; i < valueCount; i++) {sources[i] = fileSystem.source(cleanFiles[i]);}return new Snapshot(key, sequenceNumber, sources, lengths);} catch (FileNotFoundException e) {// A file must have been deleted manually!for (int i = 0; i < valueCount; i++) {if (sources[i] != null) {Util.closeQuietly(sources[i]);} else {break;}}// Since the entry is no longer valid, remove it so the metadata is accurate (i.e. the cache// size.)try {removeEntry(this);} catch (IOException ignored) {}return null;}} ```} 一个Entry主要由以下几部分构成:key:每个cache都有一个key作为其标识符。当前cache的key为其对应URL的MD5字符串 cleanFiles/dirtyFiles:每一个Entry对应多个文件,其对应的文件数由DiskLruCache.valueCount指定。当前在OkHttp中valueCount为2。即每个cache对应2个cleanFiles,2个dirtyFiles。其中第一个cleanFiles/dirtyFiles记录cache的meta数据(如URL,创建时间,SSL握手记录等等),第二个文件记录cache的真正内容。cleanFiles记录处于稳定状态的cache结果,dirtyFiles记录处于创建或更新状态的cache currentEditor:entry编辑器,对entry的所有操作都是通过其编辑器完成。编辑器内部添加了同步锁 3.3 cleanupRunnable 清理线程,用于重建精简日志:private final Runnable cleanupRunnable = new Runnable() { public void run() { synchronized (DiskLruCache.this) { if (!initialized | closed) { return; // Nothing to do }```try {trimToSize();} catch (IOException ignored) {mostRecentTrimFailed = true;}try {if (journalRebuildRequired()) {rebuildJournal();redundantOpCount = 0;}} catch (IOException e) {mostRecentRebuildFailed = true;journalWriter = Okio.buffer(Okio.blackhole());}}} ```}; 其触发条件在journalRebuildRequired()方法中:/\*\** We only rebuild the journal when it will halve the size of the journal and eliminate at least* 2000 ops. \*/ boolean journalRebuildRequired() { final int redundantOpCompactThreshold = 2000; return redundantOpCount >= redundantOpCompactThreshold && redundantOpCount >= lruEntries.size(); } 当冗余日志超过日志文件本身的一般且总条数超过2000时执行3.4 SnapShot cache快照,记录了特定cache在某一个特定时刻的内容。每次向DiskLruCache请求时返回的都是目标cache的一个快照,相关逻辑在DiskLruCache.get中:\[DiskLruCache.java\] /\*\** Returns a snapshot of the entry named {@code key}, or null if it doesn’t exist is not currently* readable. If a value is returned, it is moved to the head of the LRU queue. \*/ public synchronized Snapshot get(String key) throws IOException { initialize();```checkNotClosed();validateKey(key);Entry entry = lruEntries.get(key);if (entry == null || !entry.readable) return null;Snapshot snapshot = entry.snapshot();if (snapshot == null) return null;redundantOpCount++;//日志记录journalWriter.writeUtf8(READ).writeByte(' ').writeUtf8(key).writeByte('\n');if (journalRebuildRequired()) {executor.execute(cleanupRunnable);}return snapshot; ```} 3.5 lruEntries 管理cache entry的容器,其数据结构是LinkedHashMap。通过LinkedHashMap本身的实现逻辑达到cache的LRU替换3.6 FileSystem 使用Okio对File的封装,简化了I/O操作。3.7 DiskLruCache.edit DiskLruCache可以看成是Cache在文件系统层的具体实现,所以其基本操作接口存在一一对应的关系:Cache.get() —>DiskLruCache.get() Cache.put()—>DiskLruCache.edit() //cache插入 Cache.remove()—>DiskLruCache.remove() Cache.update()—>DiskLruCache.edit()//cache更新 其中get操作在3.4已经介绍了,remove操作较为简单,put和update大致逻辑相似,因为篇幅限制,这里仅介绍Cache.put操作的逻辑,其他的操作大家看代码就好:\[okhttp3.Cache.java\] CacheRequest put(Response response) { String requestMethod = response.request().method();```if (HttpMethod.invalidatesCache(response.request().method())) {try {remove(response.request());} catch (IOException ignored) {// The cache cannot be written.}return null;}if (!requestMethod.equals("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 (HttpHeaders.hasVaryAll(response)) {return null;}Entry entry = new Entry(response);DiskLruCache.Editor editor = null;try {editor = cache.edit(key(response.request().url()));if (editor == null) {return null;}entry.writeTo(editor);return new CacheRequestImpl(editor);} catch (IOException e) {abortQuietly(editor);return null;} ```} 可以看到核心逻辑在editor = cache.edit(key(response.request().url()));,相关代码在DiskLruCache.edit:\[okhttp3.internal.cache.DiskLruCache.java\] synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { initialize();```checkNotClosed();validateKey(key);Entry entry = lruEntries.get(key);if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null|| entry.sequenceNumber != expectedSequenceNumber)) {return null; // Snapshot is stale.}if (entry != null && entry.currentEditor != null) {return null; // 当前cache entry正在被其他对象操作}if (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
最后
答应大伙的备战金三银四,大厂面试真题来啦!
这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
《960全网最全Android开发笔记》
《379页Android开发面试宝典》
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。
如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数
《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析
资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
rnal rebuild failed, the journal writer will not be active, meaning we will not be
最后
答应大伙的备战金三银四,大厂面试真题来啦!
这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》
《960全网最全Android开发笔记》
[外链图片转存中…(img-dFEM1yMr-1630937247575)]
《379页Android开发面试宝典》
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。
如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数
[外链图片转存中…(img-8bZ9AaYi-1630937247577)]
《507页Android开发相关源码解析》
只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。
真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。
[外链图片转存中…(img-PtK5Axnq-1630937247579)]
腾讯、字节跳动、阿里、百度等BAT大厂 2020-2021面试真题解析
[外链图片转存中…(img-VkHITIw1-1630937247581)]
资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!
OkHttp3源码详解(四)缓存策略,万分膜拜相关推荐
- OkHttp3源码详解
前言:为什么有些人宁愿吃生活的苦也不愿吃学习的苦,大概是因为懒惰吧,学习的苦是需要自己主动去吃的,而生活的苦,你躺着不动它就会来找你了. 一.概述 OKHttp是一个非常优秀的网络请求框架,已经被谷歌 ...
- Java源码详解四:String源码分析--openjdk java 11源码
文章目录 注释 类的继承 数据的存储 构造函数 charAt函数 equals函数 hashCode函数 indexOf函数 intern函数 本系列是Java详解,专栏地址:Java源码分析 Str ...
- OkHttp3源码详解(五) okhttp连接池复用机制
1.概述 提高网络性能优化,很重要的一点就是降低延迟和提升响应速度. 通常我们在浏览器中发起请求的时候header部分往往是这样的 keep-alive 就是浏览器和服务端之间保持长连接,这个连接是可 ...
- OkHttp3源码详解(三) 拦截器-RetryAndFollowUpInterceptor
最大恢复追逐次数: private static final int MAX_FOLLOW_UPS = 20; 处理的业务: 实例化StreamAllocation,初始化一个Socket连接对象,获 ...
- okhttp3 请求html页面,OkHttp3源码详解(二) 整体流程
1.简单使用 同步:@Override public Response execute() throws IOException { synchronized (this) { if (execute ...
- Tensorflow 2.x(keras)源码详解之第四章:DatasetTFRecord
大家好,我是爱编程的喵喵.双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中.从事机器学习以及相关的前后端开发工作.曾在阿里云.科大讯飞.CCF等比赛获得多次Top名次.现 ...
- 【Live555】live555源码详解(四):Medium媒体基础类
[Live555]live555源码详解系列笔记 7.Media Medai所依赖关系图 依赖Medai关系图 Media和UsageEnvironment关联图
- Rocksdb Compaction源码详解(二):Compaction 完整实现过程 概览
文章目录 1. 摘要 2. Compaction 概述 3. 实现 3.1 Prepare keys 过程 3.1.1 compaction触发的条件 3.1.2 compaction 的文件筛选过程 ...
- 源码详解Android 9.0(P) 系统启动流程之SystemServer
源码详解Android 9.0(P) 系统启动流程目录: 源码详解Android 9.0(P)系统启动流程之init进程(第一阶段) 源码详解Android 9.0(P)系统启动流程之init进程(第 ...
- Go 语言 bytes.Buffer 源码详解之1
转载地址:Go 语言 bytes.Buffer 源码详解之1 - lifelmy的博客 前言 前面一篇文章 Go语言 strings.Reader 源码详解,我们对 strings 包中的 Reade ...
最新文章
- 浅析高端网站建设策划方案都包括哪些内容?
- JQuery UI 1.8.13发布看看有哪些变动
- SAP CRM WebClient UI交互式报表的Gross Value工作原理
- 【Java】关键词assert的使用
- 吴恩达深度学习 —— 3.11 随机初始化
- 理发师睡觉问题、银行叫号问题详解 操作系统
- havok之shape
- SQL Server 2012笔记分享-10:理解数据压缩
- 弃用 Notepad++ 还有更牛逼的选择
- wifi抓包解读(实战教程)
- 行翻转和列翻转_用量子计算机翻转硬币
- 扫雷游戏网页版_世界排名前30,六成都是中国人:2020年,沉迷「扫雷」的玩家是怎样一群人?| 探寻游戏意义...
- linux下编译opendds,Linux下编译OpenDDS
- DBC文件解析及CAN通信矩阵
- 解决Mac电脑连不上wifi的问题
- [BJOI2019]勘破神机
- Unity家园系统---建筑交互
- W ndows 10模拟器,手机windows10模拟器下载_手机windows10模拟器安卓版下载中文 v0.20.0.3b-66街机网...
- [HDU1512]Monkey King(可并堆)
- nanotime java_Java System nanoTime()方法
热门文章
- 3.2、关于Support for password authentication was removed on August 13, 2021报错的解决方案
- 解决.bat文件一闪而过的方法
- android 实现果冻动画效果,HTML5/Canvas粘滑的果冻动画特效
- 小游戏制作-其他系列-数独
- idea中出现Authentication failed for的问题
- Discriminative Reasoning for Document-level Relation Extraction
- swap分区,lvm的管理及计划任务
- ggplot绘图之基本语法
- mysql 1556_mysqldump: Got error: 1556: You can't use locks with log tables. when doing LOCK TABLES
- 带省略号的比喻句_标点符号往往能引发人们的联想,例如:“省略号像一条漫长的人生道路,等着你去书写它留下的空白。”请以一种标点符号(省略号除外)为描述对象,写一个比喻句,形象地阐发某种生活道理。...