OkHttp系列文章如下
本文目录:
- Cache的简介
- LinkedHashMap原理
- OkHttp的文件系统
本文主要是对put/get过程进行分析,注意缓存的判断依据不是本文,而是缓存策略
1. Cache的简介
缓存,顾名思义,也就是方便用户快速的获取值的一种储存方式。小到与CPU同频的昂贵的缓存颗粒,内存,硬盘,网络,CDN反代缓存,DNS递归查询,OS页面置换,Redis数据库,都可以看作缓存。它有如下的特点:
- 缓存载体与持久载体总是相对的,容量远远小于持久容量,成本高于持久容量,速度高于持久容量。比如硬盘与网络,目前主流的SSD硬盘可以达到500MB/S,而很多地区网速却只有4M,将网络中的文件存到硬盘中,硬盘就相当于缓存;再比如内存与硬盘,主流的DDR3内存的速度可以达到10GB/S,而硬盘相对的慢了很多数量级别,将硬盘的游戏加载到内存,内存就相对于硬盘是一种缓存。
- 需要实现
排序依据
,在java中,可以使用Comparable<T>
作为排序的的接口 - 需要一种
页面置换算法(page replacement algorithm)
将旧页面去掉换成新的页面,如最久未使用算法(LFU)、先进先出算法(FIFO)、最近最少使用算法(LFU)、非最近使用算法(NMRU)等 - 如果没有命中缓存,就需要从原始地址获取,这个步骤叫做“回源”,CDN厂商会标注“回源率”作为卖点
在OkHttp中,使用FileSystem
作为缓存载体(磁盘相对于网络的缓存),使用LRU作为页面置换算法(封装了LinkedHashMap)。
Comparable<T>
是java用来排序的接口,推荐参考阅读《Java Software Structures Designing and Using Data Structures》- 页面置换算法可以参考阅读《现代操作系统》的中译本
2. LinkedHashMap原理
2.1. 源码概述分析
在学习之前,我们要了解一下LinkedHashMap。LinkedHashMap继承于HashMap。
在HashMap中,维护了一个Node<K,V>[] table
,当put操作时,将元素按照计算出的Hash填到数组相应位置table[Hash]
中,最后迭代时,从table[0]开始向后迭代,具体的顺序取决于元素的HashCode,所以我们常说HashMap的元素迭代是不可预测的。
而在LinkedHashMap中,除了Node<K,V>[] table
,还维护着Entry<K,V> head,tail
。当put元素后,调用下列回调函数对链表将元素移动到链尾以及清理旧的元素
// move node to last
void afterNodeAccess(Node<K,V> e)
// possibly remove eldest
void afterNodeInsertion(boolean evict)
在get元素时,如果设置accessOrder
为true时,通过调用如下回调移动元素到链尾,这里特别强调移动,如果这个元素本身已经在链表中,那它将只会移动,而不是新建
// move node to last
void afterNodeAccess(Node<K,V> e)
综上,当你反复对元素进行get/put操作时,经常使用的元素会被移动到tail
中,而长期不用的元素会被移动到head
最后迭代(Iterator)时,迭代是从旧元素迭代到新元素,这就是LRU的实现
head <--> .... <--> tail
旧元素 <-----------> 反复使用的新元素
在OkHttp中,使用了DiskLruCache
对LinkedHashMap
进行了封装实现LRU,按照下图的方法进行初始化
//按照访问顺序排序的Map,设置accessOrder为true
map = new LinkedHashMap<>(0, 0.75f, true);
2.2. HashMap的对比
以下是常见的3种map的区别,以下均不计算扩容时的时间复杂度
HashMap | LinkedHashMap | TreeMap | |
---|---|---|---|
Performance get/set | O(1) | O(1) | O(logN) |
Implement | Array | Link + Array | Red-Black Tree |
Iteration | unpredictable | put/accessOrder | Comparable<Key> |
上述具体代码没有源码分析哦,王垠大神看了都会头大
- 需要复习HashMap源码?可以考虑阅读HashMap原理文章
- 本部分基于JDK1.8.0_05,可能部分函数与网上文章相冲突
- 在golang中,使用
ring
与map
实现了Lru,可以看这里
3. OkHttp的文件系统
OkHttp中的关键对象如下:
- FileSystem: 使用Okio对
File
的封装,简化了IO操作 - DiskLruCache.Editor: 添加了同步锁,并对FileSystem进行高度封装
- DiskLruCache.Entry: 维护着key对应的多个文件
- Cache.Entry:
Response
java对象与Okio流
的序列化/反序列化类 - DiskLruCache: 维护着文件的创建,清理,读取。内部有清理线程池,LinkedHashMap(也就是LruCache)
- Cache: 被上级代码调用,提供透明的put/get操作,封装了缓存检查条件与
DiskLruCache
,开发者只用配置大小即可,不需要手动管理 - Response/Requset: OkHttp的请求与回应
3.1. 文件初级封装(FileSystem)
众所周之,文件读写是流操作,是一大堆的令人头痛的try/cache操作,在OkHttp中设计了FileSystem.SYSTEM作为文件层的管理。通过用Okio库中的Source/Sink
对File进行包装,而不用更为头痛的InputStream这类东西,使上层调用与管道操作一样简单。
File(低级操作,步骤繁琐) -> Okio(封装) -> FileSystem(友好工具类)
至于Okio为何这个好,直接去官网参考
3.2. 文件高级封装(DiskLruCache.Entry/Editor/Snapshot)
本部分进行了如下的转换,进行了实际的put/get操作
FileSystem <-- DiskLruCache.Entry/Editor --> source/sink(更少参数)
DiskLruCache.Entry针对每个请求的url对应的文件进行引用维护(而没有进行创建/读取等操作),它内部维护了2个File
数组,一般来说每个url对应2~4个文件。 文件名命名规则是{md5(url)+ {0,1}},后面的0
或1
,分别表示ENTRY_METADATA与ENTRY_BODY。
比如在缓存的路径下执行ls
,结果如下
$ ls
5716ab0f06c49bc7cf602397c51d5677.0
5716ab0f06c49bc7cf602397c51d5677.1
5b2f52377611dc6201a1871bdb997466.0
5b2f52377611dc6201a1871bdb997466.1
journal
.....
DiskLruCache.Editor对工具类FileSystem
进行进一步的封装,它以DiskLruCache.Entry
作为构造参数,通过操控Entry
中维护的数组,对外暴露source/sink,为上层的java对象与文件的转换提供基于okio的流操作,我们可以通过对它的两个方法进行FindUsage查询获得OkHttp关于文件读写的全部场景
- 写入场景:第一个位置是写入元信息,也就是写入末位是0的文件中,是序列化的过程;第二个位置是写入body,也就是写入末位是1的文件中,是存二进制的过程;
- 读取场景:读取时,需要获取快照,通过调用链分析如下
3.3. 序列化与反序列化(Cache.Entry)
文件存储本质上也是序列化与反序列化的过程。本部分提供了下图的转变
Resonse(java对象) <--- Cache.Entry ---> source/sink(文件io)
代码部分不复杂,与上面的findusage位置相同,可以概括下:
如果信息本身就是二进制,就直接写到文件中;如果是文本信息,按照预设的格式写入即可。
至于序列化后的东西到底是什么,可以直接在shell下运行
cat
命令或者打开文本编辑器进行输出查看。注意这里的
Cache.Entry
与上面的DiskLruCache.Entry
是两个完全不同的对象
3.4 缓存的自动清理
在DiskLruCache初始化时,将建立线程池,最少零个线程,最大一个线程,线程空闲可以活60s,线程名叫做"OkHttp DiskLruCache",当JVM退出时,线程自动结束。
new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true))
当需要清理时,执行清理任务,它将在每次get/set后调用
private final Runnable cleanupRunnable = new Runnable() {
public void run() {
synchronized (DiskLruCache.this) {
if (!initialized | closed) {
return; // Nothing to do
}
try {
//遍历LRU缓存(从旧到新进行遍历map),并删除文件
//直到小于MaxSize为止
trimToSize();
if (journalRebuildRequired()) {
rebuildJournal();
redundantOpCount = 0;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
};
总结
- OkHttp通过对文件进行了多次封装,实现了非常简单的I/O操作
- OkHttp通过对请求url进行md5实现了与文件的映射,实现写入,删除等操作
- OkHttp内部维护着清理线程池,实现对缓存文件的自动清理