Glide 浅析 —— 缓存机制

645 阅读9分钟

Glide 浅析 —— 源码流程

上一篇博客介绍了 Glide 的整体流程,但是对于 Glide 缓存机制的细节没有进行说明,将在这篇博客中进行介绍

Glide 缓存机制是面试的高频考点,理解 Glide 缓存机制对于我们应该如何构建一个图片加载框架有着极大的帮助

Glide 缓存中分为几种类型?

Glide 中分为三种缓存:

  • 活动缓存(运行时内存),缓存 仍被界面所使用 的图片资源
  • 内存缓存(运行时内存),缓存 已不被界面所使用却仍存在于内存中 的图片资源
  • 磁盘缓存(文件存储),缓存 根据策略写入磁盘 的图片资源

活动缓存

Engine.load() 函数中,会调用 Engine.loadFromMemory() 函数根据 Key 值去缓存中查找资源

@Nullable
// Engine.java
private EngineResource<?> loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime
){
    ......
    // 根据 Key 值从活动缓存中寻找资源
    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null)
    {
        ......
        return active;
    }
    ......
    return null;
}

@Nullable
private EngineResource<?> loadFromActiveResources(Key key)
{
    EngineResource<?> active = activeResources.get(key);
    ......
    return active;
}

// ActiveResources.java

final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();

@Nullable
synchronized EngineResource<?> get(Key key)
{
    // 根据 Key 从 Map 集合中查找资源
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    ......
    EngineResource<?> active = activeRef.get();
    ......
    return active;
}

看到这应该很清晰了,活动缓存其实就是一个 Map 集合

唯一需要注意的是活动缓存中的 Map 集合对于 Value 是弱引用

因为活动缓存中的资源是直接与 UI 界面绑定的资源,所有在 UI 界面上需要使用的资源都会储存在活动缓存中

使用弱引用可以使得资源跟随 UI 界面的生命周期,当 UI 界面不再使用资源时,这些资源就会被 GC 所回收,不会造成 内存泄漏

内存缓存

我们回到 Engine.loadFromMemory() 函数

// Engine.java
private EngineResource<?> loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime
){
    ......
    // 根据 Key 值从内存缓存中寻找资源
    EngineResource<?> cached = loadFromCache(key);
    if (cached != null)
    {
        ......
        return cached;
    }

    return null;
}

private EngineResource<?> loadFromCache(Key key)
{
    EngineResource<?> cached = getEngineResourceFromCache(key);
    ......
    return cached;
}

private EngineResource<?> getEngineResourceFromCache(Key key)
{
    // cache = LruResourceCache
    Resource<?> cached = cache.remove(key);
    ......
    return result;
}

// LruCache.java(LruResourceCache 继承自 LruCache)

private final Map<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);

@Nullable
public synchronized Y remove(@NonNull T key)
{
    final Y value = cache.remove(key);
    if (value != null)
    {
        currentSize -= getSize(value);
    }
    return value;
}

我们可以看到内存缓存其实也是一个 Map 集合,但是与活动缓存相比,内存缓存似乎有些不同

内存缓存中实现的 Map 集合是一个 LinkedHashMap,并且在 LinkedHashMap 构造函数的第三个参数传入了 true,表示开启了 LinkedHashMap 中的最近最少使用(Lru)算法

最近最少使用(Lru)算法

再好的文字说明也不如运行代码出结果来得清晰,我们可以通过示例代码来理解 Lru 算法

public static void main(String[] args)
{
    Map<String, Integer> cache = new LinkedHashMap<>(100, 0.75f, true);

    cache.put("第一个元素", 1);
    cache.put("第二个元素", 2);
    cache.put("第三个元素", 3);
    cache.put("第四个元素", 4);
    cache.put("第五个元素", 5);

    for (Map.Entry<String, Integer> entry : cache.entrySet())
    {
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}

这段代码的运行结果是:

结果1.png

现在我们来修改一下这段代码

public static void main(String[] args)
{
    Map<String, Integer> cache = new LinkedHashMap<>(100, 0.75f, true);

    cache.put("第一个元素", 1);
    cache.put("第二个元素", 2);
    cache.put("第三个元素", 3);
    cache.put("第四个元素", 4);
    cache.put("第五个元素", 5);
    
    cache.get("第三个元素");

    for (Map.Entry<String, Integer> entry : cache.entrySet())
    {
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}

这段代码的运行结果是:

结果2.png

可以看到,第三个元素的位置发生了变化,这就是最近最少使用原则的体现

可以理解为最近最少使用算法是一个优先级算法

在集合中的所有元素,按照优先级从小到大排列,最近使用(取出)过的元素,优先级最高

这就是为什么这段代码的运行结果是这样:第三个元素最近被取出过,优先级最高,排在集合的末尾;其他元素没有被取出过,优先级相同,按照链表插入的顺序排列

我们可以再修改一下这段代码来加深理解

public static void main(String[] args)
{
    Map<String, Integer> cache = new LinkedHashMap<>(100, 0.75f, true);

    cache.put("第一个元素", 1);
    cache.put("第二个元素", 2);
    cache.put("第三个元素", 3);
    cache.put("第四个元素", 4);
    cache.put("第五个元素", 5);

    cache.get("第三个元素");
    cache.get("第一个元素");

    for (Map.Entry<String, Integer> entry : cache.entrySet())
    {
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}

这段代码的运行结果是:

结果3.png

原因:第一个元素最近被取出过,优先级最高,排在集合的末尾;第三个元素在第一个元素取出之前被取出过,优先级次于第一个元素,排在第一个元素之前;其他元素没有被取出过,优先级相同,按照链表插入的顺序排列

LruCache 的实现

Glide 中的 LruCache 类根据 LinkedHashMap 的 Lru 算法进行拓展实现了内存缓存

在 LruCache 类中对于 LinkedHashMap 集合的插入操作进行了一层封装

// LruCache.java
@Nullable
public synchronized Y put(@NonNull T key, @Nullable Y item)
{
    // 计算需要缓存的 Bitmap 的大小
    final int itemSize = getSize(item);
    ......
    if (item != null)
    {
        // 累计当前内存缓存中已使用的大小
        currentSize += itemSize;
    }
    // 将 Key 和 Value(Bitmap)一起存入 LinkedHashMap
    @Nullable final Y old = cache.put(key, item);
    ......
    evict();
    return old;
}

private void evict()
{
    // 传入设置的最大值
    trimToSize(maxSize);
}

/**
 * 从缓存中删除最近最少使用的项,直到当前大小小于给定大小(size)
 */
protected synchronized void trimToSize(long size)
{
    Map.Entry<T, Y> last;
    Iterator<Map.Entry<T, Y>> cacheIterator;
    // 如果当前内存缓存中已使用的大小大于给定的大小(最大值),执行循环
    while (currentSize > size)
    {
        cacheIterator = cache.entrySet().iterator();
        // 使用迭代器取出元素
        last = cacheIterator.next();
        final Y toRemove = last.getValue();
        currentSize -= getSize(toRemove);
        final T key = last.getKey();
        // 从 LinkedHashMap 中移除当前元素
        cacheIterator.remove();
        // 回收 Bitmap 资源
        onItemEvicted(key, toRemove);
    }
}

可以看得出来,Glide 中的 LruCache 类所做的的封装就是限制了 LinkedHashMap 中保存资源(Bitmap)的最大值,让 LinkedHashMap 中保存的资源不会超出设置的最大值

Glide 会根据运行设备的分辨率和屏幕尺寸来设置内存缓存的储存上限大小

当超出了设置的最大值时,会将 LinkedHashMap 中优先级低的元素(最近最少使用的元素)移除,直至 LinkedHashMap 中保存的资源大小小于设置的最大值

磁盘缓存

根据上一篇博客的流程,在 DecodeJob.notifyEncodeAndRelease() 函数中,会调用 DeferredEncodeManager.encode() 函数将解码完成的 Bitmap 资源缓存到磁盘中

我们来看一下这个 DeferredEncodeManager.encode() 函数

// DeferredEncodeManager.java
void encode(DiskCacheProvider diskCacheProvider, Options options)
{
    ......
    // diskCacheProvider.getDiskCache() = DiskLruCacheWrapper
    diskCacheProvider
        .getDiskCache()
        .put(key, new DataCacheWriter<>(encoder, toEncode, options));
    ......
}

// DiskLruCacheWrapper.java
@Override
public void put(Key key, Writer writer)
{
    ......
    // 创建 DiskLruCache
    DiskLruCache diskCache = getDiskCache();
    ......
    // 构建 DiskLruCache.Editor 修改器
    DiskLruCache.Editor editor = diskCache.edit(safeKey);
    ......
}

// DiskLruCache.java
public Editor edit(String key) throws IOException
{
    return edit(key, ANY_SEQUENCE_NUMBER);
}

private final LinkedHashMap<String, Entry> lruEntries = 
            new LinkedHashMap<String, Entry>(0, 0.75f, true);

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException
{
    ......
    Entry entry = lruEntries.get(key);
    ......
    if (entry == null)
    {
        entry = new Entry(key);
        lruEntries.put(key, entry);
    }
    ......
    Editor editor = new Editor(entry);
    entry.currentEditor = editor;
    ......
    return editor;
}

在 DiskLruCache 中也有一个 LinkedHashMap 集合,可以看到其构造函数的第三个参数为true,说明磁盘缓存也是基于 Lru 算法构建的

在 DiskLruCache 中将需要写入文件的数据使用 DiskLruCache.Editor 进行封装

我们继续看到 DiskLruCacheWrapper.put() 函数

@Override
// DiskLruCacheWrapper.java
public void put(Key key, Writer writer)
{
    ......
    DiskLruCache.Editor editor = diskCache.edit(safeKey);
    ......
    // 将资源(Bitmap)写入文件
    if (writer.write(file))
    {
        // IO 操作成功的回调
        editor.commit();
    }
    ......
}

// DiskLruCache.java
public void commit() throws IOException
{
    completeEdit(this, true);
    committed = true;
}

private synchronized void completeEdit(Editor editor, boolean success) throws IOException
{
    ......
    // 如果当前磁盘缓存中已使用的大小大于设置的最大值
    if (size > maxSize || journalRebuildRequired())
    {
        executorService.submit(cleanupCallable);
    }
}

// DiskLruCache.cleanupCallable
private final Callable<Void> cleanupCallable = new Callable<Void>()
{
    public Void call() throws Exception
    {
        ......
        trimToSize();
        ......
        return null;
    }
};

private void trimToSize() throws IOException
{
    // 如果当前磁盘缓存中已使用的大小大于设置的最大值,执行循环
    while (size > maxSize)
    {
        // 使用迭代器取出元素
        Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
        remove(toEvict.getKey());
    }
}

public synchronized boolean remove(String key) throws IOException
{
    ......
    // 从 LinkedHashMap 移除对应 Key 值的元素
    lruEntries.remove(key);
    ......
}

可以看出磁盘缓存与内存缓存结构上来说是差不多的,只不过是磁盘缓存相比内存缓存而言,多了一个文件 IO 读写的过程

Glide 缓存机制

我们已经知道了 Glide 中三种类型的缓存,那么这些缓存之间是如何工作的呢?

第一种情况:资源存放在活动缓存中

根据上一篇博客的流程,在 Engine.load() 函数中,会调用 Engine.loadFromMemory() 函数根据 Key 值去缓存中查找资源

此时资源存放在活动缓存中,会直接回调 ResourceCallback.onResourceReady() 函数,最后将图片显示到 UI 界面上

第二种情况:资源存放在内存缓存中

根据上一篇博客的流程,在 Engine.load() 函数中,会调用 Engine.loadFromMemory() 函数根据 Key 值去缓存中查找资源

此时资源存放在内存缓存中,就会执行 Engine.loadFromCache() 函数根据 Key 值去内存缓存中查找资源

// Engine.java
private EngineResource<?> loadFromCache(Key key)
{
    // 在内存缓存中查找资源
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null)
    {
        ......
        // 将该资源添加到活动缓存中
        activeResources.activate(key, cached);
    }
    return cached;
}

private EngineResource<?> getEngineResourceFromCache(Key key)
{
    ......
    // 从内存缓存中取出并移除对应 Key 值的元素
    Resource<?> cached = cache.remove(key);
    ......
}

当资源存放在内存缓存时,会将该资源移动到活动缓存中,并回调 ResourceCallback.onResourceReady() 函数,最后将图片显示到 UI 界面上

第三种情况:资源存放在磁盘缓存中

根据上一篇博客的流程,在 DecodeJob.getNextStage() 函数中,会判断资源是否在磁盘缓存中然后返回状态

如果资源存放在磁盘缓存中,DecodeJob.getNextStage() 函数就会返回 Stage.RESOURCE_CACHE

根据 Stage.RESOURCE_CACHE 状态就会获取到 ResourceCacheGenerator 执行器

当执行 DecodeJob.runGenerators() 函数时就会执行到 ResourceCacheGenerator.startNext() 函数

// ResourceCacheGenerator.java
public boolean startNext()
{
    ......
    // 从磁盘缓存中取出对应 Key 值的文件
    cacheFile = helper.getDiskCache().get(currentKey);
    ......
    /* ------------------------------- 之后就是解码流程 ---------------------------------- */
    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader())
    {
        loadData = helper.getLoadData().get(loadDataListIndex++);
        if (loadData != null
            && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
                || helper.hasLoadPath(loadData.fetcher.getDataClass())))
        {
            started = true;
            startNextLoad(loadData);
        }
    }
    return started;
}

当资源存放在磁盘缓存时,会将资源所在的文件从磁盘缓存中取出并执行解码流程,解码完成后就会将资源保存在活动缓存中,并回调 ResourceCallback#onResourceReady 函数,最后将图片显示到 UI 界面上

第四种情况:资源不存在于任何一种缓存中

上一篇博客的整体流程就是属于第四种情况

当资源不存在于任何一种缓存中时,会通过网络获取输入流 InputStream 并执行解码流程,解码完成后就会将资源保存在活动缓存中,根据缓存策略判断是否要保存在磁盘缓存中,并回调 ResourceCallback.onResourceReady() 函数,最后将图片显示到 UI 界面上

第五种情况:存放在活动缓存中的资源跟随 UI 界面生命周期的变化

通过上一篇博客的介绍,我们知道 RequestManager 对象会绑定 UI 界面的生命周期

// RequestManager.java
@Override
public synchronized void onStart()
{
    resumeRequests();
    targetTracker.onStart();
}

@Override
public synchronized void onStop()
{
    pauseRequests();
    targetTracker.onStop();
}

@Override
public synchronized void onDestroy()
{
    targetTracker.onDestroy();
    for (Target<?> target : targetTracker.getAll())
    {
        clear(target);
    }
    targetTracker.clear();
    requestTracker.clearRequests();
    ......
}

在 RequestManager 中会通过一个 TargetTracker 来管理 Target;通过一个 RequestTracker 来管理 Request

在对应 UI 生命周期的函数回调中,会回调 Target 和 Request 中对应的生命周期函数

在执行 RequestManager.track() 函数时,就会将 Target 和 Request 与生命周期绑定

// RequestManager.java
synchronized void track(@NonNull Target<?> target, @NonNull Request request)
{
    targetTracker.track(target);
    requestTracker.runRequest(request);
}

在这里只跟踪 UI 界面的 onStop 回调,其他的生命周期回调也是一样的

// RequestManager.java
public synchronized void onStop()
{
    // 回调 Request 的对应生命周期函数
    pauseRequests();
    ......
}

public synchronized void pauseRequests()
{
    requestTracker.pauseRequests();
}

// RequestTracker.java
public void pauseRequests()
{
    // 遍历 RequestTracker 中记录的 Request
    for (Request request : Util.getSnapshot(requests))
    {
        ......
        request.pause();
        ......
    }
}

// SingleRequest.java(以 SingleRequest 为例)
@Override
public void pause()
{
    ......
    clear();
    ......
}

@Override
public void clear()
{
    ......
    engine.release(toRelease);
    ......
}

// Engine.java
public void release(Resource<?> resource)
{
    ......
    ((EngineResource<?>) resource).release();
    ......
}

// EngineResource.java
void release()
{
    listener.onResourceReleased(key, this);
}

// Engine.java
@Override
public void onResourceReleased(Key cacheKey, EngineResource<?> resource)
{
    // 从活动缓存中移除对应 Key 值的元素
    activeResources.deactivate(cacheKey);
    ......
    // 将 Key 和 Value(Bitmap)一起存入内存缓存
    cache.put(cacheKey, resource);
    ......
}

可以看到 在 UI 界面的 onStop 回调中,会将活动缓存中的所有资源移到内存缓存中

onDestroy 的流程也是类似的,也会将活动缓存中的所有资源移到内存缓存中

Glide 中的三种缓存,只有活动缓存会跟随 UI 界面的生命周期而变化

总结

可以用一幅流程图来总结 Glide 的缓存机制

Glide 缓存流程.png