Glide 源码分析:缓存机制

本文是 Glide 源码分析系列的第二篇,主要通过分析源码总结 Glide 的缓存机制。

从加载流程揭开缓存机制的面纱

首先回忆一下上一篇关于 Glide 加载流程源码分析的内容,我们从 Glide.with().load().into() 这个最简单最基本的用法入手,一步步深入源码,梳理出了完整的图片加载流程。由于当时分析重点在于整体流程的把握上,所以对于缓存相关的部分都是简单带过而没有进行深入分析。首先是为了避免文章篇幅过长,其次因为缓存它不是独立的部分,它埋藏在整个加载流程的各个环节中,所以对缓存机制的理解应该建立在对整体流程清晰的把握上。

而在本文中,我们将从整个加载流程入手,找出缓存相关的部分,进而还原出 Glide 缓存机制的整体面貌。然后再对各个部分进行详细分析。

那么我们开始来看看加载流程中被我们错过的缓存操作。

读取内存缓存

我们在 GenericRequestBuilder 中构建了一个 GenericRequest 实例并交给 RequestTracker 去处理,执行它的 begin() 方法。在 GenericRequest 的 确定了 ImageView 的大小之后在 onSizeReady() 回调中调用了 Engine 的load() 进行加载。这时我们第一次遇到了缓存操作

public class Engine implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {

public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
......

final String id = fetcher.getId();
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());

/// 从LruResourceCache中获取
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
///GenericRequest
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}

/// 从activeResources中获取
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
///GenericRequest
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}

......
}

private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}

EngineResource<?> active = null;
WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
if (activeRef != null) {
active = activeRef.get();
if (active != null) {
active.acquire();
} else {
activeResources.remove(key);
}
}

return active;
}

///从LruResourceCache中获取,若有则移除并放入activesource
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}

EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
return cached;
}

@SuppressWarnings("unchecked")
private EngineResource<?> getEngineResourceFromCache(Key key) {
Resource<?> cached = cache.remove(key);

final EngineResource result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
// Save an object allocation if we've cached an EngineResource (the typical case).
result = (EngineResource) cached;
} else {
result = new EngineResource(cached, true /*isCacheable*/);
}
return result;
}
}

程序首先调用 loadFromCache() 尝试从 MemoryCache 中获取,如果命中缓存则将缓存从 MemoryCache 中移除并放入 activeResources,然后返回。如果缓存失效则尝试从 activeResources 中获取,如果都失效再构建 EngineRunnable 从磁盘或者网络获取。

是否决定从 MemoryCache 和 activeResources 中获取的前提条件是 isMemoryCacheable 为 true。这个值从 GenericRequestBuilder 传过来,默认为true,也就是说 Glide 默认开启内存缓存。除非你主动调用了 skipMemoryCache() 使该加载请求跳过内存缓存。该方法就是通过将 isMemoryCacheable 置为 false 实现的。

这里出现了 MemoryCache 和 activeResources,它们一起构成的 Glide 中的内存缓存。它们的 Key,都是由 url、图片大小、decoder、encoder等变量组成。MemoryCache 用于保存最近使用过而当前不在使用的 EngineResource,这也是缓存命中是需要将缓存移除并添加到 activeResources 的原因。其内部使用 LinkedHashMap,当大小达到一个阈值时通过 LRU 算法来清除。在 Glide 中 MemoryCache 的默认实现是 LruResourceCache,在 GlideBuilder 中被初始化。activeResources 用于保存当前正在被使用的 EngineResource,是一个使用 Key 作为键, EngineResource 的弱引用为值的 HashMap。

读取磁盘缓存

当 MemoryCache 和 activeResources 都失效时,程序才构建一个 EngineRunnable 并交给线程池执行。在 EngineRunnable 的 run() 方法中就调用了 decode() 尝试从磁盘或网络获取图片,这里,我们第二次遇到了缓存操作

class EngineRunnable implements Runnable, Prioritized {

@Override
public void run() {
if (isCancelled) {
return;
}

Exception exception = null;
Resource<?> resource = null;
try {
///得到了Resource<GlideDrawable>对象
resource = decode();
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Exception decoding", e);
}
exception = e;
}

if (isCancelled) {
if (resource != null) {
resource.recycle();
}
return;
}

if (resource == null) {
/// 第一次走decode()尝试从磁盘获取,失败后会走这里
onLoadFailed(exception);
} else {
onLoadComplete(resource);
}
}

private boolean isDecodingFromCache() {
return stage == Stage.CACHE;
}

private Resource<?> decode() throws Exception {
if (isDecodingFromCache()) {
///从磁盘缓存中decode图片,第一次会走这
return decodeFromCache();
} else {
///从源中decode图片,第二次会走这
return decodeFromSource();
}
}

private Resource<?> decodeFromCache() throws Exception {
Resource<?> result = null;
try {
result = decodeJob.decodeResultFromCache();
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Exception decoding result from cache: " + e);
}
}

if (result == null) {
result = decodeJob.decodeSourceFromCache();
}
return result;
}
}

在 decode() 方法中,决定从源获取图片之前,先调用 decodeFromCache() 方法尝试从磁盘中获取图片。而 decodeFromCache() 方法中又先后调用 decodeResultFromCache() 获取处理图 和 decodeSourceFromCache() 获取原图。

那么我们去看看这两个方法

class DecodeJob<A, T, Z> {

///从磁盘decode转化过的resource,然后transcode
public Resource<Z> decodeResultFromCache() throws Exception {
if (!diskCacheStrategy.cacheResult()) {
return null;
}

long startTime = LogTime.getLogTime();
///Resource<GifBitmapWrapper>
Resource<T> transformed = loadFromCache(resultKey);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded transformed from cache", startTime);
}
startTime = LogTime.getLogTime();
///Resource<GlideDrawable>
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from cache", startTime);
}
return result;
}

public Resource<Z> decodeSourceFromCache() throws Exception {
if (!diskCacheStrategy.cacheSource()) {
return null;
}

long startTime = LogTime.getLogTime();
///Resource<GifBitmapWrapper> 使用的是OriginalKey
Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Decoded source from cache", startTime);
}
return transformEncodeAndTranscode(decoded);
}

private Resource<T> loadFromCache(Key key) throws IOException {
File cacheFile = diskCacheProvider.getDiskCache().get(key);
if (cacheFile == null) {
return null;
}

Resource<T> result = null;
try {
///FileToStreamDecoder,将file解码成Resource<GifBitmapWrapper>实例是GifBitmapWrapperResource
result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
} finally {
if (result == null) {
diskCacheProvider.getDiskCache().delete(key);
}
}
return result;
}
}

decodeResultFromCache() 方法和 decodeSourceFromCache() 方法内部都调用了 loadFromCache() 方法来获取磁盘缓存,只不过前者使用的参数是 resultKey 而后者使用的是 OriginalKey。这里的 loadProvider 是一个 LazyDiskCacheProvider 对象,里面封装了一个 InternalCacheDiskCacheFactory,当调用 getCacheDecoder() 时,方法内部将调用 InternalCacheDiskCacheFactory 的 build() 方法,最终返回一个 DiskLruCacheWrapper 对象。现在只要知道它是我们操作磁盘缓存的直接对象即可,内部原理将在文章后半部分进行分析。

它们得到的都是一个缓存文件,只不过前者写入的内容是变换后的图片,后者后者写入的内容是原始的图片。所以前者获取文件之后只需要进行解码和转码即可,而后者需要进行变换加上解码和转码。

decodeSourceFromCache() 方法中从磁盘缓存中获取原图之后调用了 transformEncodeAndTranscode() 方法进行变换和转码,这里也涉及到缓存的操作,这个会在后面讲到。

是否决定从磁盘缓存中获取取决于 DiskCacheStrategy,它是一个枚举类,用来表示一组缓存策略,cacheSource 属性表示是否缓存原图,cacheResult 属性表示是否缓存处理图。Glide 的默认的磁盘缓存是 RESULT,即只缓存处理图,也可以通过 diskCacheStrategy() 方法来指定。

public enum DiskCacheStrategy {
/** Caches with both {@link #SOURCE} and {@link #RESULT}. */
ALL(true, true),
/** Saves no data to cache. */
NONE(false, false),
/** Saves just the original data to cache. */
SOURCE(true, false),
/** Saves the media item after all transformations to cache. */
RESULT(false, true);

private final boolean cacheSource;
private final boolean cacheResult;

DiskCacheStrategy(boolean cacheSource, boolean cacheResult) {
this.cacheSource = cacheSource;
this.cacheResult = cacheResult;
}
}

往磁盘缓存写入原图

当从磁盘缓存中既取不到原图也取不到处理图时,才会发起网络请求去获取。相应的逻辑在 DecodeJob 的 decodeFromSource() 方法中。

在 decodeFromSource() 方法中,程序首先调用了 decodeSource() 方法来从网络中获取 InputStream 以及解码成 Bitmap,然后调用 transformEncodeAndTranscode() 来进行图片的变换和转码。这两个步骤中都涉及到对缓存的操作。先来看看 decodeSource() 方法

private Resource<T> decodeSource() throws Exception {
Resource<T> decoded = null;
try {
long startTime = LogTime.getLogTime();
///ImageVideoFetcher,内部使用... 返回一个ImageVideoWrapper
final A data = fetcher.loadData(priority);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Fetched data", startTime);
}
if (isCancelled) {
return null;
}
///返回 Resource<GifBitmapWrapper>
decoded = decodeFromSourceData(data);
} finally {
fetcher.cleanup();
}
return decoded;
}

private Resource<T> decodeFromSourceData(A data) throws IOException {
final Resource<T> decoded;
///判断磁盘缓存策略中是否缓存source,缓存ImageVideoWrapper并decode(原始InputStream)
if (diskCacheStrategy.cacheSource()) {
decoded = cacheAndDecodeSourceData(data);
} else {
......
}
return decoded;
}

private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
long startTime = LogTime.getLogTime();
///loadProvider是FixedLoadProvider,里面封装了ImageVideoGifDrawableLoadProvider
// 这里实际返回的ImageVideoWrapperEncoder
SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
///diskCacheProvider是一个LazyDiskCacheProvider,返回DiskLruCacheWrapper实例
///resultKey由Engine构造decodejob的时候传进来,由EngineKeyFactory.builidKey返回.
diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Wrote source to cache", startTime);
}

startTime = LogTime.getLogTime();
Resource<T> result = loadFromCache(resultKey.getOriginalKey());
if (Log.isLoggable(TAG, Log.VERBOSE) && result != null) {
logWithTimeAndKey("Decoded source from cache", startTime);
}
return result;
}

decodeSource() 方法中 DateFetcher 的 loadData() 方法从网络中获取数据之后,调用了 decodeFromSourceData() 方法开始进行解码。在 decodeFromSourceData() 方法中,解码前首先判断当前磁盘缓存策略,如果 cacheSource 为 true,那么解码前还有写入磁盘缓存的操作,也就是 cacheAndDecodeSourceData() 方法。方法中获取了 DiskLruCacheWrapper 对象然后调用了它的 put() 方法来写入缓存,最终会将从服务器获取的 InputSteream 写入一个缓存文件中,缓存对应的 Key 是由 Url 生成的 OriginalKey。

往磁盘缓存写入处理图

回到 decodeSource() 方法,将图片解码之后,接着就是调用 transformEncodeAndTranscode() 进行变换和转码,这里也涉及到缓存的操作

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
long startTime = LogTime.getLogTime();
///因为decodeSourceFromCache去取出的resource还没有经过transform,要先transform再缓存起来
Resource<T> transformed = transform(decoded);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transformed resource from source", startTime);
}

///将transform后的resource写入缓存 bitmap或GifDrawable
writeTransformedToCache(transformed);

startTime = LogTime.getLogTime();
///与decodeResultFromCache的第二步一样
Resource<Z> result = transcode(transformed);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Transcoded transformed from source", startTime);
}
return result;
}

private void writeTransformedToCache(Resource<T> transformed) {
if (transformed == null || !diskCacheStrategy.cacheResult()) {
return;
}
long startTime = LogTime.getLogTime();
///GifBitmapWrapperResourceEncoder
SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
///resultKey是一个EngineKey
diskCacheProvider.getDiskCache().put(resultKey, writer);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Wrote transformed from source to cache", startTime);
}
}

transformEncodeAndTranscode() 方法中调用了 transform() 方法进行图片变换之后调用了 writeTransformedToCache() 方法来尝试将 Resource 写入磁盘缓存。具体是借助 GifBitmapWrapperResourceEncoder 来完成,有兴趣可以自行研究。

写入内存缓存

按着图片的加载流程,经过变换和转码之后,会回到 EngineRunnable 的 run() 方法,然后回调 EngineJob 的 onResourceReady() 方法,通知图片获取已经完成。我们直接来到 handleResultOnMainThread() 方法,这时 EngineJob 正准备将 onResourceReady() 回调分发给 GenericRequest。

private void handleResultOnMainThread() {
if (isCancelled) {
resource.recycle();
return;
} else if (cbs.isEmpty()) {
throw new IllegalStateException("Received a resource without any callbacks to notify");
}
///封装成一个EngineResource
engineResource = engineResourceFactory.build(resource, isCacheable);
hasResource = true;

// Hold on to resource for duration of request so we don't recycle it in the middle of notifying if it
// synchronously released by one of the callbacks.
engineResource.acquire();
///key是一个EngineKey。如果resource.isCacheable()为true,resource将添加到activeResources中,并在移除时加入memorycache
listener.onEngineJobComplete(key, engineResource);

///这里的ResourceCallback就是GenericRequest,由GenericRequest调用EngineJob的load方法时传入
for (ResourceCallback cb : cbs) {
if (!isInIgnoredCallbacks(cb)) {
engineResource.acquire();
///回调GenericRequest
cb.onResourceReady(engineResource);
}
}
// Our request is complete, so we can release the resource.
engineResource.release();
}

public void onEngineJobComplete(Key key, EngineResource<?> resource) {
Util.assertMainThread();
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null) {
///监听engineresource的释放,从activeResources中移除,移除时放入MemoryCache中
resource.setResourceListener(key, this);

if (resource.isCacheable()) {
///将resource放到activeResources中
activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
}
}
// TODO: should this check that the engine job is still current?
jobs.remove(key);
}

@Override
public void onResourceReleased(Key cacheKey, EngineResource resource) {
Util.assertMainThread();
activeResources.remove(cacheKey);
if (resource.isCacheable()) {
///将resource转移到cache中
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}

注意 listener.onEngineJobComplete(key, engineResource) ,这里的 listener 正是 EngineJob 自身。在 EngineJob 的 onEngineJobComplete() 方法中给 EngineResource 设置了一个 ResourceListener,并且如果没有设置跳过内存缓存,将 EngineResource 放入 activeResources 中。那么 ResourceListener 做了什么呢?

ResourceListener 接口中只有 onResourceReleased() 一个方法,当 EngineResource 需要被释放时会回调该方法来通知监听者。这里的监听者正是 EngineJob,在 EngineJob 的 onResourceReleased() 方法中,将 EngineResource 从 activeResources 中删除,并且如果没有设置跳过内存缓存,则放入 MemoryCache 中,否则回收 EngineResource。这讲导致 EngineResource 中持有的 Bitmap 回收到 BitmapPool 中,等待复用。

接着图片将显示到 Target 上,加载流程结束,其中涉及的缓存操作也分析完毕。

那么,我们以缓存作为重点将加载流程梳理一下,就得到这样一个流程图

《Glide 源码分析:缓存机制》

现在应该对整个缓存的流程有一个整体的认识了,下面将逐一各部分的内部细节,将涉及内存缓存、磁盘缓存和 BitmapPool。

内存缓存

Glide 的内存缓存并不像传统的那样仅仅使用 强引用+LRU 算法,还加入了 ActiveResource 这个概念。防止 LruResourceCache 回收了正在使用的资源。

LruResourceCache + BitmapPool 缓存的最大空间默认为应用可用最大内存*0.4,低配手机是应用可用最大内存*0.33.

引用计数

内存缓存包括 LruResourceCache 和 ActiveResources 存储的对象都是 EngineResource 对象。它是 Resource 对象的封装,并且内部维护对 Resource 的引用计数。调用 acquire() 引用计数+1,调用 release() 引用计数-1.当调用 release() 后引用计数为0时,由 ResourceListener(目前只有 Engine)来将其从 ActiveResources 移除并决定放入 LruResourceCache 还是调用 Resouece 的 recycler() 方法来回收相关资源,例如将 Bitmap 放入 BitmapPool。

LruResourceCache

LruResourceCache 用于保存最近被使用但是当前不在使用的资源。它的最大容量与屏幕分辨率和 Bitmap 质量参数有关,默认是 宽度 Pixels*高度 Piexls*ARGB_8888图片的质量参数*2,这样至少足够缓存两个屏幕大小的图片了。当容量过大时,使用 LRU 算法来移除最近最少使用的缓存项。

LruResourceCache 实现了 MemoryCache 接口,提供了 get()、remove()、getCurrentSize()、setResourceRemovedListener() 等方法,要注意的是并没有提供 get() 方法,在 Glide 中读取 LruResourceCache 往往是为了放入 ActiveResources,所以用 remove() 就足够了。同时继承了 LruCache,缓存逻辑的实现主要来自于 LruCache。LruCache 内部使用 LinkedHashMap 来实现 LRU 算法。没有十分复杂的逻辑,就不详细分析了。

还有值得注意的是,Engine 通过 setResourceRemovedListener() 方法设置了 ResourceRemovedListener。当一个缓存项因为 LruResourceCache 容量过大使用 LRU 算法被移除(不包括调用 remove() 方法移除的情况)时,将接受到 onResourceRemoved() 回调。Engine 在 onResourceRemoved() 调用了 Resource 的 recycler() 方法,这意味着 Resource 持有的 Bitmap 将会放入到 BitmapPool 中。

总结一下,LruResourceCache 用于保存最近被使用但是当前不在使用的资源。其中缓存的来源只有一个:ActiveResources。当 ActiveResources 中的缓存不再被使用时,会被移除,放入 LruResourceCache 中。缓存的去向有两个:一个是缓存通过 remove() 被读取时,转移到 ActiveResources 中,第二个是最近使用较少被 LRU 算法移除,然后相关的 Bitmap 资源回收到 BitmapPool。

ActiveResources

ActiveResources 用于保存当前正在使用的资源,它其实就是一个裸露的 HashMap,对缓存的读取与写入就是 get() 和 put() 方法的调用。它使用 EngineKey 为键,使用 EngineResource 的弱引用为值。所以也没有容量大小可言。

虽说 ActiveResources 中的资源是当前正在使用的资源,Glide 也提供了负责清理 ActiveResources 弱可达资源的实现(可能是某些特殊场景下没有调用 release() 会导致 ActiveResources 中的资源实际上不在使用了?)。实现方法是构造 EngineResource 的弱引用 WeakReference 时传进 ReferenceQueue 来监听弱引用的回收。当系统检测到该 EngineResource 是弱可达,即不在被使用时,就将该弱引用放入 ReferenceQueue 中。如果 ReferenceQueue 不为空,就说明 EngineResource 该被回收了,可是在什么时候回收呢?

Glide 在当前线程对应 Looper 所对应的 MessageQueue 中通过 addIdleHandler() 方法添加了一个 IdleHandler 实例 RefQueueIdleHandler,当 MessageQueue 空闲的时候就会回调 IdleHandler 的 queueIdle() 方法,在 queueIdle() 方法中清理 WeakReference 被回收的 activeResources 资源。

private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}

EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
return cached;
}

private static class ResourceWeakReference extends WeakReference<EngineResource<?>> {
private final Key key;

public ResourceWeakReference(Key key, EngineResource<?> r, ReferenceQueue<? super EngineResource<?>> q) {
super(r, q);
this.key = key;
}
}

private ReferenceQueue<EngineResource<?>> getReferenceQueue() {
if (resourceReferenceQueue == null) {
resourceReferenceQueue = new ReferenceQueue<EngineResource<?>>();
MessageQueue queue = Looper.myQueue();
queue.addIdleHandler(new RefQueueIdleHandler(activeResources, resourceReferenceQueue));
}
return resourceReferenceQueue;
}

// Responsible for cleaning up the active resource map by remove weak references that have been cleared.
private static class RefQueueIdleHandler implements MessageQueue.IdleHandler {
private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
private final ReferenceQueue<EngineResource<?>> queue;

public RefQueueIdleHandler(Map<Key, WeakReference<EngineResource<?>>> activeResources,
ReferenceQueue<EngineResource<?>> queue) {
this.activeResources = activeResources;
this.queue = queue;
}

@Override
public boolean queueIdle() {
ResourceWeakReference ref = (ResourceWeakReference) queue.poll();
if (ref != null) {
activeResources.remove(ref.key);
}

return true;
}
}

磁盘缓存

除了内存缓存之外, Glide 还设计了磁盘缓存。磁盘缓存的默认路径在 /data/data/\ /cache/image_manager_disk_cache,默认的大小为 250MB,默认的实现类是 DiskLruCacheWrapper。

Glide 的磁盘缓存默认只缓存处理图,不过可以根据需要通过设置 DiskCacheStrategy 来定制缓存策略。Glide 中不管是原图还是处理图,每张图片都与一个缓存文件对应,存取原图时使用 OriginalKey,内部封装了图片的 Url 和 Signature 信息。而存取处理图时使用 EngineKey 内部封装了图片的 Url 和 Signature 之外,还包括图片尺寸和图片处理流程中相关的 Decoder、Encoder、Transformation 所返回的 id,,通过这些信息就能唯一确定一张图片了。

DiskLruCacheWrapper

DiskLruCacheWrapper 实现了 DiskCache 接口,提供的方法非常简洁,提供了 get()、put()、delete()、clear() 四个方法。DiskLruCacheWrapper 内部封装了 DiskLruCache 对象,缓存文件的管理由它来完成。

下面从 DiskLruCacheWrapper 的角度分析一下缓存的写入与读取。

缓存写入
public File get(Key key) {
///使用SHA-256算法生成唯一String
String safeKey = safeKeyGenerator.getSafeKey(key);
File result = null;
try {
//It is possible that the there will be a put in between these two gets. If so that shouldn't be a problem
//because we will always put the same value at the same key so our input streams will still represent
//the same data
final DiskLruCache.Value value = getDiskCache().get(safeKey);
if (value != null) {
result = value.getFile(0);
}
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to get from disk cache", e);
}
}
return result;
}

public String getSafeKey(Key key) {
String safeKey;
synchronized (loadIdToSafeHash) {
safeKey = loadIdToSafeHash.get(key);
}
if (safeKey == null) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
key.updateDiskCacheKey(messageDigest);
safeKey = Util.sha256BytesToHex(messageDigest.digest());
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
synchronized (loadIdToSafeHash) {
loadIdToSafeHash.put(key, safeKey);
}
}
return safeKey;
}

private synchronized DiskLruCache getDiskCache() throws IOException {
if (diskLruCache == null) {
diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
}
return diskLruCache;
}

put() 方法首先调用 getSafeKey() 方法将 Key 转化为一个唯一的字符串,原理是将 Key 中封装的信息(原图就是图片的 Url,处理图就是 Url、尺寸、Decoder等)通过 SHA-256 算法进行编码。缓存的存取都是使用这个字符串,同时这也是缓存文件的文件名。

getDiskCache() 方法返回了 DiskLruCache,方法内部调用了它的静态方法 open() 来创建或返回一个现有的 DiskLruCache 实例。

返回 DiskLruCache 实例之后,通过之前生成的字符串作为参数调用了它的 get() 方法,该方法 返回 DiskLruCache.Value 对象,然后通过 Value 对象来返回一个缓存文件。DiskLruCacheWrapper 就完成了,之后就由 ResourceDecoder 获取文件的 InputSteream 然后解码成 Bitmap 了。

put() 的逻辑非常简单,因为 DiskLruCache 已经封装了缓存实现的细节。DiskLruCache 的实现细节后面再详细分析。

缓存读取
public void put(Key key, Writer writer) {
String safeKey = safeKeyGenerator.getSafeKey(key);
writeLocker.acquire(key);
try {
DiskLruCache.Editor editor = getDiskCache().edit(safeKey);
// Editor will be null if there are two concurrent puts. In the worst case we will just silently fail.
if (editor != null) {
try {
File file = editor.getFile(0);
if (writer.write(file)) {
editor.commit();
}
} finally {
editor.abortUnlessCommitted();
}
}
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Unable to put to disk cache", e);
}
} finally {
writeLocker.release(key);
}
}

class SourceWriter<DataType> implements DiskCache.Writer {

private final Encoder<DataType> encoder;
private final DataType data;

public SourceWriter(Encoder<DataType> encoder, DataType data) {
this.encoder = encoder;
this.data = data;
}

@Override
public boolean write(File file) {
boolean success = false;
OutputStream os = null;
try {
os = fileOpener.open(file);
success = encoder.encode(data, os);
} catch (FileNotFoundException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to find file to write to disk cache", e);
}
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
// Do nothing.
}
}
}
return success;
}
}

put() 方法接收两个参数,Key 和 Writer,实际上接收的是 SourceWriter 实例,其内部封装了一个 Encoder。put() 方法内部也是先生成字符串 safeKey,然后通过 DiskLruCache 实例的 edit() 方法来获取一个 Editor 实例,进而获取一个缓存文件。然后调用 Writer 的 write() 方法,write() 方法内部将数据写入缓存文件的 OutputStream 中。数据写入完成后调用 Editor 的 commit() 方法来刷新数据。

看来 put() 和 get() 方法的逻辑都比较简单,都是通过 Key 生成字符串,然后通过字符串获取对应的缓存文件,然后使 ResourceDecoder 或者 Encoder 来读取或者写入数据。那么我们来看看 DiskLruCache 是怎么管理缓存文件的。

DiskLruCache

DiskLruCache 表示磁盘缓存,它管理了多个缓存文件,每个 Key 对应一个或多个缓存文件,运行时相关信息保存在一个 LinkedHashMap 中,LinkedHashMap 以 String 为键,以一个 Entry 对象为值。当容量过大时使用 LinkedHashMap 的 LRU 算法来移除 Entry。

Entry 用来表示一个缓存实体,封装了一些相关信息。我们看看它的构造函数

private Entry(String key) {
this.key = key;
///valueCount默认是1
this.lengths = new long[valueCount];
cleanFiles = new File[valueCount];
dirtyFiles = new File[valueCount];

// The names are repetitive so re-use the same builder to avoid allocations.
StringBuilder fileBuilder = new StringBuilder(key).append('.');
int truncateTo = fileBuilder.length();
for (int i = 0; i < valueCount; i++) {
fileBuilder.append(i);
///cleanFiles用key.1命名
cleanFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.append(".tmp");
///dirtyFiles用key.1.tmp命名
dirtyFiles[i] = new File(directory, fileBuilder.toString());
fileBuilder.setLength(truncateTo);
}
}

可见 Entry 封装了 Key,对应的缓存文件和文件大小等信息。一个缓存实体对应两个缓存文件,cleanFile 文件中的数据时刻是干净可读的,dirtyFile 的作用是作为临时文件,当需要写入缓存时,返回的是 dirtyFile,等调用 commit() 方法时再更新数据。这样缓存的写入时也不会影响缓存的读取了。dirtyFile 用 key.1命名,dirtyFiles 用 key.1.tmp 命名,知道缓存 Key 就知道对应的缓存文件了。

缓存构建

磁盘缓存与内存缓存不同,内存缓存在程序每次重新启动时都是空白的全新的状态,需要一个预热的过程,等缓存写入一定量的时候才能开始发挥作用,而磁盘缓存则不能受程序重新启动的影响,程序重新启动时,要能够自动恢复到上次运行的状态,每个 Key 对应哪个缓存文件、每个缓存的大小、哪些缓存文件的数据是干净的、哪些缓存文件的数据是不正常的写入需要删除、当前磁盘缓存的大小等等,都需要实时管理好。这就需要有一个文件实时记录下磁盘缓存的相关信息,以便能够随时恢复。DiskLruCache 就用到了这样一个文件,文件名为 journal。我们先来看看该文件的格式。

libcore.io.DiskLruCache
1
100
1

CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

这是典型的 journal 文件内容。文件的前5行是文件头

  • 第一行 固定为 libcore.io.DiskLruCache
  • 第二行 版本号,源码中定为1
  • 第三行 应用版本号,由 DiskLruCache 的使用者指定
  • 第四行 valueCount,每个 Key 对应几个文件,源码中定为1
  • 第五行 空行

第6行开始记录的是磁盘缓存的操作记录,每一行的格式为操作类型+Key+可选数据项。数据项之间用空格分隔,如果操作类型是 CLEAN,那么可选数据项是数字,描述缓存文件的大小。下面介绍一下四种操作类型

  • DIRTY 表示一个 Entry 被创建或正在写入,一个合法的 DIRTY 操作后面必须接着 CLEAN 操作或者 REMOVE 操作,否则表示该文件只是临时文件,应该被删除。
  • CLEAN 表示一个 Entry 被成功写入,可以被读取。CLEAN 行后面应该接着 valueCount 个数字表示每个缓存文件的大小
  • READ 表示一个 Entry 被读取
  • REMOVE 表示一个 Entry 被删除

journal 文件记录了每一个对磁盘缓存的操作,通过读取这个文件,程序就能还原出磁盘缓存的状态了。下面来看看磁盘缓存具体的构建过程。

  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}

// If a bkp file exists, use it instead.
///判断是否有journal备份文件,如果有则进一步判断有无journal源文件,有则删除备份文件,无则将备份文件改名为源文件
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}

// Prefer to pick up where we left off.
///有journal源文件则从该文件中加载到新的DiskLruCache
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}

// Create a new empty cache.
///既没有备份文件也没有原journal文件,新建一个
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}

DiskLruCache 在 open() 方法中构建。构建 DiskLruCache 需要指定目录、应用版本、每个 Key 的缓存文件个数、磁盘缓存的最大容量。

程序首先判断是否有 journal 备份文件(备份文件命名为 journal.bkp),如果有的话则进一步判断有无 journal 原文件,有则删除备份文件,无则将备份文件改名为原文件。

接下来再次判断 journal 文件是否存在

  • journal 文件存在

    如果 journal 文件存在则调用 readJournal() 和 processJournal() 方法。先来看 readJournal()

    private void readJournal() throws IOException {
    StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
    try {
    ///校验文件头
    String magic = reader.readLine();
    String version = reader.readLine();
    String appVersionString = reader.readLine();
    String valueCountString = reader.readLine();
    String blank = reader.readLine();
    if (!MAGIC.equals(magic)
    || !VERSION_1.equals(version)
    || !Integer.toString(appVersion).equals(appVersionString)
    || !Integer.toString(valueCount).equals(valueCountString)
    || !"".equals(blank)) {
    throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
    + valueCountString + ", " + blank + "]");
    }

    int lineCount = 0;
    while (true) {
    try {
    ///处理一行
    readJournalLine(reader.readLine());
    lineCount++;
    } catch (EOFException endOfJournal) {
    break;
    }
    }

    redundantOpCount = lineCount - lruEntries.size();

    // If we ended on a truncated line, rebuild the journal before appending to it.
    if (reader.hasUnterminatedLine()) {
    rebuildJournal();
    } else {
    journalWriter = new BufferedWriter(new OutputStreamWriter(
    new FileOutputStream(journalFile, true), Util.US_ASCII));
    }
    } finally {
    Util.closeQuietly(reader);
    }
    }

    首先校验文件头,然后循环调用 readJournalLine() 方法处理每一行

    private void readJournalLine(String line) throws IOException {
    int firstSpace = line.indexOf(' ');
    if (firstSpace == -1) {
    throw new IOException("unexpected journal line: " + line);
    }

    ///从行中读取key 格式 REMOVE|CLEAN|DIRTY key 文件大小(知道了key也就知道了key对应的cleanFile和dirtyFile)
    int keyBegin = firstSpace + 1;
    int secondSpace = line.indexOf(' ', keyBegin);
    final String key;
    if (secondSpace == -1) {
    key = line.substring(keyBegin);
    ///处理 remove
    if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
    lruEntries.remove(key);
    return;
    }
    } else {
    key = line.substring(keyBegin, secondSpace);
    }

    Entry entry = lruEntries.get(key);
    if (entry == null) {
    entry = new Entry(key);
    lruEntries.put(key, entry);
    }

    ///如果是clean,那么readable就为true
    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
    String[] parts = line.substring(secondSpace + 1).split(" ");
    entry.readable = true;
    entry.currentEditor = null;
    entry.setLengths(parts);
    } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
    entry.currentEditor = new Editor(entry);
    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
    // This work was already done by calling lruEntries.get().
    } else {
    throw new IOException("unexpected journal line: " + line);
    }
    }

    先取出行中的 Key,如果是 REMOVE 操作,则将 Entry 从 LinkedHashMap 中移除。一个 Entry 表示一个缓存实体,封装了 Key、对应的缓存文件、文件大小 等信息。

    如果 Key 对应的 Entry 不存在,则创建一个,然后放入 LinkedHashMap 中。最后根据操作类型 CLEAN 还是 DIRTY,进行一些属性初始化。如果 Entry 的 currentEditor 属性不为空,表示当前正在写入。

    readJournalLine() 主要就是读取 journal 文件按照操作记录来还原了 LinkedHashMap。但是仔细分析可以发现它还没处理好那些单独出现的 DIRTY 记录,这些 Entry 应在被删除。那么我们接下来看看 processJournal() 方法。

    private void processJournal() throws IOException {
    deleteIfExists(journalFileTmp);
    for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
    Entry entry = i.next();
    if (entry.currentEditor == null) {
    for (int t = 0; t < valueCount; t++) {
    size += entry.lengths[t];
    }
    } else {
    entry.currentEditor = null;
    for (int t = 0; t < valueCount; t++) {
    deleteIfExists(entry.getCleanFile(t));
    deleteIfExists(entry.getDirtyFile(t));
    }
    i.remove();
    }
    }
    }

processJournal() 果然对这部分 Entry 做了清理,判断依据是如果是单独出现的 DIRTY 记录,那么该 Entry 的 currentEditor 必定是不为空的。同时 processJournal() 也统计了当前磁盘磁盘缓存的大小。

那么 journal 文件存在的情况就分析完了。

  • journal 文件不存在

    如果文件不存在,则创建目录,新建 DiskLryCache ,调用 rebuildJournal() 方法生成 journal 文件。

    private synchronized void rebuildJournal() throws IOException {
    if (journalWriter != null) {
    journalWriter.close();
    }

    Writer writer = new BufferedWriter(
    new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
    try {
    writer.write(MAGIC);
    writer.write("\n");
    writer.write(VERSION_1);
    writer.write("\n");
    writer.write(Integer.toString(appVersion));
    writer.write("\n");
    writer.write(Integer.toString(valueCount));
    writer.write("\n");
    writer.write("\n");

    for (Entry entry : lruEntries.values()) {
    if (entry.currentEditor != null) {
    writer.write(DIRTY + ' ' + entry.key + '\n');
    } else {
    writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
    }
    }
    } finally {
    writer.close();
    }

    if (journalFile.exists()) {
    renameTo(journalFile, journalFileBackup, true);
    }
    renameTo(journalFileTmp, journalFile, false);
    journalFileBackup.delete();

    journalWriter = new BufferedWriter(
    new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
    }

首先创建 journal.tmp 文件,写入文件头,然后将临时文件重新命名为 journal 原文件。

那么 DiskLruCache 磁盘缓存的构建就分析完毕了,主要是当操作日志文件 journal 文件存在时,通过读取文件来还原 LinkHashMap,同时统计当前磁盘缓存的大小。

缓存写入

需要写入缓存时,通常先调用 edit() 方法获取一个 Editor 对象,通过该对象的 getFile() 方法获取一个文件以供写入缓存,写入成功后 调用 Editor 对象的 commit() 方法来提交写入的数据。

public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Value is stale.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}

Editor editor = new Editor(entry);
entry.currentEditor = editor;

// Flush the journal before creating files to prevent file leaks.
journalWriter.append(DIRTY);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
journalWriter.flush();
return editor;
}

edit() 方法首先通过 key 在 LinkedHashMap 中获取 Entry,如果 Entry 为空则创建一个放入 LinkedHashMap 中。然后创建一个 Editor 对象并设置到 Entry 的 currentEditor 属性中。最后在 journal 新增一条 DIRTY 记录,表示该 key 的缓存文件正在被写入。

接下来看看 Editor 的 getFile() 方法

public File getFile(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
if (!entry.readable) {
written[index] = true;
}
File dirtyFile = entry.getDirtyFile(index);
if (!directory.exists()) {
directory.mkdirs();
}
return dirtyFile;
}
}

getFile() 方法返回 Entry 对应的 dirtyFile。

向 Editor 返回的文件写入缓存数据后,需要调用 commit() 方法提交数据,否则 dirtyFile 中的数据将被视为非法写入而被删除。

  public void commit() throws IOException {
// The object using this Editor must catch and handle any errors
// during the write. If there is an error and they call commit
// anyway, we will assume whatever they managed to write was valid.
// Normally they should call abort.
completeEdit(this, true);
committed = true;
}

private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}

// If this edit is creating the entry for the first time, every index must have a value.
///如果这个记录以前有值,则readable为TRUE
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!editor.written[i]) {
editor.abort();
throw new IllegalStateException("Newly created entry didn't create value for index " + i);
}
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
return;
}
}
}

//将dirty重命名为clean
for (int i = 0; i < valueCount; i++) {
File dirty = entry.getDirtyFile(i);
if (success) {
if (dirty.exists()) {
File clean = entry.getCleanFile(i);
dirty.renameTo(clean);
long oldLength = entry.lengths[i];
long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
deleteIfExists(dirty);
}
}

redundantOpCount++;
entry.currentEditor = null;
///在journal文件中添加一条记录
if (entry.readable | success) {
entry.readable = true;
journalWriter.append(CLEAN);
journalWriter.append(' ');
journalWriter.append(entry.key);
journalWriter.append(entry.getLengths());
journalWriter.append('\n');

if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.append(REMOVE);
journalWriter.append(' ');
journalWriter.append(entry.key);
journalWriter.append('\n');
}
journalWriter.flush();

if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}

commit() 方法内部又调用了 completeEdit() 方法。completeEdit() 方法主要将 dirtyFile 重命名为 cleanFile,重新计算缓存大小,然后在 journal 文件中增加一条 CLEAN 记录。

redundantOpCount 变量用来记录磁盘缓存的操作次数,当操作次数大于某个值时,就会根据 LinkedHashMap 重新创建 journal 文件,以防止 journal 文件过大。

缓存读取

读取缓存时调用 get() 方法即可,它返回一个 Value 对象,表示 Entry 的一个快照。

public synchronized Value get(String key) throws IOException {
checkNotClosed();
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);
}

主要是用过 key 获取 Entry 的 cleanFile,封装为 Value 对象返回,同时在 journal 文件中新增一条 READ 记录。

BitmapPool

虽然有了图片内存缓存程序在一般情况都都能工作得很好,不会出现大的问题,但是内存缓存的容量总是有限度的,不能无限制地增长,所以程序还是会周期性的申请和释放内存。特别是在一个长列表快速滑动时,会造成大量的图片申请和释放,导致 GC 频繁,进而产生卡顿。

应用运行时图片内存往往占相当大的一部分,如果这部分内存能够尽量的复用,就能够显著地减少内存的频繁申请和释放了。基于这个考虑,Glide 针对 Bitmap 设计了复用方案,这就是 BitmapPool。

Glide 构建了一个 BitmapPool,图片的 Bitmap 的申请和释放都需要通过它来处理。需要加载新的图片时,先从 BitmapPool 中查找有没有相应大小或者稍大一点的 Bitmap,有则直接使用,没有再创建新的 Bimap。一个长列表中的图片往往是大小相同的,所以这个复用率还是相当可观的。

BitmapPool 的最大容量与屏幕分辨率有关,默认是 宽度 Pixels*高度 Piexls*ARGB_8888图片的质量参数*4,这样至少足够缓存四个屏幕大小的图片。容量到达阈值时,使用 LRU 算法从最近最少使用的图片尺寸中移除图片。

复用策略

BitmapPool 使用策略模式来封装不同的复用策略,策略接口是 LruPoolStrategy,定义了 put()、get()、getSize() 等方法。Glide 中有两种复用策略

  • AttributeStrategy 复用的图片需要图片的尺寸和 Bitmap.Config 完全一致
  • SizeConfigStrategy 复用要求相对宽松,复用的图片需要 Bitmap.Config 一致,但复用图片的所需内存比原图小即可。确保 Bitmap.Config 一致后,如果有内存大小一致的图片则直接复用,没有则选取内存稍大一点的图片。需要 KITKAT 以上版本

Glide 在 KITKAT 以上系统中采用 SizeConfigStrategy,否则采用 AttributeStrategy。显然采取 SizeConfigStrategy 的复用率更高。之所以有这两者的区分,跟 Bitmap 的 reconfigure() 方法有关,BitmapPool 能够复用旧图片的内存得益于这个方法,这个方法是在 KITKAT(API 19) 增加的。该方法的注释如下

/**
* <p>Modifies the bitmap to have a specified width, height, and {@link
* Config}, without affecting the underlying allocation backing the bitmap.
* Bitmap pixel data is not re-initialized for the new configuration.</p>
*
* <p>This method can be used to avoid allocating a new bitmap, instead
* reusing an existing bitmap's allocation for a new configuration of equal
* or lesser size. If the Bitmap's allocation isn't large enough to support
* the new configuration, an IllegalArgumentException will be thrown and the
* bitmap will not be modified.</p>
*
* <p>The result of {@link #getByteCount()} will reflect the new configuration,
* while {@link #getAllocationByteCount()} will reflect that of the initial
* configuration.</p>
*
* <p>WARNING: This method should NOT be called on a bitmap currently used
* by the view system. It does not make guarantees about how the underlying
* pixel buffer is remapped to the new config, just that the allocation is
* reused. Additionally, the view system does not account for bitmap
* properties being modifying during use, e.g. while attached to
* drawables.</p>
*
* @see #setWidth(int)
* @see #setHeight(int)
* @see #setConfig(Config)
*/
public void reconfigure(int width, int height, Config config) {
checkRecycled("Can't call reconfigure() on a recycled bitmap");
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("width and height must be > 0");
}
if (!isMutable()) {
throw new IllegalStateException("only mutable bitmaps may be reconfigured");
}
if (mBuffer == null) {
throw new IllegalStateException("native-backed bitmaps may not be reconfigured");
}

nativeReconfigure(mNativeBitmap, width, height, config.nativeInt, mBuffer.length);
mWidth = width;
mHeight = height;
}

简单来说,reconfigure() 方法能够在不重新初始化 Bitmap 和不影响底层内存分配的前提下修改 Bitmap 的尺寸和类型。使用该方法能够复用旧 Bitmap 的内存从而避免给新 Bitmap 分配内存。通过复用生成的 Bitmap,新的内存大小可以通过 getByteCount() 方法获得。

由于 Bitmap 是复用的,所以它所映射的底层内存中还是原始图片的数据,所以 BitmapPool 将 Bitmap 返回给使用者前,还需要使用 Bitmap 的 eraseColor(Color.TRANSPARENT) 来擦除旧的数据。

LRU 算法实现

BitmapPool 并没有使用 LruCache 来实现 LRU 功能,其内部使用 LinkedHashMap。而是自定义了一个数据结构 GroupedLinkedMap,其内部使用 HashMap,不同之处是其每个键对应的值是 LinkedEntry 对象,LinkedEntry 对象内部持有一个 ArrryList 来保存一组图片,并且持有 next 和 prev 引用指向其他 LinkedEntry,以构成一个双向链表,通过 makeTail() 和 makeHead() 方法来改变链表头尾的位置,从而实现 LRU 功能,其 LRU 算法针对的是对某尺寸的一组图片,而非某张图片。这样就能通过 Key 访问一组图片了。(其实本人觉得LruCache也可以实现,存的值类型是Arraylis 就好了)

Glide 中的缓存机制算是介绍完了,本文先是从 Glide 的图片加载流程入手,还原出 Glide 缓存的整体设计。然后从 内存缓存、磁盘缓存、BitmapPool 三个方面分别介绍了各自的一些实现细节。由于本人水平有限,对 Glide 缓存机制的理解深度可能有些欠缺,如有发现文章中欠妥的地方欢迎指正。

感谢阅读。

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/591bf2a28d6d8100589c97e9
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞