Glide 源码分析解读-缓存模块-基于最新版Glide 4.9.0

缓存模块


我在分析 Glide 源码前将 Glide 的项目 clone 到了本地,阅读时添加了很多注释以及自己的理解等等,现在已经推到了 Github 上,有兴趣的同学可以看看:
https://github.com/0xZhangKe/Glide-note

缓存模块涉及到的东西比较多,比较重要,所以需要单独用一章节来讲。

关于缓存的获取、数据加载相关的逻辑在 Engine#load 方法中。
先来看看缓存流程,流程如下图:

cache_process

全部的缓存流程大致如上图所示。

Glide 实例化时会实例化三个缓存相关的类以及一个计算缓存大小的类:

//根据当前机器参数计算需要设置的缓存大小
MemorySizeCalculator calculator = new MemorySizeCalculator(context);
//创建 Bitmap 池
if (bitmapPool == null) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
        int size = calculator.getBitmapPoolSize();
        bitmapPool = new LruBitmapPool(size);
    } else {
        bitmapPool = new BitmapPoolAdapter();
    }
}
//创建内存缓存
if (memoryCache == null) {
    memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
}
//创建磁盘缓存
if (diskCacheFactory == null) {
    diskCacheFactory = new InternalCacheDiskCacheFactory(context);
}

除此之外 Engine 中还有一个 ActiveResources 作为第一级缓存。下面分别来介绍一下。

ActiveResources


ActiveResources 是第一级缓存,表示当前正在活动中的资源。
类路径:

com.bumptech.glide.load.engine.ActiveResources

Engine#load 方法中构建好 Key 之后第一件事就是去这个缓存中获取资源,获取到则直接返回,获取不到才继续从其他缓存中寻找。

当资源加载成功,或者通过缓存中命中资源后都会将其放入 ActivityResources 中,资源被释放时移除出 ActivityResources 。

由于其中的生命周期较短,所以没有大小限制

ActiveResources 中通过一个 Map 来存储数据,数据保存在一个虚引用(WeakReference)中。

刚刚说的 activeResource 使用一个 Map<Key, WeakReference<EngineResource<?>>> 来存储的,此外还有一个引用队列:

ReferenceQueue<EngineResource<?>> resourceReferenceQueue;

每当向 activeResource 中添加一个 WeakReference 对象时都会将 resourceReferenceQueue 和这个 WeakReference 关联起来,用来跟踪这个 WeakReference 的 gc,一旦这个弱引用被 gc 掉,就会将它从 activeResource 中移除,ReferenceQueue 的具体作用可以自行谷歌,大概就是用来跟踪弱引用(或者软引用、虚引用)是否被 gc 的。

那么 ReferenceQueue 具体是在何时去判断 WeakReference 是否被 gc 了呢,Handler 机制大家应该都知道,但不知道大家有没有用过 MessageQueue.IdleHandler 这个东东,可以调用 MessageQueue#addIdleHandler 添加一个 MessageQueue.IdleHandler 对象,Handler 会在线程空闲时调用这个方法。resourceReferenceQueue 在创建时会创建一个 Engine#RefQueueIdleHandler 对象并将其添加到当前线程的 MessageQueue 中,ReferenceQueue 会在 IdleHandler 回调的方法中去判断 activeResource 中的 WeakReference 是不是被 gc 了,如果是,则将引用从 activeResource 中移除,代码如下:

//MessageQueue 中的消息暂时处理完回调
@Override
public boolean queueIdle() {
    ResourceWeakReference ref = (ResourceWeakReference) queue.poll();
    if (ref != null) {
        activeResources.remove(ref.key);
    }
    //返回 true,表示下次处理完仍然继续回调
    return true;
}

MemorySizeCalculator


这个类是用来计算 BitmapPool 、ArrayPool 以及 MemoryCache 大小的。
计算方式如下:

//默认为 4MB,如果是低内存设备则在此基础上除以二
arrayPoolSize =
        isLowMemoryDevice(builder.activityManager)
                ? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR
                : builder.arrayPoolSizeBytes;
//其中会先获取当前进程可使用内存大小,
//然后通过判断是否是否为低内存设备乘以相应的系数,
//普通设备是乘以 0.4,低内存为 0.33,这样得到的是 Glide 可使用的最大内存阈值 maxSize
int maxSize =
        getMaxSize(
                builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);
​
int widthPixels = builder.screenDimensions.getWidthPixels();
int heightPixels = builder.screenDimensions.getHeightPixels();
//计算一张格式为 ARGB_8888 ,大小为屏幕大小的图片的占用内存大小
//BYTES_PER_ARGB_8888_PIXEL 值为 4
int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
​
int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);
​
int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);
//去掉 ArrayPool 占用的内存后还剩余的内存
int availableSize = maxSize - arrayPoolSize;
​
if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) {
    //未超出内存限制
    memoryCacheSize = targetMemoryCacheSize;
    bitmapPoolSize = targetBitmapPoolSize;
} else {
    //超出内存限制
    float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens);
    memoryCacheSize = Math.round(part * builder.memoryCacheScreens);
    bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens);
}

直接看上面的注释即可。

BitmapPool


Bitmap 是用来复用 Bitmap 从而避免重复创建 Bitmap 而带来的内存浪费,Glide 通过 SDK 版本不同创建不同的 BitmapPool 实例,版本低于 Build.VERSION_CODES.HONEYCOMB(11) 实例为 BitmapPoolAdapter,其中的方法体几乎都是空的,也就是是个实例不做任何缓存。
否则实例为 LruBitmapPool,先来看这个类。

LruBitmapPool


LruBitmapPool 中没有做太多的事,主要任务都交给了 LruPoolStrategy,这里只是做一些缓存大小管理、封装、日志记录等等操作。

每次调用 put 缓存数据时都会调用 trimToSize 方法判断已缓存内容是否大于设定的最大内存,如果大于则使用 LruPoolStrategy#removeLast 方法逐步移除,直到内存小于设定的最大内存为止。

LruPoolStrategy 有两个实现类:SizeConfigStrategy 以及 AttributeStrategy,根据系统版本创建不同的实例,这两个差异不大,KITKAT 之后使用的都是 SizeConfigStrategy,这个比较重要。

SizeConfigStrategy


SizeConfigStrategy 顾名思义,是通过 Bitmap 的 size 与 Config 来当做 key 缓存 Bitmap,Key 也会通过 KeyPool 来缓存在一个队列(Queue)中。

与 AttributeStrategy 相同的是,其中都使用 Glide 内部自定义的数据结构:GroupedLinkedMap 来存储 Bitmap。

当调用 put 方法缓存一个 Bitmap 时会先通过 Bitmap 的大小以及 Bitmap.Config 创建(从 KeyPool 中获取)Key,然后将这个 Key 与 Bitmap 按照键值对的方式存入 GroupedLinkedMap 中。

此外其中还包含一个 sortedSizes,这是一个 HashMap,Key 对应 put 进来的 Bitmap.Config,value 对应一个 TreeMap,TreeMap 中记录着每一个 size 的 Bitmap 在当前缓存中的个数,即 put 时加一,get 时减一。

TreeMap 是有序的数据结构,当需要通过 Bitmap 的 size 与 Config 从缓存中获取一个 Biamp 时未必会一定要获取到 size 完全相同的 Bitmap,由于 TreeMap 的特性,调用其 ceilingKey 可以获取到一个相等或大于当前 size 的一个最小值,用这个 Key 去获取 Bitmap,然后重置一下大小即可。

重点看一下 GroupedLinkedMap,这是 Glide 为了 实现 LRU 算法自定义的一个数据结构,看名字是已分组的链表 Map?看一下下面的图就明白了:

GroupedLinkedMap

其中包含三种数据结构:哈希表(HashMap)、循环链表以及列表(ArrayList)。
这个结构其实类似 Java 里提供的 LinkedHashMap 类。

循环链表是通过内部类 GroupedLinkedMap$LinkedEntry 实现的,其中除了定义了链表结构需要的上下两个节点信息之外还包含着一个 Key 与一个 Values,定义如下:

private static class LinkedEntry<K, V> {
    private final K key;
    private List<V> values;
    LinkedEntry<K, V> next;
    LinkedEntry<K, V> prev;
    
    ...
}

其实就是将 HashMap 的 Values 使用链表串了起来,每个 Value 中又存了个 List

调用 put 方法时会先根据 Key 去这个 Map 中获取 LinkedEntry,获取不到则创建一个,并且加入到链表的尾部,然后将 value (也就是 Bitmap)存入 LinkedEntry 中的 List 中。

所以这里说的分组指的是通过 Key 来对 Bitmap 进行分组,对于同一个 Key(size 与 config 都相同)的 Bitmap 都会存入同一个 LinkedEntry 中。

调用 get 方法获取 Bitmap 时会先通过 Key 去 keyToEntry 中获取 LinkedEntry 对象,获取不到则创建一个,然后将其加入到链表头部,此时已经有了 LinkedEntry 对象,调用 LinkedEntry#removeLast 方法返回并删除 List 中的最后一个元素。

通过上面两步可以看到之所以使用链表是为了支持 LRU 算法,最近使用的 Bitmap 都会移动到链表的前端,使用次数越少就越靠后,当调用 removeLast 方法时就直接调用链表最后一个元素的 removeLast 方法移除元素。

好了 BitmapPool 大概就这么多内容,总结一下:

  1. BitmapPool 大小通过 MemorySizeCalculator 设置;
  2. 使用 LRU 算法维护 BitmapPool ;
  3. Glide 会根据 Bitmap 的大小与 Config 生成一个 Key;
  4. Key 也有自己对应的对象池,使用 Queue 实现;
  5. 数据最终存储在 GroupedLinkedMap 中;
  6. GroupedLinkedMap 使用哈希表、循环链表、List 来存储数据。

MemoryCache


相比较而言内存缓存就简单多了,如果从上面说的 ActiveResources 中没获取到资源则开始从这里寻找。
内存缓存同样使用 LRU 算法,实现类为 LruResourceCache,这个类没几行代码,继承了 LruCache ,所以着重看一下 LruCache 好了。

其实 Java 集合里面提供了一个很好的用来实现 LRU 算法的数据结构,即上面提到过的 LinkedHashMap。其基于 HashMap 实现,同时又将 HashMap 中的 Entity 串成了一个双向链表。
LruCache 中就是使用这个集合来缓存数据,其中代码量也不多,主要就是在 LinkedHashMap 的基础上又提供了对内存的管理的几个操作。

特别地,LruResourceCache 中提供了一个 ResourceRemovedListener 接口,当有资源从 MemoryCache 中被移除时会回调其中的方法,Engine 中接收到这个消息后就会进行 Bitmap 的回收操作。

磁盘缓存


缓存路径默认为 Context#getCacheDir() 下面的 image_manager_disk_cache 文件夹,默认缓存大小为 250MB。

磁盘缓存实现类由 InternalCacheDiskCacheFactory 创建,最终会通过缓存路径及缓存文件夹最大值创建一个 DiskLruCacheWrapper 对象。

DiskLruCacheWrapper 实现了 DiskCache 接口,接口主要的代码如下:

File get(Key key);
void put(Key key, Writer writer);
void delete(Key key);
void clear();

可以看到其中提供了作为一个缓存类必须的几个方法,并且文件以 Key 的形式操作。

SafeKeyGenerator 类用来将 Key 对象转换为字符串,Key 不同的实现类生成 Key 的方式也不同,一般来说会通过图片宽高、加密解码器、引擎等等生成一个 byte[] 然后再转为字符串,以此来保证图片资源的唯一性

另外,在向磁盘写入文件时(put 方法)会使用重入锁来同步代码,也就是 DiskCacheWriteLocker 类,其中主要是对 ReentrantLock 的包装。

DiskLruCacheWrapper 顾名思义也是一个包装类,包装的是 DiskLruCache,那再来看看这个类。

DiskLruCache


这里考虑一个问题,磁盘缓存同样使用的是 LRU 算法,但文件是存在磁盘中的,如何在 APP 启动之后准确的按照使用次数排序读取缓存文件呢?

Glide 是使用一个日志清单文件来保存这种顺序,DiskLruCache 在 APP 第一次安装时会在缓存文件夹下创建一个 journal 日志文件来记录图片的添加、删除、读取等等操作,后面每次打开 APP 都会读取这个文件,把其中记录下来的缓存文件名读取到 LinkedHashMap 中,后面每次对图片的操作不仅是操作这个 LinkedHashMap 还要记录在 journal 文件中.
journal 文件内容如下图:

journal 文件内容

开头的 libcore.io.DiskLruCache 是魔数,用来标识文件,后面的三个 1 是版本号 valueCount 等等,再往下就是图片的操作日志了。

DIRTY、CLEAN 代表操作类型,除了这两个还有 REMOVE 以及 READ,紧接着的一长串字符串是文件的 Key,由上文提到的 SafeKeyGenerator 类生成,是由图片的宽、高、加密解码器等等生成的 SHA-256 散列码 后面的数字是图片大小。

根据这个字符串就可以在同目录下找到对应的图片缓存文件,那么打开缓存文件夹即可看到上面日志中记录的文件:


缓存文件列表

可以看到日志文件中记录的缓存文件就在这个文件夹下面。

由于涉及到磁盘缓存的外部排序问题,所以相对而言磁盘缓存比较复杂。

那么 Glide 的缓存模块至此就结束了,主要是 BitmapPool 中的数据结构以及磁盘缓存比较复杂,其他的倒也不是很复杂。

关于 Glide 其它模块的源码解析可以看我的上一篇博客
https://www.jianshu.com/p/9bb50924d42a

另外,我在分析 Glide 源码前将 Glide 的项目 clone 到了本地,阅读时添加了很多注释以及自己的理解等等,现在已经推到了 Github 上,有兴趣的同学可以看看:
https://github.com/0xZhangKe/Glide-note

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343