| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> Java知识库 -> Glide的缓存源码分析 -> 正文阅读 |
|
[Java知识库]Glide的缓存源码分析 |
Glide的缓存流程上一篇讲解了Glide的整体流程,其实很多时候,只有第一次加载图片的时候,我们才会按照那一个流程去走。因为很多时候,我们都是有缓存了。有了缓存之后,加载流程就会稍微变一下了。那么今天,我们就来讲解一下Glide中的缓存。在讲解Glide缓存之后,我建议大家先去了解一下 先来一张Glide缓存的流程图吧,让大家对Glide的流程有一个印象,方便之后的分析,以下流程图是基于配置了允许缓存的流程,配置了不允许缓存的不在本博客的讨论范围。 Glide缓存流程图通过上面这个流程图,我们可以知道Glide的缓存可以分为三级,第一个是 ActiveResources那么就先简单介绍一个
看一下
接着看一下是如何保存和删除
这上面要分析的是关于
以上大概就是 预备知识讲解完了,接下来就是进入Glide的缓存流程了。还记得上一篇博客,我们讲解缓存的时候是省略了这一部分。 废话不多说了,直接看Engine.load方法
这里面很多关于缓存的代码,首先看 Glide缓存流程分析
|
组成 | 注释 |
---|---|
model | load的参数 |
signature | BaseRequestOptions 的成员变量,默认会是EmptySignature.obtain() 在加载本地resource资源时会变成ApplicationVersionSignature.obtain(context) |
width height | 如果没有指定override(int size) ,那么将得到view的size |
transformations | 默认会基于ImageView 的scaleType设置对应的四个Transformation ; 如果指定了transform ,那么就基于该值进行设置; 详见BaseRequestOptions.transform(Transformation, boolean) |
resourceClass | 解码后的资源,如果没有asBitmap 、asGif ,一般会是Object |
transcodeClass | 最终要转换成的数据类型,根据as 方法确定,加载本地res或者网络URL,都会调用asDrawable ,所以为Drawable |
options | 如果没有设置过transform ,此处会根据ImageView 的scaleType 默认指定一个KV |
那么接下来就是 memoryResource = loadFromMemory(key, isMemoryCacheable, startTime)
这段代码,这段代码也是核心。首先我们看看memoryResource
是什么东西,它是EngineResource
,也就是说,我们从缓存中找到的对象是EngineResource
。在阅读缓存之前,我们首先要了解一下什么是EngineResource
,我们先看一下
class EngineResource<Z> implements Resource<Z> {
private final boolean isMemoryCacheable;
private final boolean isRecyclable;
private final Resource<Z> resource;
private final ResourceListener listener;
private final Key key;
private int acquired;
private boolean isRecycled;
interface ResourceListener {
void onResourceReleased(Key key, EngineResource<?> resource);
}
EngineResource(
Resource<Z> toWrap,
boolean isMemoryCacheable,
boolean isRecyclable,
Key key,
ResourceListener listener) {
resource = Preconditions.checkNotNull(toWrap);
this.isMemoryCacheable = isMemoryCacheable;
this.isRecyclable = isRecyclable;
this.key = key;
this.listener = Preconditions.checkNotNull(listener);
}
Resource<Z> getResource() {
return resource;
}
boolean isMemoryCacheable() {
return isMemoryCacheable;
}
@NonNull
@Override
public Class<Z> getResourceClass() {
return resource.getResourceClass();
}
@NonNull
@Override
public Z get() {
return resource.get();
}
@Override
public int getSize() {
return resource.getSize();
}
@Override
public synchronized void recycle() {
if (acquired > 0) {
throw new IllegalStateException("Cannot recycle a resource while it is still acquired");
}
if (isRecycled) {
throw new IllegalStateException("Cannot recycle a resource that has already been recycled");
}
isRecycled = true;
if (isRecyclable) {
resource.recycle();
}
}
synchronized void acquire() {
if (isRecycled) {
throw new IllegalStateException("Cannot acquire a recycled resource");
}
++acquired;
}
@SuppressWarnings("SynchronizeOnNonFinalField")
void release() {
boolean release = false;
synchronized (this) {
if (acquired <= 0) {
throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
}
if (--acquired == 0) {
release = true;
}
}
if (release) {
//这里实际的作用是将ActiveResource缓存移动到内存缓存
listener.onResourceReleased(key, this);
}
}
}
我们主要关注两个方法,一个acquire,一个release。acquire方法很简单,就是每调用一次这个方法,就给acquired成员变量加一。release也很简单,每次调用都给acquired成员变量减一,当acquired成员变量为0的时候,调用listener.onResourceReleased
。这里采用的算法就是垃圾回收里面最简单的引用计数法去管理EngineResource
。接下来我, 要去看loadFromMemory()
这个方法了
private EngineResource<?> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
//如果不允许缓存,直接返回null
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}
//从ActiveResource中去查找,如果找到,这个资源的引用计数就要加一
@Nullable
private EngineResource<?> loadFromActiveResources(Key key) {
EngineResource<?> active = activeResources.get(key);
if (active != null) {
active.acquire();
}
return active;
}
//从MemoryCache中查找
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) {
//首先将找到key对应的cache,并移除,为什么这么做,我们看上一个函数,如果找到的cache不为null,也就是memoryCache缓存命中了,那么需要将这个cache移动到ActiveResource,所以就要将cache从MemoryCache中移除
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;
}
以上就是在有内存缓存或者弱应用缓存情况下,并且能命中的情况分析了。对应我们上面流程图的红色边框里面的部分。
到这里如果都还加载不到的话,那么就需要去磁盘获取数据了。磁盘读写也是用的LRU算法。但是这个和内存的LRU算法有一点小区别。为什么呢?因为内存缓存是我们运行的时候,程序加载内存里面的资源,可以直接通过一个LinkedHashMap
去实现。但是磁盘不同,我总不可能吧所有磁盘的资源读出来然后加载在内存里面吧,这样的话,肯定会引发oom了。那么Glide是怎么做磁盘的LRU的呢?
Glide 是使用一个日志清单文件来保存这种顺序,DiskLruCache
在 APP 第一次安装时会在缓存文件夹下创建一个 journal 日志文件来记录图片的添加、删除、读取等等操作,后面每次打开 APP 都会读取这个文件,把其中记录下来的缓存文件名读取到 LinkedHashMap
中,后面每次对图片的操作不仅是操作这个 LinkedHashMap
还要记录在 journal 文件中. journal 文件内容如下图:
开头的 libcore.io.DiskLruCache
是魔数,用来标识文件,后面的三个 1 是版本号 valueCount
等等,再往下就是图片的操作日志了。
DIRTY、CLEAN 代表操作类型,除了这两个还有 REMOVE 以及READ,紧接着的一长串字符串是文件的 Key,由 SafeKeyGenerator
类生成,是由图片的宽、高、加密解码器等等生成的 SHA-256 散列码后面的数字是图片大小。
根据这个字符串就可以在同目录下找到对应的图片缓存文件,那么打开缓存文件夹即可看到上面日志中记录的文件:
可以看到日志文件中记录的缓存文件就在这个文件夹下面。
由于涉及到磁盘缓存的外部排序问题,所以相对而言磁盘缓存比较复杂。
磁盘的缓存的前置知识就讲这么多。接下来还是要去看磁盘是怎么进行读写的。在上一篇Glide的流程分析博客中,我们Engine的load方法最终会走到
ResourceCacheGenerator#startNext
方法里面所以,我们直接看这个方法
public boolean startNext() {
//首先这里调用了getCacheKeys(),这里返回的不是空,而是一个GlideUrl结果的列表
List<Key> sourceIds = helper.getCacheKeys();//注意这段代码会将key保存下来,供下面的一些操作使用
if (sourceIds.isEmpty()) {
return false;
}
// 获得了三个可以到达的registeredResourceClasses
// GifDrawable、Bitmap、BitmapDrawable
List<Class<?>> resourceClasses = helper.getRegisteredResourceClasses();
if (resourceClasses.isEmpty()) {
if (File.class.equals(helper.getTranscodeClass())) {
return false;
}
throw new IllegalStateException(
"Failed to find any load path from "
+ helper.getModelClass()
+ " to "
+ helper.getTranscodeClass());
}
while (modelLoaders == null || !hasNextModelLoader()) {
resourceClassIndex++;
if (resourceClassIndex >= resourceClasses.size()) {
sourceIdIndex++;
if (sourceIdIndex >= sourceIds.size()) {
//如果是第一次请求,那么就会在这里返回,后面的就不会去执行了
return false;
}
resourceClassIndex = 0;
}
Key sourceId = sourceIds.get(sourceIdIndex);
Class<?> resourceClass = resourceClasses.get(resourceClassIndex);
Transformation<?> transformation = helper.getTransformation(resourceClass);
// PMD.AvoidInstantiatingObjectsInLoops Each iteration is comparatively expensive anyway,
// we only run until the first one succeeds, the loop runs for only a limited
// number of iterations on the order of 10-20 in the worst case.
//假设有缓存,那么就需要构建一个ResourceCacheKey
currentKey =
new ResourceCacheKey( // NOPMD AvoidInstantiatingObjectsInLoops
helper.getArrayPool(),
sourceId,
helper.getSignature(),
helper.getWidth(),
helper.getHeight(),
transformation,
resourceClass,
helper.getOptions());
//从磁盘中找到key对应的文件
cacheFile = helper.getDiskCache().get(currentKey);
//如果找到对应的文件,那么跳出循环
if (cacheFile != null) {
sourceKey = sourceId;
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
}
//找到缓存文件后,就会执行到这里
loadData = null;
boolean started = false;
while (!started && hasNextModelLoader()) {
ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
loadData =
modelLoader.buildLoadData(
cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
started = true;
loadData.fetcher.loadData(helper.getPriority(), this);
}
}
return started;
}
我们先看怎么从磁盘中去取缓存的。
首先先构造一个ResourceCacheKey
,然后通过 helper.getDiskCache().get()
。这里的getDiskcache
创建的是DiskLruCacheWrapper
,所以get方法最终调用的是DiskLruCacheWrapper
的get方法。这就是获取磁盘缓存的内容了。以上分析完的部分都是读取缓存的部分。那么写缓存的部分在哪里呢?思考一个问题,怎么样才能写缓存?肯定是有数据之后才能写缓存啊。所以我们直接跳到有数据之后,怎么去写缓存。至于怎么获取数据,在上一篇博客中已经讲解的很清楚了,这里就不在重复了。
@Override
public void onDataReady(Object data) {
DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
dataToCache = data;
// We might be being called back on someone else's thread. Before doing anything, we should
// reschedule to get back onto Glide's thread.
cb.reschedule();
} else {
cb.onDataFetcherReady(
loadData.sourceKey,
data,
loadData.fetcher,
loadData.fetcher.getDataSource(),
originalKey);
}
}
我们解读一下onDataReady
里面的代码。首先,获取DiskCacheStrategy
判断能不能被缓存,这里的判断代码在SourceGenerator.startNext()
中出现过,显然是可以的。然后将data保存到dataToCache
,并调用cb.reschedule()
。
我们在前面分析过,该方法的作用就是将DecodeJob
提交到Glide的source线程池中。然后执行DecodeJob.run()
方法,经过runWrapped()
、 runGenerators()
方法后,又回到了SourceGenerator.startNext()
方法。
在方法的开头,会判断dataToCache
是否为空,此时显然不为空,所以会调用cacheData(Object)
方法进行data的缓存处理。缓存完毕后,会为该缓存文件生成一个SourceCacheGenerator
。然后在startNext()
方法中会直接调用该变量进行加载。
//#SourceGenerator.java
public boolean startNext() {
//现在是第二次执行到这里
if (dataToCache != null) {
Object data = dataToCache;
dataToCache = null;
cacheData(data);
}
if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
return true;
}
//省略代码
}
private void cacheData(Object dataToCache) {
long startTime = LogTime.getLogTime();
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();
}
sourceCacheGenerator =
new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
}
现在我们缓存文件也写完了,那么就会回调DataCacheGenerator的startNext方法
public boolean startNext() {
while (modelLoaders == null || !hasNextModelLoader()) {
sourceIdIndex++;
if (sourceIdIndex >= cacheKeys.size()) {
return false;
}
Key sourceId = cacheKeys.get(sourceIdIndex);
// PMD.AvoidInstantiatingObjectsInLoops The loop iterates a limited number of times
// and the actions it performs are much more expensive than a single allocation.
@SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
Key originalKey = new DataCacheKey(sourceId, helper.getSignature());//获取缓存的原始资源文件,这时候获取到的就不会为空了
cacheFile = helper.getDiskCache().get(originalKey);
if (cacheFile != null) {
this.sourceKey = sourceId;
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
}
loadData = null;
boolean started = false;
while (!started && hasNextModelLoader()) {
ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
loadData =
modelLoader.buildLoadData(
cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
started = true;
loadData.fetcher.loadData(helper.getPriority(), this);//最终会回调到SourceGenerator#onDataFetcherReady
}
}
return started;
}
我们得到数据之后需要进行一定的解码之后才会缓存,这一段代码在哪里呢?在DecodeJob.onResourceDecoded
,在解码完成之后,我们需要进行磁盘的缓存。注意上一步有一个缓存的操作写的是原始的数据。现在我们需要的是解码之后的数据。
<Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
@SuppressWarnings("unchecked")
Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass();
Transformation<Z> appliedTransformation = null;
Resource<Z> transformed = decoded;
if (dataSource != DataSource.RESOURCE_DISK_CACHE) {
appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
transformed = appliedTransformation.transform(glideContext, decoded, width, height);
}
// TODO: Make this the responsibility of the Transformation.
if (!decoded.equals(transformed)) {
decoded.recycle();
}
final EncodeStrategy encodeStrategy;
final ResourceEncoder<Z> encoder;
if (decodeHelper.isResourceEncoderAvailable(transformed)) {
// encoder就是BitmapEncoder
encoder = decodeHelper.getResultEncoder(transformed);
encodeStrategy = encoder.getEncodeStrategy(options);
} else {
encoder = null;
encodeStrategy = EncodeStrategy.NONE;
}
Resource<Z> result = transformed;
boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
if (diskCacheStrategy.isResourceCacheable(
isFromAlternateCacheKey, dataSource, encodeStrategy)) {
if (encoder == null) {
throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());
}
final Key key;
switch (encodeStrategy) {
case SOURCE:
key = new DataCacheKey(currentSourceKey, signature);
break;
case TRANSFORMED:
key =
new ResourceCacheKey(
decodeHelper.getArrayPool(),
currentSourceKey,
signature,
width,
height,
appliedTransformation,
resourceSubClass,
options);
break;
default:
throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy);
}
LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
deferredEncodeManager.init(key, encoder, lockedResult);
result = lockedResult;
}
return result;
}
从上面的源码中可以看到,当dataSource != DataSource.RESOURCE_DISK_CACHE
时会进行transform。哦~,这是因为resource cache肯定已经经历过transform了,所以就不用重新来一遍了。
然后是此过程中的磁盘缓存过程,影响的因素有encodeStrategy
、DiskCacheStrategy.isResourceCacheable
:
Bitmap
或BitmapDrawable
,那么就是TRANSFORMED
;GifDrawable
,那么就是SOURCE
。BitmapEncoder
、BitmapDrawableEncoder
和GifDrawableEncoder
类isFromAlternateCacheKey
搜了一遍源码,只有一个没有使用过的类BaseGlideUrlLoader
中发现了痕迹,还是一个空集合实现,没有其他任何位置在使用,所以此处可以简单理解的该参数一直为false。DataSource.LOCAL
且encodeStrategy为EncodeStrategy.TRANSFORMED
时,才允许缓存。换句话说,只有本地的resource数据为Bitmap
或BitmapDrawable
的资源才可以缓存。最后,如果可以缓存,会初始化一个deferredEncodeManager
,在展示resource资源后会调用此对象进行磁盘缓存的写入。写入的代码如下:
// DecodeJob.java
//
// deferredEncodeManager.init(key, encoder, LockedResource.obtain(transformed));
private void notifyEncodeAndRelease(Resource<R> resource, DataSource dataSource) {
if (resource instanceof Initializable) {
((Initializable) resource).initialize();
}
Resource<R> result = resource;
LockedResource<R> lockedResource = null;
if (deferredEncodeManager.hasResourceToEncode()) {
lockedResource = LockedResource.obtain(resource);
result = lockedResource;
}
notifyComplete(result, dataSource);
stage = Stage.ENCODE;
try {
if (deferredEncodeManager.hasResourceToEncode()) {
deferredEncodeManager.encode(diskCacheProvider, options);
}
} finally {
if (lockedResource != null) {
lockedResource.unlock();
}
}
// Call onEncodeComplete outside the finally block so that it's not called if the encode process
// throws.
onEncodeComplete();
}
// DecodeJob#DeferredEncodeManager
private static class DeferredEncodeManager<Z> {
private Key key;
private ResourceEncoder<Z> encoder;
private LockedResource<Z> toEncode;
@Synthetic
DeferredEncodeManager() { }
// We just need the encoder and resource type to match, which this will enforce.
@SuppressWarnings("unchecked")
<X> void init(Key key, ResourceEncoder<X> encoder, LockedResource<X> toEncode) {
this.key = key;
this.encoder = (ResourceEncoder<Z>) encoder;
this.toEncode = (LockedResource<Z>) toEncode;
}
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();
}
}
boolean hasResourceToEncode() {
return toEncode != null;
}
void clear() {
key = null;
encoder = null;
toEncode = null;
}
}
至此,磁盘缓存的读写都已经完毕。剩下的就是内存缓存的两个层次了。我们回到DecodeJob.notifyEncodeAndRelease
方法中,经过notifyComplete
、EngineJob.onResourceReady
、notifyCallbacksOfResult
方法中。
在该方法中一方面会将原始的resource包装成一个EngineResource
,然后通过回调传给Engine.onEngineJobComplete
,在这里会将资源保持在active resource中:
@Override
public synchronized void onEngineJobComplete(
EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null) {
resource.setResourceListener(key, this);
if (resource.isCacheable()) {
//将资源缓存到activeResources中,
activeResources.activate(key, resource);
}
}
jobs.removeIfCurrent(key, engineJob);
}
以上就是关于Glide的缓存的全部内容了。
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 7:00:36- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |