Glide源码解析之缓存

Glide源码解析之缓存

Glide缓存机制分为内存缓存和硬盘缓存。内存缓存的目的是避免重复加载图片到内存、提升加载速度;硬盘缓存的目的是避免重复从网络下载图片造成流量的浪费、同时提升加载速度

读取内存缓存

读取内存缓存的地方是在 Engine#load 方法里:

1
2
//读取内存中缓存的图片
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

进入 loadFromMemory 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (!isMemoryCacheable) {
//禁用内存缓存则返回空
return null;
}
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
//要加载的图片正在被使用(即该图片是active的),可直接使用该图片
return active;
}
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
//该图片缓存在LRU内存里
return cached;
}

内存缓存也分两级。先看 activeResources 是否存在,activeResources使用一个集合保存着图片的弱引用

1
2
//该集合保存着图片的弱引用,Key代表图片的key
Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();

如果存在的话,该图片资源EngineResource的引用次数acquired会加1。如果不存在的话则接着去内存中找:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private EngineResource<?> getEngineResourceFromCache(Key key) {
//从内存中读取缓存,cache 的类型是 MemoryCache
Resource<?> cached = cache.remove(key);
final EngineResource<?> result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
result = (EngineResource<?>) cached;
} else {
result =
new EngineResource<>(
cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}

MemoryCache 是个接口。MemoryCache有两个实现类 LruResourceCache 和 MemoryCacheAdapter(低版本适配)。重点看一下LruResourceCache类。它继承了 LruCache 类,LruCache 类里使用 LinkedHashMap 来保存最近使用的图片:

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

LinkedHashMap类是java自带的类,实现了LRU缓存(LRU是最近最少使用算法),可以设置一个内存的阈值,当内存超过该阈值时会移除掉最老的缓存。LinkedHashMap实现LRU算法的原理可参考 https://www.jianshu.com/p/8f4f58b4b8ab

读取硬盘缓存

硬盘缓存也分为两种情况,一种是缓存原始图片,一种是缓存转换后的图片。

读取原始图片缓存的代码是在 DataCacheGenerator 类的 startNext 方法里:

1
cacheFile = helper.getDiskCache().get(originalKey);

helper.getDiskCache()获取到DiskCache对象,DiskCache 是接口类,DiskCache默认的实现类是 DiskLruCacheWrapper,看一下它的 get 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
//计算key的sha256值
String safeKey = safeKeyGenerator.getSafeKey(key);
File result = null;
try {
//获取缓存值
final DiskLruCache.Value value = getDiskCache().get(safeKey);
if (value != null) {
result = value.getFile(0);
}
} catch (IOException e) {
...
}
return result;

细看这一行代码:

1
final DiskLruCache.Value value = getDiskCache().get(safeKey)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
return null;
}
for (File file : entry.cleanFiles) {
// A file must have been deleted manually!
if (!file.exists()) {
return null;
}
}
redundantOpCount++;
journalWriter.append(READ);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);

lruEntries的类型也是LinkedHashMap,可知硬盘缓存也遵循LRU算法以避免磁盘缓存的文件过大。

写入内存缓存

写入弱引用缓存分为两种情况:

  1. 一种是从内存强引用LRU读取缓存后,把对应的图片从LRU移除的同时会把该图片加入到弱引用集合中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    private EngineResource<?> loadFromCache(Key key) {
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
    //把该图片加入到弱引用集合,图片的引用计数加1
    cached.acquire();
    activeResources.activate(key, cached);
    }
    return cached;
    }
  2. 另一种情况是当首次加载图片的任务完成之后把该图片加入到弱引用集合中。如下:

    1
    2
    3
    4
    5
    6
    7
    8
    public synchronized void onEngineJobComplete(
    EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    //把图片加入到弱引用集合
    if (resource != null && resource.isMemoryCacheable()) {
    activeResources.activate(key, resource);
    }
    ...
    }

写入LRU缓存

写入LRU缓存是在 Engine 类的 onResourceReleased 方法中:

1
cache.put(cacheKey, resource);

对于 onResourceReleased 方法的调用时机,分为两种情况:

一种是图片的引用计数次数变为 0 的时候会调用,具体代码位置在 EngineResource 类的 release 方法中:

1
2
3
4
5
6
7
void release() {
...
if (release) {
//图片的引用计数次数变为 0 的时候会调用 onResourceReleased 方法
listener.onResourceReleased(key, this);
}
}

另一种情况是当ResourceWeakReference弱引用的对象被 gc 回收后调用 cleanupActiveReference 方法,cleanupActiveReference 同样会调用 onResourceReleased 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
synchronized (this) {
//图片被 gc 回收后移除 activeEngineResources 中对应的图片key
activeEngineResources.remove(ref.key);
if (!ref.isCacheable || ref.resource == null) {
return;
}
}
EngineResource<?> newResource =
new EngineResource<>(
ref.resource, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ false, ref.key, listener);
//图片被 gc 回收掉后,创建一个新的图片资源 newResource,放到LRU内存中
listener.onResourceReleased(ref.key, newResource);
}

写入硬盘缓存

一处是在DecodeJob 类中,当图片数据获取完成之后,会先解码再进行编码,onDataFetcherReady -> decodeFromRetrievedData -> notifyEncodeAndRelease 中:

1
2
3
4
if (deferredEncodeManager.hasResourceToEncode()) {
//调用encode方法把图片写入磁盘
deferredEncodeManager.encode(diskCacheProvider, options);
}

encode方法具体实现如下:

1
2
3
4
5
6
7
8
9
10
void encode(DiskCacheProvider diskCacheProvider, Options options) {
try {
//把图片写入磁盘
diskCacheProvider
.getDiskCache()
.put(key, new DataCacheWriter<>(encoder, toEncode, options));
} finally {
toEncode.unlock();
}
}

另一处是在SourceGenerator类中,

1
2
3
4
5
6
7
8
public boolean startNext() {
//缓存原始未修改的图片
if (dataToCache != null) {
Object data = dataToCache;
dataToCache = null;
cacheData(data);
}
}

进入 cacheData 方法中:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void cacheData(Object dataToCache) {
try {
Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
DataCacheWriter<Object> writer =
new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
//把图片写入磁盘
helper.getDiskCache().put(originalKey, writer);
} finally {
loadData.fetcher.cleanup();
}
...
}

BitmapPool

bitmap 的内存管理是一个很重要的话题。glide 使用了 BitmapPool 来提升 bitmap 的复用,减少 bitmap 分配内存和回收内存的次数,从而减少 GC 的频率、应用运行更加流畅。

Android 官方提供 inBitmap 的 api 来复用 bitmap.实例如下:

1
2
3
4
5
6
7
8
9
10
if (cache != null) {
// Try to find a bitmap to use for inBitmap.
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
// If a suitable bitmap has been found, set it as the value of
// inBitmap.
options.inBitmap = inBitmap;
}
}

BitmapPool 有两个实现类: LruBitmapPool 和 BitmapPoolAdapter 类.BitmapPoolAdapter 的实现是一个空壳子,重点看一下 LruBitmapPool 类。LruBitmapPool 提供了 put 和 get 方法来操作 bitmap。LruBitmapPool 实际上是通过 LruPoolStrategy 对象来实现存取的。LruPoolStrategy 有多个实现类,不同的系统版本会使用不同的实现类:

1
2
3
4
5
6
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//android 4.4 版本以上
strategy = new SizeConfigStrategy();
} else {
strategy = new AttributeStrategy();
}

因为目前大多数 app 的最低系统版本在 4.4 以上了,所以 重点看一下 SizeConfigStrategy ,先看 put 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void put(Bitmap bitmap) {
//获取图片所占内存的字节数
int size = Util.getBitmapByteSize(bitmap);
//key由size和Bitmap.Config组成
Key key = keyPool.get(size, bitmap.getConfig());
//groupedMap的类型是 GroupedLinkedMap,类似 LinkedHashMap ,支持 LRU 算法。
groupedMap.put(key, bitmap);
NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig());
Integer current = sizes.get(key.size);
//记录该尺寸的图片的数量
sizes.put(key.size, current == null ? 1 : current + 1);
}

GroupedLinkedMap 为 glide 自定义的数据结构,支持 LRU 算法,与 LinkedHashMap 类似。不同的是,LinkedHashMap 的 LRU 是针对的 value 值,而 GroupedLinkedMap 是针对的 key 值即图片的 size、而不是bitmap。

接下来看一下 get 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public Bitmap get(int width, int height, Bitmap.Config config) {
int size = Util.getBitmapByteSize(width, height, config);
//这行代码很关键。找到最接近目标值缓存图片的key
Key bestKey = findBestKey(size, config);
Bitmap result = groupedMap.get(bestKey);
if (result != null) {
// Decrement must be called before reconfigure.
decrementBitmapOfSize(bestKey.size, result);
result.reconfigure(width, height, config);
}
return result;
}

findBestKey 方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Key findBestKey(int size, Bitmap.Config config) {
Key result = keyPool.get(size, config);
for (Bitmap.Config possibleConfig : getInConfigs(config)) {
NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
//返回大于等于size的最小值。
Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
//如果返回的缓存大小超过了目标值的 8 倍,缓存的使用率变低、就没必要复用缓存了
if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
if (possibleSize != size
|| (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
keyPool.offer(result);
result = keyPool.get(possibleSize, possibleConfig);
}
break;
}
}
return result;
}

ArrayPool

作用与 bitmapPool 类似,也是复用数组以减少内存的分配和回收,从而减少 GC 的频率、应用运行更加流畅。