Android 三级缓存 郭霖,开源框架 | Glide 的三级缓存

说到缓存,都会想到内存缓存 LruCache 和磁盘缓存 DiskLruCache,两者都是基于 LRU(Lest Resently Used)算法并使用 LinkedHashMap 实现的,不同的是前者是保存在内存中,后者是保存在磁盘文件中。Glide也不例外照样使用了这两种缓存,本文不讲 LruCache 和 DiskLruCache 具体的实现原理,从写入和读取缓存的角度解析 Glide 的缓存策略。Glide 默认是使用内存缓存,如果要使用磁盘缓存或者屏蔽内存缓存可以如下设置:

RequestOptions options = new RequestOptions()

.skipMemoryCache(true) //屏蔽内存缓存

.diskCacheStrategy(DiskCacheStrategy.ALL); //使用磁盘缓存

Glide.with(fragment)

.load(url)

.apply(options)

.into(imageView);

DiskCacheStrategy 的4个抽象方法:

public abstract boolean isDataCacheable(DataSource dataSource);

public abstract boolean isResourceCacheable(boolean isFromAlternateCacheKey,

DataSource dataSource, EncodeStrategy encodeStrategy);

public abstract boolean decodeCachedResource();

public abstract boolean decodeCachedData();

isDataCacheable()

返回true,表示缓存原始的没有进行修改过的图片;

isResourceCacheable()

返回true,表示缓存最终转化的图片;

decodeCacheResource()

返回ture,表示解码缓存的最终转化的图片;

decodeCacheData()

返回true,表示解码缓存的没有进行修改过的图片;

DiskCacheStrategy 有5种缓存类型:

ALL

远程图片资源使用 DATA 和 RESOURCE 缓存,本地图片只使用 RESOURCE 缓存;

NONE

不缓存任何图片资源;

DATA

将检索到的原始图片资源(解码之前的)写入磁盘缓存;

RESOURCE

将检索到的原始图片资源解码之后(压缩或转换)再写入磁盘缓存;

AUTOMATIC

根据图片源自动选取缓存策略,数据源可以是:DataFetcher、EncodeStrategy、ResourceEncoder;

1. 内存缓存

Glide 的内存缓存中有两级,一部分是弱引用缓存,一部分是 LruCache,弱引用缓存使用 WeakReference 修饰引用的图片,用于缓存正在使用中的图片;LruCache 就是常见的内存缓存,保存当前应用使用过但不是正在使用中的图片。

1.1 缓存读取

前面分析图片请求流程时,在 Egine.load() 方法内忽略了缓存部分,回到里面:

EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,

resourceClass, transcodeClass, options);

EngineResource> active = loadFromActiveResources(key, isMemoryCacheable);

if (active != null) {

cb.onResourceReady(active, DataSource.MEMORY_CACHE);

if (VERBOSE_IS_LOGGABLE) {

logWithTimeAndKey("Loaded resource from active resources", startTime, key);

}

return null;

}

EngineResource> cached = loadFromCache(key, isMemoryCacheable);

if (cached != null) {

cb.onResourceReady(cached, DataSource.MEMORY_CACHE);

if (VERBOSE_IS_LOGGABLE) {

logWithTimeAndKey("Loaded resource from cache", startTime, key);

}

return null;

}

图片进行网络请求前首先会调用 loadFromActiveResources() 获取应用正在使用的资源,资源是保存在一个弱引用(WeakReference)的 HashMap 里:

如果获取到的资源为空,接着调用 loadFromCache() 从内存缓存 LruCache 中获取并移除,然后保存在前面正在使用的弱引用的 HashMap 中,LruCache 的原理是使用一个 LinkedHashMap 对请求过的图片进行保存。

两种情况下获取到的资源不为空时都会调用 SingleRequest.onResourceReady() 通知主线程更新UI。

首先来看 loadFromActiveResources(),这里是从正在使用的图片资源中获取,如果我们请求的图片之前请求过并且正在使用中,那么这个方法就可以拿到这个图片资源:

private EngineResource> loadFromActiveResources(Key key, boolean isMemoryCacheable) {

if (!isMemoryCacheable) {

return null;

}

EngineResource> active = activeResources.get(key);

if (active != null) {

active.acquire();

}

return active;

}

第一步先判断 isMemeoryCacheable,由我们设置 RequestOptions 的 skipMemoryCache(true) 决定,为 true 表示不使用内存缓存 isMemeoryCacheable 就为 false,直接返回 null;为 false 表示使用内存缓存,Glide 默认是使用内存缓存的,此时 isMemeoryCacheable 为 true,接着调用 ActiveResources.get() 方法通过 key 获取图片资源:

final Map activeEngineResources = new HashMap<>();

EngineResource> get(Key key) {

ResourceWeakReference activeRef = activeEngineResources.get(key);

if (activeRef == null) {

return null;

}

EngineResource> active = activeRef.get();

if (active == null) {

cleanupActiveReference(activeRef);

}

return active;

}

拿到图片的 key 从 activeEngineResources 中获取一个弱引用的对象,activeEngineResources 是一个 HashMap,保存的是我们正在使用的图片资源,由于是弱引用的对象,只要系统发起 GC 操作,这些对象都会被回收掉。这就是弱引用缓存,一方面提高了请求效率,另一方面也避免了应用由于图片资源过大导致的内存溢出问题。

如果弱引用缓存中没有我们想要请求的图片,接着就调用 loadFromCache() 去内存缓存 LruCache 中查找,也就是说需要请求的这个图片是否之前使用过但现在没有使用了,Glide 便将其保存在 LruCache 中:

private EngineResource> loadFromCache(Key key, boolean isMemoryCacheable) {

if (!isMemoryCacheable) {

return null;

}

EngineResource> cached = getEngineResourceFromCache(key);

if (cached != null) {

cached.acquire();

activeResources.activate(key, cached); //缓存正在使用的图片

}

return cached;

}

private EngineResource> getEngineResourceFromCache(Key key) {

Resource> cached = cache.remove(key); //移除并返回 LreCache 中的图片资源

final EngineResource> result;

if (cached == null) {

result = null;

} else if (cached instanceof EngineResource) {

result = (EngineResource>) cached;

} else {

result = new EngineResource<>(cached, true /*isMemoryCacheable*/, true /*isRecyclable*/);

}

return result;

}

重点看 cache.remove(key),cache 就是 LruCache,调用 remove() 获取缓存并移除,拿到资源后如果不为空,由于应用此刻正好需要使用这个资源,首先先将它保存在正在使用的资源中,调用 activeResources.activate(key, cached) 将这个资源保存在我们前面提到的 HashMap 中,最后再返回给 Engine.load();

由此可见,通常情况下 Glide 是有两级缓存的,弱引用缓存和内存缓存,正在使用的图片保存在弱引用的 HashMap 中,使用过但现在不使用的图片保存在 LruCache 的 LinkedHashMap 中,两者之间存在交互的,如果现在请求的图片存在于 LreCache 中,Glide 会将这张图片从 LruCache 中移除并保存在弱引用缓存 activeResources 中,如果正在使用的图片现在不使用了(图片的引用计数为0)Glide 又会将这张图片从 activeResources 中移除并存入 LruCache 中。

Glide 获取一张图片时,首先会从弱引用缓存中获取,没有则从内存缓存 LruCache 中获取,如果有磁盘缓存,接着去磁盘中获取,最后才是通过网络获取。

1.2 缓存写入

前面我们了解了 Glide 在请求一个图片时缓存的获取原理,而缓存又是在什么时候写入的呢?前一章了解到,当我们完成一次图片的请求时,通过 Handler 发送消息给主线程,最终会调用到 EngineJob 的 handleResultOnMainThread() 方法,进去看看都做了什么:

void handleResultOnMainThread() {

...

engineResource = engineResourceFactory.build(resource, isCacheable);

hasResource = true;

engineResource.acquire(); //关注点1

listener.onEngineJobComplete(this, key, engineResource); //关注点2

for (int i = 0, size = cbs.size(); i < size; i++) {

ResourceCallback cb = cbs.get(i);

if (!isInIgnoredCallbacks(cb)) {

engineResource.acquire();

cb.onResourceReady(engineResource, dataSource);

}

}

// Our request is complete, so we can release the resource.

engineResource.release(); //关注点3

release(false /*isRemovedFromQueue*/);

}

先来看关注点2,这里又回到了 Engine 的 onEngineJobComplete() 方法中:

public void onEngineJobComplete(EngineJob> engineJob, Key key, EngineResource> resource) {

Util.assertMainThread();

if (resource != null) {

resource.setResourceListener(key, this);

if (resource.isCacheable()) {

activeResources.activate(key, resource);

}

}

jobs.removeIfCurrent(key, engineJob);

}

void activate(Key key, EngineResource> resource) {

ResourceWeakReference toPut =

new ResourceWeakReference(

key,

resource,

getReferenceQueue(),

isActiveResourceRetentionAllowed);

ResourceWeakReference removed = activeEngineResources.put(key, toPut); //传入弱引用缓存

if (removed != null) {

removed.reset();

}

}

很明显,如果 resource 不为空调用 activeResources.activate(),这个方法就是将我们这里的 resource 存入了弱引用缓存中。

那么内存缓存又是什么时候写入的呢?

回到 handleResultOnMainThread() 里面,关注点1和关注点3分别调用了 engineResource 的 acquire() 和 release() 方法,acquire() 每调用一次引用计数 acquired 加1,release() 方法每调用一次 acquired 减1:

void acquire() {

...

++acquired;

}

void release() {

...

if (--acquired == 0) {

listener.onResourceReleased(key, this);

}

}

引用计数 acquired 表示当前正在使用资源的使用者数,大于0表示资源正在使用中,值为0表示没有使用者使用此刻就需要将它写入内存缓存中,release() 中调用 onResourceReleased() 将没有使用的资源写入内存缓存,仍然又回到了 Engine 中的 onResourceReleased() 方法:

private final MemoryCache cache;

public void onResourceReleased(Key cacheKey, EngineResource> resource) {

Util.assertMainThread();

activeResources.deactivate(cacheKey); //先从弱引用缓存中移除

if (resource.isCacheable()) {

cache.put(cacheKey, resource); //写入内存缓存

} else {

resourceRecycler.recycle(resource);

}

}

由于这个图片当前已没有使用者,调用 activeResources.deactivate() 先把它从弱引用缓存中清除,然后就是将数据写入内存缓存,cache 是 MemoryCache 类型的,MemoryCache 是一个接口类它的实现者就是 LruCache。

由此可见,正在使用的图片使用 activeResources 以弱引用的方式保存起来,Glide 给图片设置了一个引用计数变量 acquired 用于统计图片当前的引用数,acquired 为0即为图片没有使用者,就将图片从弱引用缓存中移除然后保存到 LruCache 中。

2. 磁盘缓存

2.1 磁盘缓存读取

回到之前讲解请求图片流程,DecodeJob 的 run() 方法调用了 runWrapped(),之前只考虑了第一次请求图片的情况,如果是第二次请求图片,进入 runWrapper() 看看是怎么处理的:

private void runWrapped() {

switch (runReason) {

case INITIALIZE:

stage = getNextStage(Stage.INITIALIZE); //关注点1

currentGenerator = getNextGenerator(); //关注点2

runGenerators(); //关注点3

break;

case SWITCH_TO_SOURCE_SERVICE:

runGenerators();

break;

case DECODE_DATA:

decodeFromRetrievedData();

break;

default:

throw new IllegalStateException("Unrecognized run reason: " + runReason);

}

}

还是 INITIALIZE,首先来看关注点1,调用 getNextStage 获取下一步的操作标记:

private Stage getNextStage(Stage current) {

switch (current) {

case INITIALIZE:

return diskCacheStrategy.decodeCachedResource()

? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);

case RESOURCE_CACHE: //磁盘缓存的修改后的图片

return diskCacheStrategy.decodeCachedData()

? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);

case DATA_CACHE: //磁盘缓存的原始图片

return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;

case SOURCE:

case FINISHED:

return Stage.FINISHED;

default:

throw new IllegalArgumentException("Unrecognized stage: " + current);

}

}

当前阶段为:INITIALIZE,首先判断是否需要解码磁盘缓存中经过压缩或者转换的图片,需要则返回 RESOURCE_CACHE,否则判断是否要解码磁盘缓存中原始的图片,即未经过压缩和转换的图片,需要则返回 DATA_CACHE,如果两种都不需要则返回 SOURCE 表示直接请求原始图片,这里具体是使用哪种缓存取决于我们在设置 RequestOptions 的diskCacheStrategy(DiskCacheStrategy.ALL) 时决定;

接着来看关注点2,假设我们通过 getNextStage() 拿到的 stage 为 RESOURCE_CACHE,那么进入 getNextGenerator() 中就会返回一个 ResourceCacheGenerator 对象:

private DataFetcherGenerator getNextGenerator() {

switch (stage) {

case RESOURCE_CACHE:

return new ResourceCacheGenerator(decodeHelper, this); //磁盘缓存中获取修改后的图片

case DATA_CACHE:

return new DataCacheGenerator(decodeHelper, this); //磁盘缓存中获取原始图片

case SOURCE:

return new SourceGenerator(decodeHelper, this); //直接请求图片

case FINISHED:

return null;

default:

throw new IllegalStateException("Unrecognized stage: " + stage);

}

}

ResourceCacheGenerator 对象有什么作用呢?继续来看关注点3,调用 runGenerators(),紧接着调用 currentGenerator.startNext(),这里就是执行这个 ResourceCacheGenerator 的地方,进入 ResourceCacheGenerator 的 startNext():

public boolean startNext() {

...

currentKey =

new ResourceCacheKey

helper.getArrayPool(),

sourceId,

helper.getSignature(),

helper.getWidth(),

helper.getHeight(),

transformation,

resourceClass,

helper.getOptions()); //创建图片资源的key

cacheFile = helper.getDiskCache().get(currentKey); //获取磁盘缓存

... //没有磁盘缓存,

return started;

}

首先为当前图片创建一个用于标识图片唯一性的 key,接着很明显了,调用 helper.getDiskCache().get(currentKey) 就是从我们的磁盘缓存中获取图片资源,由于这里是 ResourceCacheGenerator 的 startNext(),所以获取到的资源是经过压缩或者转换后的图片。

要获取未经过压缩及转换的图片的话如何获取呢?首先需要我们在设置 RequestOptions 的 diskCacheStrategy() 时设置的 DiskCacheStrategy 类型是可以缓存原始图片的(ALL、DATA、AUTOMATIC 都可以缓存原始图片),接着 Glide 内部通过调用 DataCacheGenerator 的 startNext() 方法就能获取到原始的图片。

总结下来,当原始图片缓存和修改后的图片缓存都存在时,首先会获取经过压缩或转换后的图片,然后才去获取原始图片,毕竟原始图片一般比经过处理后的图片大且占据更多内存空间,在使用的时候应避免缓存以及从缓存中获取原始的图片。

2.2 磁盘缓存写入

缓存原始图片

磁盘缓存的写入是在请求图片的时候写入的,在磁盘获取的时候 getNextGenerator() 如果返回的是 SourceGenerator 时,表明需要去请求图片,进入 SourceGenerator 的 startNext() 方法:

public boolean startNext() {

if (dataToCache != null) {

Object data = dataToCache;

dataToCache = null;

cacheData(data); //关键点

}

...

}

在图片请求之前调用了 cacheData() 方法:

private void cacheData(Object dataToCache) {

long startTime = LogTime.getLogTime();

try {

Encoder encoder = helper.getSourceEncoder(dataToCache);

DataCacheWriter writer =

new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());

originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());

helper.getDiskCache().put(originalKey, writer); // 写入磁盘缓存

...

} finally {

loadData.fetcher.cleanup();

}

sourceCacheGenerator =

new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);

}

调用 helper.getDiskCache().put(originalKey, writer) 将数据写入了磁盘缓存,但这个方法是在 dataToCache 不为空时调用的,dataToCache 是在哪里存值的呢?还是 startNext() 方法里,接着调用loadData.fetcher.loadData(),由于我们使用的是从网络获取图片,所以fetcher 就是 HttpUrlFetcher,也就是调用了 HttpUrlFetcher 的 loadData() :

public void loadData(@NonNull Priority priority,

@NonNull DataCallback super InputStream> callback) {

long startTime = LogTime.getLogTime();

try {

InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders()); //进行网络请求

callback.onDataReady(result); //调用 SourceGenerator的onDataReady()方法

}

...

}

从网络请求图片资源,请求完成后将请求结果传给了 SourceGenerator 的 onDataReday():

public void onDataReady(Object data) {

DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();

if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {

dataToCache = data; //关注点1

cb.reschedule();

} else {

cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,

loadData.fetcher.getDataSource(), originalKey); //关注点2

}

}

看关注点1,如果请求返回的数据 data 不为空且需要缓存原始数据,就将 data 赋值给我们刚才提到的 dataToCache,接着调用 cb.reschedule() 会再一次进入到 SourceGenerator 的 startNext() 方法,这个时候 dataToCache 已经不为空就可以写入磁盘缓存了,注意这里是缓存的原始未经过任何修改的图片,如果不需要缓存原始数据,直接调用 DecodeJob.onDataFetcherReady()。

缓存编码(压缩或转换)后的图片

什么时候缓存压缩或转换后的图片呢?cacheData() 方法里面原始图片写入磁盘缓存完成后新建了一个 DataCacheGenerator,然后有如下流程:DataCacheGenerator.startNext() -> HttpUrlFetcher.loadData() -> DataCacheGenerator.onDataReady() -> DecodeJob.onDataFetcherReady() -> decodeFromRetrievedData() -> notifyEncodeAndRelease() -> DeferredEncodeManager.encode(),进入 encode() 方法看看:

void encode(DiskCacheProvider diskCacheProvider, Options options) {

GlideTrace.beginSection("DecodeJob.encode");

try {

diskCacheProvider.getDiskCache().put(key,

new DataCacheWriter<>(encoder, toEncode, options)); //写入磁盘缓存

} finally {

toEncode.unlock();

GlideTrace.endSection();

}

}

代码写的很清楚了,这里就把编码后的数据写入了磁盘缓存中。

3. 总结

到此为此就介绍完了 Glide 的两种缓存的读取和写入原理,需要注意的是内存缓存不仅是 LruCache 还提供了一种弱引用缓存,用于缓存正在使用的图片资源,由于是弱引用的缓存当系统发起 GC 时就会被回收掉,有效避免了内存较低时系统开启 GC 回收部分内存时可能发生的内存泄漏问题,以及由于图片过大导致内存不足时可能引发的内存溢出问题。

假设我们在一开始设置了 Glide 支持磁盘缓存,且原图和编码后(即压缩或转换)的图片都要缓存;

缓存读取顺序:

弱引用缓存 -> LruCache -> DiskLruCache

缓存写入顺序:

DiskLruCache 缓存原图 -> 弱引用缓存 -> LruCache -> DiskLruCache 缓存编码后的图片

注意:

弱引用缓存和 LruCache 之间存在缓存的转换关系,图片从正在使用状态转为不使用状态,Glide 将图片从弱引用缓存移除然后缓存到 LruCache 中,假如 LruCache 中的某张图片现在需要使用,则图片从 LruCache 中移除缓存到弱引用缓存中,弱引用缓存中保存的是正在使用的图片。

4. 参考

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值