上一篇博客介绍了 Glide 的整体流程,但是对于 Glide 缓存机制的细节没有进行说明,将在这篇博客中进行介绍。Glide 缓存机制是面试的高频考点,理解 Glide 缓存机制对于我们应该如何构建一个图片加载框架有着极大的帮助。
Glide 缓存中分为几种类型?
Glide 中分为三种缓存:
- 活动缓存(运行时内存)
- 内存缓存(运行时内存)
- 磁盘缓存(文件存储)
活动缓存
根据上一篇博客的流程,在 Engine#load
函数中,会调用 Engine#loadFromMemory
函数根据 Key 值去缓存中查找资源。
我们来看一下这个 Engine#loadFromMemory
函数。
@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());
}
}
复制代码
这段代码的运行结果是:
现在我们来修改一下这段代码
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());
}
}
复制代码
这段代码的运行结果是:
可以看到,第三个元素的位置发生了变化,这就是最近最少使用原则的体现。可以理解为最近最少使用算法是一个优先级算法。在集合中的所有元素,按照优先级从小到大排列,最近使用(取出)过的元素,优先级最高。
这就是为什么这段代码的运行结果是这样:第三个元素最近被取出过,优先级最高,排在集合的末尾;其他元素没有被取出过,优先级相同,按照链表插入的顺序排列。
我们可以再修改一下这段代码来加深理解
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());
}
}
复制代码
这段代码的运行结果是:
原因:第一个元素最近被取出过,优先级最高,排在集合的末尾;第三个元素在第一个元素取出之前被取出过,优先级次于第一个元素,排在第一个元素之前;其他元素没有被取出过,优先级相同,按照链表插入的顺序排列。
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 中保存的资源不会超出设置的最大值。
当超出了设置的最大值时,会将 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 框架。