GroupedLinkedMap 是 Glide 为实现其 BitmapPool 中 LRU(最近最少使用)逻辑而专门设计的高效双层哈希链表数据结构。它巧妙地解决了两个核心问题:按Bitmap规格快速分组查找 与 组内高效的LRU管理。
🧱 核心结构设计
GroupedLinkedMap 的内部结构是一个经典的 “哈希表+双链表” 组合:
// 概念结构示意(非实际代码)
public class GroupedLinkedMap<K, V> {
// 第一层:哈希映射,用于快速查找某个规格对应的Bitmap链表
private final Map<K, LinkedEntry<K, V>> keyToEntry = new HashMap<>();
// 第二层:双链表,用于维护所有组的全局LRU顺序
private final LinkedEntry<K, V> head = new LinkedEntry<>();
}
keyToEntry(HashMap):作为索引层。键(Key)是SizeConfigStrategy为Bitmap规格(大小、格式)生成的标识符(例如“4096x4096_ARGB_8888”),值(Value)是一个LinkedEntry节点,代表一组相同规格的Bitmap集合。head(双链表头节点):作为顺序层。所有LinkedEntry节点通过prev和next指针,串联成一个按访问时间排序的双向循环链表。
🔄 工作流程:存取如何发生
理解它如何运作的最佳方式是跟踪一次完整的Bitmap存取过程。
1. 存入一个Bitmap(put 方法)
假设我们要将一个 100x100 的 ARGB_8888 Bitmap 放入池中。
- 步骤A:生成Key。
SizeConfigStrategy会为这个Bitmap计算出其对应的Key(例如,规格标识)。 - 步骤B:查找或创建组。
GroupedLinkedMap用这个Key去keyToEntry哈希表中查找。如果找到,则定位到对应的LinkedEntry节点;如果没找到,则新建一个LinkedEntry节点,并放入哈希表。 - 步骤C:插入并更新顺序。将这个Bitmap添加到对应
LinkedEntry的内部列表(List<V>)的末尾。更重要的是,在双链表中,将这个LinkedEntry节点移动到整个链表的尾部(head.prev)。链表尾部代表最近被使用过。 - 总结:
put操作的核心是 “定位到组,数据入组,组节点提到最新”。
2. 取出一个Bitmap(get 方法)
当需要一个 100x100 的 ARGB_8888 Bitmap 时:
- 步骤A:生成Key。用同样的逻辑生成请求的Key。
- 步骤B:查找组。用Key在
keyToEntry中快速找到对应的LinkedEntry节点。 - 步骤C:从组内获取并更新顺序。从该节点内部列表的末尾(
List.size()-1)取出最后一个Bitmap(后进先出,LIFO,效率高)。同样,将这个LinkedEntry节点移动到双链表尾部,标记为最近使用。 - 步骤D:清理空组。如果这个
LinkedEntry节点内部的列表被取空了,就将这个节点从双链表和哈希表中都移除。 - 总结:
get操作的核心是 “快速定位组,组内取最新,提组到最新,空组则清理”。
🎯 设计精髓与优势
这种设计有几个精妙之处,恰好满足了 BitmapPool 的需求:
- 高效的LRU驱逐:当
BitmapPool需要释放内存时,LruBitmapPool会从GroupedLinkedMap的双链表头部(head.next) 开始移除LinkedEntry节点及其内部的Bitmap。链表头部对应的就是最久未被访问的规格组,这完美实现了LRU策略。 - 精确的分组管理:通过哈希表按规格分组,避免了遍历所有Bitmap来寻找匹配项,查找和插入的平均时间复杂度接近 O(1)。
- 内存友好的操作:组内使用
ArrayList存储,取用末尾元素无需移动数据;只有在组变空时才会移除节点,减少了频繁的哈希表与链表操作。
简单来说,你可以把 GroupedLinkedMap 想象成一个多层的储物柜。第一层(哈希表) 告诉你“哪种大小的衣服放哪个区域”,第二层(双链表) 记录了“哪个区域的衣服最久没被拿过”。当你需要找一件衣服时,能快速定位到区域(Key),并从该区域最新放入的那堆里拿一件(LIFO),同时把这个区域标记为“刚用过”。当储物柜满了要清空间时,就去找那个“最久没被用过的区域”整个清掉。
希望这个解释能帮你清晰地理解 GroupedLinkedMap 的巧妙之处。如果你想进一步探讨 SizeConfigStrategy 如何生成Key,或者 LruBitmapPool 如何与它协同完成整个内存管理,我可以继续为你展开。