前言
默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存:
- 活动资源 (Active Resources) - 现在是否有另一个 View 正在展示这张图片?
- 内存缓存 (Memory cache) - 该图片是否最近被加载过并仍存在于内存中?
- 资源类型(Resource) - 该图片是否之前曾被解码、转换并写入过磁盘缓存?
- 数据来源 (Data) - 构建这个图片的资源是否之前曾被写入过文件缓存?
前两步检查图片是否在内存中,如果是则直接返回图片。后两步则检查图片是否在磁盘上,以便快速但异步地返回图片。
如果四个步骤都未能找到图片,则Glide会返回到原始资源以取回数据(原始文件,Uri, Url等)。
缓存 Key
既然是缓存功能,就必然会有用于进行缓存的 Key。那么 Glide 的缓存 Key 是怎么生成的呢?
Glide 的缓存 Key 生成规则非常繁琐,决定缓存Key的参数竟然有 8 个。不过繁琐归繁琐,至少逻辑还是比较简单的,我们先来看一下 Glide 缓存 Key 的生成逻辑。
生成缓存 Key 的代码在 Engine 类的 load() 方法当中,来看一下:
public class Engine
implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
...
public LoadStatus load(
GlideContext glideContext,
Object model,
Key signature,
int width,
int height,
Class> resourceClass,
Class transcodeClass,
Priority priority,
DiskCacheStrategy diskCacheStrategy,
Map, Transformation>> transformations,
boolean isTransformationRequired,
boolean isScaleOnlyOrNoTransform,
Options options,
boolean isMemoryCacheable,
boolean useUnlimitedSourceExecutorPool,
boolean useAnimationPool,
boolean onlyRetrieveFromCache,
ResourceCallback cb,
Executor callbackExecutor) {
long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
EngineKey key =
keyFactory.buildKey(
model,
signature,
width,
height,
transformations,
resourceClass,
transcodeClass,
options);
EngineResource> memoryResource;
synchronized (this) {
// ⚠️ 1:下面要用到
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
...
}
...
}
这里 model
就是我们要加载的图片的唯一标识,比如说如果是一张网络上的图片的话,那么这个id就是这张图片的url地址。
接下来这行,将这个 model
连同着signature、width、height等等 8 个参数一起传入到 EngineKeyFactory 的 buildKey() 方法当中,从而构建出了一个 EngineKey 对象,这个 EngineKey 也就是 Glide 中的缓存 Key 了。
可见,决定缓存 Key 的条件非常多,即使你用 override() 方法改变了一下图片的 width
或者 height
,也会生成一个完全不同的缓存 Key。
EngineKey 类的源码大家有兴趣可以自己去看一下,其实主要就是重写了 equals()
和 hashCode()
方法,保证只有传入 EngineKey 的所有参数都相同的情况下才认为是同一个 EngineKey 对象。
内存缓存
有了缓存 Key,接下来就可以开始进行缓存了,那么我们先从内存缓存看起。
首先你要知道,默认情况下,Glide 自动就是开启内存缓存的。
也就是说,当我们使用 Glide 加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用 Glide 再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了。
如果你有什么特殊的原因需要禁用内存缓存功能,Glide 对此提供了接口:
Glide.with(this)
.load(url)
.skipMemoryCache(true)
.into(imageView);
可以看到,只需要调用 skipMemoryCache() 方法并传入 true
,就表示禁用掉Glide的内存缓存功能。
没错,关于Glide内存缓存的用法就只有这么多,可以说是相当简单。接下来就让我们就通过阅读源码来分析一下 Glide 的内存缓存功能是如何实现的。
其实说到内存缓存的实现,非常容易就让人想到 LruCache 算法(Least Recently Used),也叫近期最少使用算法。
首先回忆一下,在上一篇文章的第三步 into() 方法中,我们当时分析到了在 开始用引擎load:
方法的时候,说到过,先去缓存去找,如果没有就去走 * waitForExistingOrStartNewJob* 方法
。当时我们没有展开说缓存那块,那么我们现在来看下它的源码,也就是上面 ⚠️1 的那 loadFromMemory 方法,我们看一下代码:
@Nullable
private EngineResource> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}
// 优先加载内存中的活动缓存 - ActiveResources
EngineResource> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
// 如果活动缓存中没有,就加载 LRU 内存缓存中的资源数据。
EngineResource> cached = loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}
可以看到,首先就判断了 isMemoryCacheable
是不是 false,如果是 false 的话就直接返回 null。这是什么意思呢?
其实很简单,我们刚刚不是学了一个 skipMemoryCache() 方法吗?如果在这个方法中传入 true,那么这里的 isMemoryCacheable
就会是 false,表示内存缓存已被禁用 Glide 的图片加载过程中会调用两个方法来获取内存缓存,loadFromActiveResources() 和 loadFromCache() 。
我们来看一下 loadFromActiveResources() 的源码:
@Nullable
private EngineResource> loadFromActiveResources(Key key) {
EngineResource> active = activeResources.get(key);
if (active != null) {
active.acquire();
}
return active;
}
这里调用到了 ActiveResources 类里面的 * get* 方法,代码如下:
final class ActiveResources {
@VisibleForTesting
final Map activeEngineResources = new HashMap<>();
···
@Nullable
synchronized 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;
}
···
@VisibleForTesting
static final class ResourceWeakReference extends WeakReference> {
···
}
可以看到 activeResources
就是一个弱引用的 HashMap,用来缓存正在使用中的图片。我们可以看到,loadFromActiveResources() 方法就是从 activeResources
这个 HashMap 当中取值的。使用 activeResources
来缓存正在使用中的图片,可以保护这些图片不会被 LruCache 算法回收掉。
接下来再看一下 loadFromCache(key) 方法,在这个方法中,会使用缓存 Key 来从 cache 当中取值,而这里的 cache 对象就是在构建 Glide 对象时创建的 LruResourceCache,那么说明这里其实使用的就是 LruCache 算法了。代码如下:
public class Engine
implements EngineJobListener,
MemoryCache.ResourceRemovedListener,
EngineResource.ResourceListener {
···
private final MemoryCache cache;
···
private EngineResource> loadFromCache(Key key) {
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);
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, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}
先看 cache,这个是 MemoryCache ,MemoryCache 代码如下:
/** An interface for adding and removing resources from an in memory cache. */
public interface MemoryCache {
···
可以看到是一个接口,在 GlideBuilder 类中实现如下:
if (memoryCache == null) {
memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}
而 LruResourceCache 从名字上看是实现LRU算法的,进去看一下:
/** An LRU in memory cache for {@link com.bumptech.glide.load.engine.Resource}s. */
public class LruResourceCache extends LruCache> implements MemoryCache {
···
果然是,那内部就是 LinkedHashMap 实现的,我们接着看 getEngineResourceFromCache 方法里面的 remove 方法,LinkedHashMap 的 remove 方法是,存在这个key对应的值,那就删除并且返回,没有的话直接返回空。
为什么 Glide 会弄 2 个内存缓存(一个 Map + 弱引用,一个 LRU 内存缓存?
郭神的回道是这样的:ActiveResources 就是一个弱引用的 HashMap ,用来缓存正在使用中的图片,使用 ActiveResources 来缓存正在使用中的图片,可以保护这些图片不会被 LruCache 算法回收掉。
DevYK的理解是这样的:比如我们 Lru 内存缓存 size 设置装 99 张图片,在滑动 RecycleView 的时候,如果刚刚滑动到 100 张,那么就会回收掉我们已经加载出来的第一张,这个时候如果返回滑动到第一张,会重新判断是否有内存缓存,如果没有就会重新开一个 Request 请求,很明显这里如果清理掉了第一张图片并不是我们要的效果。所以在从内存缓存中拿到资源数据的时候就主动添加到活动资源中,并且清理掉内存缓存中的资源。这么做很显然好处是 保护不想被回收掉的图片不被 LruCache 算法回收掉,充分利用了资源。
好的,从内存缓存中读取数据的逻辑大概就是这些了。概括一下来说,就是如果能从内存缓存当中读取到要加载的图片,那么就直接进行回调,如果读取不到的话,才会开启线程执行后面的图片加载逻辑。
硬盘缓存
调用如下代码开启磁盘缓存:
Glide.with(this)
.load(url)
.diskCacheStrategy(DiskCacheStrategy. RESOURCE)//使用磁盘资源缓存功能
.into(imageView);
这个 diskCacheStrategy() 方法它可以接收五种参数:
- DiskCacheStrategy.NONE: 表示不缓存任何内容。
- DiskCacheStrategy.RESOURCE: 表示只缓存转换过后的图片。
- DiskCacheStrategy.DATA: 表示只缓存原始图片。
- DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。
- DiskCacheStrategy.AUTOMATIC:表示根据数据获取器和编码策略自动选择策略(默认)
默认的策略叫做
AUTOMATIC
,它会尝试对本地和远程图片使用最佳的策略。当你加载远程数据(比如,从URL下载)时,AUTOMATIC
策略仅会存储未被你的加载过程修改过(比如,变换,裁剪–译者注)的原始数据,因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多。对于本地数据,AUTOMATIC
策略则会仅存储变换过的缩略图,因为即使你需要再次生成另一个尺寸或类型的图片,取回原始数据也很容易。
关于 Glide 硬盘缓存的用法也就只有这么多,那么接下来我们通过阅读源码来分析一下,Glide的硬盘缓存功能是如何实现的。
DiskCacheStrategy.RESOURCE 资源类型
获取资源数据
在上一篇文章中,创建 DecodeJob 并且调用 start 方法开始工作:
,当时没有说了不考虑缓存,现在重新看一下缓存这块,代码如下:
class EngineJob implements DecodeJob.Callback, Poolable {
···
public synchronized void start(DecodeJob decodeJob) {
this.decodeJob = decodeJob;
GlideExecutor executor =
decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor();
executor.execute(decodeJob);
}
···
}
可以看到,这里通过 willDecodeFromCache() 方法来判断是否从磁盘缓存中读取数据,
/**
* Returns true if this job will attempt to decode a resource from the disk cache, and false if it
* will always decode from source.
*/
boolean willDecodeFromCache() {
Stage firstStage = getNextStage(Stage.INITIALIZE);
return firstStage == Stage.RESOURCE_CACHE || firstStage == Stage.DATA_CACHE;
}
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:
// Skip loading from source if the user opted to only retrieve the resource from cache.
return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
case SOURCE:
case FINISHED:
return Stage.FINISHED;
default:
throw new IllegalArgumentException("Unrecognized stage: " + current);
}
}
从代码可以看到最终返回 true 还是 false 是根据我们选择的 DiskCacheStrategy 模式来决定,上面说到了一共有五种模式,这里看一下具体源码,我们在选择模式的时候,各个值对应的结果是什么,这样,在 getNextStage 方法中逻辑结果了。
public abstract class DiskCacheStrategy {
public static final DiskCacheStrategy ALL =
new DiskCacheStrategy() {
@Override
public boolean isDataCacheable(DataSource dataSource) {
return dataSource == DataSource.REMOTE;
}
@Override
public boolean isResourceCacheable(
boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) {
return dataSource != DataSource.RESOURCE_DISK_CACHE
&& dataSource != DataSource.MEMORY_CACHE;
}
@Override
public boolean decodeCachedResource() {
return true;
}
@Override
public boolean decodeCachedData() {
return true;
}
};
public static final DiskCacheStrategy NONE =
new DiskCacheStrategy() {
@Override
public boolean isDataCacheable(DataSource dataSource) {
return false;
}
@Override
public boolean isResourceCacheable(
boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) {
return false;
}
@Override
public boolean decodeCachedResource() {
return false;
}
@Override
public boolean decodeCachedData() {
return false;
}
};
public static final DiskCacheStrategy DATA =
new DiskCacheStrategy() {
@Override
public boolean isDataCacheable(DataSource dataSource) {
return dataSource != DataSource.DATA_DISK_CACHE && dataSource != DataSource.MEMORY_CACHE;
}
@Override
public boolean isResourceCacheable(
boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) {
return false;
}
@Override
public boolean decodeCachedResource() {
return false;
}
@Override
public boolean decodeCachedData() {
return true;
}
};
public static final DiskCacheStrategy RESOURCE =
new DiskCacheStrategy() {
@Override
public boolean isDataCacheable(DataSource dataSource) {
return false;
}
@Override
public boolean isResourceCacheable(
boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) {
return dataSource != DataSource.RESOURCE_DISK_CACHE
&& dataSource != DataSource.MEMORY_CACHE;
}
@Override
public boolean decodeCachedResource() {
return true;
}
@Override
public boolean decodeCachedData() {
return false;
}
};
public static final DiskCacheStrategy AUTOMATIC =
new DiskCacheStrategy() {
@Override
public boolean isDataCacheable(DataSource dataSource) {
return dataSource == DataSource.REMOTE;
}
@Override
public boolean isResourceCacheable(
boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy) {
return ((isFromAlternateCacheKey && dataSource == DataSource.DATA_DISK_CACHE)
|| dataSource == DataSource.LOCAL)
&& encodeStrategy == EncodeStrategy.TRANSFORMED;
}
@Override
public boolean decodeCachedResource() {
return true;
}
@Override
public boolean decodeCachedData() {
return true;
}
};
public abstract boolean isDataCacheable(DataSource dataSource);
public abstract boolean isResourceCacheable(
boolean isFromAlternateCacheKey, DataSource dataSource, EncodeStrategy encodeStrategy);
public abstract boolean decodeCachedResource();
public abstract boolean decodeCachedData();
}
看到这个类,应该就很清楚了。那么我们默认是 decodeJob.willDecodeFromCache() 默认是 true 来分析磁盘缓存里面的逻辑,看过上一期就可以知道 decodeJob 其实就是 runnable,执行 execute 方法其实是走到 DecodeJob 的 run() 方法中:
@Override
public void run() {
···
// 1. 执行runWrapped
runWrapped();
···
}
上一期分析过这个方法了,我们直接看重点,* runWrapped();* 的源码:
private void runWrapped() {
switch (runReason) {
case INITIALIZE:
// 2.找到执行的状态
stage = getNextStage(Stage.INITIALIZE);
// 3.找到具体执行器
currentGenerator = getNextGenerator();
// 4.开始执行
runGenerators();
break;
case SWITCH_TO_SOURCE_SERVICE:
runGenerators();
break;
case DECODE_DATA:
decodeFromRetrievedData();
break;
default:
throw new IllegalStateException("Unrecognized run reason: " + runReason);
}
}
上面分析 willDecodeFromCache() 方法的时候,有⚠️的地方我们知道 Stage.INITIALIZE,所以我们走到 case INITIALIZE:
,看一下getNextGenerator 方法的代码:
private DataFetcherGenerator getNextGenerator() {
switch (stage) {
case RESOURCE_CACHE: // 3.1解码后的资源执行器
return new ResourceCacheGenerator(decodeHelper, this);
case DATA_CACHE: // 原始数据执行器
return new DataCacheGenerator(decodeHelper, this);
case SOURCE: // 新的请求,http 执行器
return new SourceGenerator(decodeHelper, this);
case FINISHED:
return null;
default:
throw new IllegalStateException("Unrecognized stage: " + stage);
}
}
通过上面代码可知,主要是执行 currentGenerator.startNext() 就句代码,currentGerator 是一个接口,通过注释 3.1 我们知道这里它的实现类是 ResourceCacheGenerator ,那么我们具体看下 ResourceCacheGenerator 的 startNext 函数;
class ResourceCacheGenerator implements DataFetcherGenerator,
DataFetcher.DataCallback
通过上面注释可以得到几点信息
- 首先根据 资源 ID 等一些信息拿到资源缓存 Key
- 通过 key 拿到缓存文件
- 构建一个 ByteBufferFetcher 加载缓存文件
- 加载完成之后回调到 DecodeJob 中。
存储资源数据
先来看下面一段代码:
class DecodeJob implements DataFetcherGenerator.FetcherReadyCallback,
Runnable,
Comparable>,
Poolable {
...
private void notifyEncodeAndRelease(Resource resource, DataSource dataSource) {
....
stage = Stage.ENCODE;
try {
//1. 是否可以将转换后的图片缓存
if (deferredEncodeManager.hasResourceToEncode()) {
//1.1 缓存入口
deferredEncodeManager.encode(diskCacheProvider, options);
}
} finally {
...
}
onEncodeComplete();
}
}
void encode(DiskCacheProvider diskCacheProvider, Options options) {
GlideTrace.beginSection("DecodeJob.encode");
try {
//1.2 将 Bitmap 缓存到资源磁盘
diskCacheProvider.getDiskCache().put(key,
new DataCacheWriter<>(encoder, toEncode, options));
} finally {
toEncode.unlock();
GlideTrace.endSection();
}
}
通过上面我们知道 http 请求到图片输入流之后经过一系列处理,转换得到目标 Bitmap 资源,最后通过回调到 DecodeJob 进行缓存起来。
清理资源缓存
- 用户主动通过系统来清理
- 卸载软件
- 调用 DisCache.clear();
DiskCacheStrategy.DATA 原始数据类型
获取原始数据
参考上小节 DiskCacheStrategy.RESOURCE 获取资源,不同的是把 ResourceCacheGenerator 换成 DataCacheGenerator 加载了。
存储原始数据
这里既然存的是原始数据那么我们直接从 http 请求之后的响应数据开始查看,通过上一篇我们知道是在 HttpUrlFetcher 中请求网络,直接定位到目的地:
public class HttpUrlFetcher implements DataFetcher {
@Override
public void loadData(@NonNull Priority priority,
@NonNull DataCallback super InputStream> callback) {
long startTime = LogTime.getLogTime();
try {
//1. 通过 loadDataWithRedirects 来进行http 请求,返回 InputStream
InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());
//2. 将请求之后的数据返回出去
callback.onDataReady(result);
} catch (IOException e) {
...
} finally {
...
}
}
}
根据注释可以得知,这里主要用于网络请求,请求响应数据回调给 MultiModelLoader 中。我们看下 它具体实现:
class MultiModelLoader implements ModelLoader {
...
@Override
public void onDataReady(@Nullable Data data) {
//如果数据不为空,那么就回调给 SourceGenerator
if (data != null) {
callback.onDataReady(data);
} else {
startNextOrFail();
}
}
....
}
这里的 callback 指的是 SourceGenerator ,继续跟
class SourceGenerator implements DataFetcherGenerator,
DataFetcher.DataCallback
通过上面注释可以知道 cb.reschedule(); 最后回调到 EngineJob 类,会执行 reschedule(DecodeJob> job) 函数的 getActiveSourceExecutor().execute(job); 用线程池执行任务,最后又回到了 DecodeJob 的 run 函数 拿到执行器DataCacheGenerator ,最终会在 SourceGenerator 的 startNext() 函数,之前流程代码我就不贴了,上面讲了很多次了,相信大家应该记得了,我们直接看 startNext() 函数吧:
class SourceGenerator implements DataFetcherGenerator,
DataFetcher.DataCallback
通过上面代码得知,这里将原始数据写入文件中了。
清理资源缓存
- 用户主动通过系统来清理
- 卸载软件
- 调用 DisCache.clear();
磁盘缓存小节
- 资源缓存是在把图片转换完之后才缓存;
- 原始数据是网络请求成功之后就写入缓存;
参考文献:
DevYK
郭霖
申明:开始结束的图片来源网上,侵删