(转)OkHttp3源码分析[DiskLruCache]

原地址:https://blog.csdn.net/yangxi_pekin/article/details/73459961

OkHttp系列文章如下

本文目录:

  • Cache的简介
  • LinkedHashMap原理
  • OkHttp的文件系统

本文主要是对put/get过程进行分析,注意缓存的判断依据不是本文,而是缓存策略


1. Cache的简介

缓存,顾名思义,也就是方便用户快速的获取值的一种储存方式。小到与CPU同频的昂贵的缓存颗粒,内存,硬盘,网络,CDN反代缓存,DNS递归查询,OS页面置换,Redis数据库,都可以看作缓存。它有如下的特点:

  1. 缓存载体与持久载体总是相对的,容量远远小于持久容量,成本高于持久容量,速度高于持久容量。比如硬盘与网络,目前主流的SSD硬盘可以达到500MB/S,而很多地区网速却只有4M,将网络中的文件存到硬盘中,硬盘就相当于缓存;再比如内存与硬盘,主流的DDR3内存的速度可以达到10GB/S,而硬盘相对的慢了很多数量级别,将硬盘的游戏加载到内存,内存就相对于硬盘是一种缓存。
  2. 需要实现排序依据,在java中,可以使用Comparable<T>作为排序的的接口
  3. 需要一种页面置换算法(page replacement algorithm)将旧页面去掉换成新的页面,如最久未使用算法(LFU)、先进先出算法(FIFO)、最近最少使用算法(LFU)、非最近使用算法(NMRU)等
  4. 如果没有命中缓存,就需要从原始地址获取,这个步骤叫做“回源”,CDN厂商会标注“回源率”作为卖点

在OkHttp中,使用FileSystem作为缓存载体(磁盘相对于网络的缓存),使用LRU作为页面置换算法(封装了LinkedHashMap)。

  1. Comparable<T>是java用来排序的接口,推荐参考阅读《Java Software Structures Designing and Using Data Structures》
  2. 页面置换算法可以参考阅读《现代操作系统》的中译本

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元素后,调用下列回调函数对链表将元素移动到链尾以及清理旧的元素

 
  1. // move node to last

  2. void afterNodeAccess(Node<K,V> e)

  3. // possibly remove eldest

  4. void afterNodeInsertion(boolean evict)

在get元素时,如果设置accessOrder为true时,通过调用如下回调移动元素到链尾,这里特别强调移动,如果这个元素本身已经在链表中,那它将只会移动,而不是新建

 
  1. // move node to last

  2. void afterNodeAccess(Node<K,V> e)

综上,当你反复对元素进行get/put操作时,经常使用的元素会被移动到tail中,而长期不用的元素会被移动到head

最后迭代(Iterator)时,迭代是从旧元素迭代到新元素,这就是LRU的实现

 
  1. head <--> .... <--> tail

  2.  
  3. 旧元素 <-----------> 反复使用的新元素

在OkHttp中,使用了DiskLruCacheLinkedHashMap进行了封装实现LRU,按照下图的方法进行初始化

 
  1. //按照访问顺序排序的Map,设置accessOrder为true

  2. 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>

上述具体代码没有源码分析哦,王垠大神看了都会头大

  1. 需要复习HashMap源码?可以考虑阅读HashMap原理文章
  2. 本部分基于JDK1.8.0_05,可能部分函数与网上文章相冲突
  3. 在golang中,使用ringmap实现了Lru,可以看这里

3. OkHttp的文件系统

OkHttp中的关键对象如下:

  • FileSystem: 使用Okio对File的封装,简化了IO操作
  • DiskLruCache.Editor: 添加了同步锁,并对FileSystem进行高度封装
  • DiskLruCache.Entry: 维护着key对应的多个文件
  • Cache.Entry: Responsejava对象与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}},后面的01,分别表示ENTRY_METADATAENTRY_BODY

比如在缓存的路径下执行ls,结果如下

 
  1. $ ls

  2. 5716ab0f06c49bc7cf602397c51d5677.0

  3. 5716ab0f06c49bc7cf602397c51d5677.1

  4. 5b2f52377611dc6201a1871bdb997466.0

  5. 5b2f52377611dc6201a1871bdb997466.1

  6. journal

  7. .....

DiskLruCache.Editor对工具类FileSystem进行进一步的封装,它以DiskLruCache.Entry作为构造参数,通过操控Entry中维护的数组,对外暴露source/sink,为上层的java对象与文件的转换提供基于okio的流操作,我们可以通过对它的两个方法进行FindUsage查询获得OkHttp关于文件读写的全部场景

  • 写入场景:第一个位置是写入元信息,也就是写入末位是0的文件中,是序列化的过程;第二个位置是写入body,也就是写入末位是1的文件中,是存二进制的过程;

    OkHttp Sink to file

  • 读取场景:读取时,需要获取快照,通过调用链分析如下

    okhttp snapshot source

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退出时,线程自动结束。

 
  1. new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,

  2. new LinkedBlockingQueue<Runnable>(), Util.threadFactory("OkHttp DiskLruCache", true))

当需要清理时,执行清理任务,它将在每次get/set后调用

 
  1. private final Runnable cleanupRunnable = new Runnable() {

  2. public void run() {

  3. synchronized (DiskLruCache.this) {

  4. if (!initialized | closed) {

  5. return; // Nothing to do

  6. }

  7. try {

  8. //遍历LRU缓存(从旧到新进行遍历map),并删除文件

  9. //直到小于MaxSize为止

  10. trimToSize();

  11. if (journalRebuildRequired()) {

  12. rebuildJournal();

  13. redundantOpCount = 0;

  14. }

  15. } catch (IOException e) {

  16. throw new RuntimeException(e);

  17. }

  18. }

  19. }

  20. };

总结

  1. OkHttp通过对文件进行了多次封装,实现了非常简单的I/O操作
  2. OkHttp通过对请求url进行md5实现了与文件的映射,实现写入,删除等操作
  3. OkHttp内部维护着清理线程池,实现对缓存文件的自动清理

Refference

  1. https://zh.wikipedia.org/wiki/缓存
  2. https://en.wikipedia.org/wiki/Page_replacement_algorithm#Least_recently_used
  3. http://stackoverflow.com/questions/2889777/difference-between-hashmap-linkedhashmap-and-treemap

猜你喜欢

转载自blog.csdn.net/duyiqun/article/details/81275093