聊聊Glide中的LRU BitmapPool

973 阅读5分钟

1. 前言

今天的主题是“聊一聊Glide缓存的技术细节”。此文的成因是,前日有个同学发了段Glide的源码给我看,希望我帮他解答一下他的疑惑。

❝glide的LruBitmapPool是根据宽高和Bitmap.Config来查找缓存中的Bitmap的,如果两张图片的宽高和Bitmap.Config相同,会不会出现取错图片的情况。❞

2. 初探LruBitmapPool原理

答案当然是显而易见的。glide这个大名鼎鼎且务实的官方库,经过了这么多年的迭代,当然不会在获取缓存的时候取错图片了。但是这个问题却是个很好的问题,glide用宽高作为key的做法引起了我极大的好奇心。虽然我之前没看过glide源码,但是LruBitmapPool的Lru让我倍感亲切。看到Lru的第一反应是,LinkedHashMap嘛,心里旁白是"小样,这还不简单嘛?",于是定位到LruBitmapPool源码,Command+F快速的输入LinkedHashMap,按下回车键,信心满满的等待命中查询结果。结果却大吃一惊,用我女儿最近的口头禅就是,oops(哎哟),没有找到LinkedHashMap。这就更加激发了我的好奇心,跟着glide老大哥学习下不用LinkedHashMap是如何实现Lru缓存的。当然,首先并不着急看源码,第一步,需要确认glide源码中的LRU缓存和用LinkedHashMap实现的LRU是不是具有同样的功能,把它的注释看一遍。

❝An BitmapPool implementation that uses an LruPoolStrategy to bucket Bitmaps and then uses an LRU eviction policy to evict Bitmaps from the least recently used bucket in order to keep the pool below a given maximum size limit.❞

好消息,glide的LRU缓存策略和用LinkedHashMap实现的LRU缓存策略是一样的,都是把最近最少使用的缓存对象置换出去。翻译如下:

❝BitmapPool使用LruPoolStrategy来存储Bitmaps,然后使用LRU驱逐策略从最近使用最少的桶中删除Bitmaps,以保证缓存池大小不会超过给定的最大尺寸限制。❞

好吧,顺藤摸瓜,查找LruPoolStrategy。

哇,熟悉的配方,熟悉的味道。这不就是自己实现了Map那一套么,虽然它没使用LinkedHashMap,但是终归没有逃出Map的技术思想。

LruPoolStrategy有三个实现类,AttributeStrategy、SizeStrategy、SizeConfigStrategy。顾名思义,这里使用的是策略模式,要搞明白这类代码,先搞懂最简单的那个实现类,即AttributeStrategy。为什么说它最简单,看源码注释,它是根据宽高和Bitmap.Config做精准匹配的,根据Key去查询缓存,只能命中宽高和Config相等的,当然这是Android4.4之前的Bitmap复用机制,谁简单,谁能降低研究问题的复杂度就先研究谁。通读下源码,答案渐渐浮上水面了,有一个熟悉又陌生的东西,出现在眼前了,GroupedLinkedMap,说它熟悉,因为后面两个单词LinkedMap,顾名思义,虽然我不知道你具体是个啥,但你肯定是个Map或者类似Map的东西。说它陌生,因为GroupedLinkedMap我闻所未闻。欲知答案,老规矩,翻开源码,先看注释。

❝Similar to LinkedHashMap when access ordered except that it is access ordered on groups of bitmaps rather than individual objects. The idea is to be able to find the LRU bitmap size, rather than the LRU bitmap object. We can then remove bitmaps from the least recently used size of bitmap when we need to reduce our cache size.For the purposes of the LRU, we count gets for a particular size of bitmap as an access, even if no bitmaps of that size are present. We do not count addition or removal of bitmaps as an access.❞

注释释放出以下几个信息。

  1. 它与LinkedHashMap相似,但又不完全相同,有以下几个区别。
  2. GroundLinkedMap是从宽高和Bitmap.Config的维度,将缓存分组。具体意思就是,如果有多张100X100尺寸的图片,它们会保存到该尺寸对应的桶里面。而传统的LinkedHashMap,如果保存多个Key一样的对象,则会发生替换。
  3. LinkedHashMap get、put、remove操作都会让内部的数据重新排序,而GroundLinkedMap则只有get操作会让内部的数据发生排序,我的理解是,这样更合理,put操作,是图片不用了才放到缓存中,至于后续会不会被用到,什么时候用,都不确定,所以应该降低它的新鲜度。
  4. GroundLinkedMap对于没命中到的Key,也会认为是一次有效的access。该key会排序放到最前面。

上面的注释,可谓高屋建瓴,提纲挈领,它与LinkedHashMap作对比,让开发者明白了GroundLinkedMap是干什么的,同时又让大家明白,为什么会摒弃LinkedHashMap另外造一个轮子。GroundLinkedMap有两个属性,一个Map和一个双链表。Map是用来快速检索的,双链表是用来维护access顺序的,跟LinkedHashMap可谓异曲同工,区别上面也说了LinkedHashMap 的增、删、查操作,在命中缓存的情况下都会认为是一次有效access,会用双链表调整命中元素的顺序。而GroundLinkedMap则只针对查操作,不管命中与否,都会认为是一次有效access,会改变缓存的新鲜度。GroundLinkedMap的实现原理与LinkedHashMap实现原理极其相似,如果没看太懂,可以先自行搜索LinkedHashMap原理。值得注意的是,LinkedEntry中的values正是缓存分组的容器。

3. LinkedHashMap和GroundLinkedMap区别

为了验证它们两的区别,同时为了让读者更直观的感受它们之间的区别,我写了一个测试代码。首先在GroundLinkedMap增加了一个遍历打印的方法。然后写了一段测试代码,在get put remove 后,遍历两者的内容,观察它们顺序是否发生了变化。

结论:

  1. LinkedHashMap同一个key如果put多次,会发生替换而GroupedLinkedMap同一个key多次put,会分组。
  2. LinkedHashMap访问过的Entry会放到双链表的尾端而GroupedLinkedMap会将将访问过的Entry放到双链表的头部。(这只是区别,没有实际意义)
  3. 事实证明put操作不会影响GroupedLinkedMap内部双链表的数据顺序。看GroupedLinkedMap get(key2)和put(key3)之后的打印结果,顺序没变
  4. GroupedLinkedMap的get操作即使没有命中结果,也会把该key保存起来,并放到队首,看最后那一块的打印。