Android-图片加载框架-Glide-4-9-0-(二)-切入源码层深入分析-Glide-缓存策略(1)

| 磁盘缓存-原始数据 | DiskLruCacheWrapper | 网络请求成功后将原始数据在磁盘中缓存 |

如果对 Glide 执行流程不明白的可以先看 Android 图片加载框架 Glide 4.9.0 (一) 从源码的角度分析一次最简单的执行流程

在介绍缓存原理之前,先来看一张加载缓存执行顺序,先有个印象。

缓存 key 生成

不管是内存缓存还是磁盘缓存,存储的时候肯定需要一个唯一 key 值,那么 Glide cache key 是怎么生成的?通过上一篇源码加载流程介绍,我们知道在 Engine 的 load 函数中进行对 key 的生成。下面我们就通过代码来看一下。

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

public synchronized LoadStatus load(
GlideContext glideContext,
Object model,
Key signature,
int width,
int height,
Class<?> resourceClass, Class transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map

}

}

//生成 key
EngineKey buildKey(Object model, Key signature, int width, int height,
Map<Class<?>, Transformation<?>> transformations, Class<?> resourceClass, Class<?> transcodeClass, Options options) {
return new EngineKey(model, signature, width, height, transformations, resourceClass,
transcodeClass, options);
}
复制代码

class EngineKey implements Key {

@Override
public boolean equals(Object o) {
if (o instanceof EngineKey) {
EngineKey other = (EngineKey) o;
return model.equals(other.model)
&& signature.equals(other.signature)
&& height == other.height
&& width == other.width
&& transformations.equals(other.transformations)
&& resourceClass.equals(other.resourceClass)
&& transcodeClass.equals(other.transcodeClass)
&& options.equals(other.options);
}
return false;
}

@Override
public int hashCode() {
if (hashCode == 0) {
hashCode = model.hashCode();
hashCode = 31 * hashCode + signature.hashCode();
hashCode = 31 * hashCode + width;
hashCode = 31 * hashCode + height;
hashCode = 31 * hashCode + transformations.hashCode();
hashCode = 31 * hashCode + resourceClass.hashCode();
hashCode = 31 * hashCode + transcodeClass.hashCode();
hashCode = 31 * hashCode + options.hashCode();
}
return hashCode;
}

}
复制代码

根据注释和代码可以看到传入的参数之多,主要是根据 url ,签名,宽高等,其内部重写了 hashCode,equals,来保证对象的唯一性。

内存缓存

通过下面代码开启内存缓存,当然 Glide 默认是为我们开启了内存缓存所以不需要我们调用 skipMemoryCache

//在 BaseRequestOptions 成员变量中默认为内存缓存开启。
private boolean isCacheable = true;

//调用层调用
Glide.
with(MainActivity.this.getApplication()).
//开启使用内存缓存
skipMemoryCache(true).
into(imageView);
复制代码

现在缓存 key 有了之后,就可以根据 key 拿到对应的缓存了,通过文章开始的介绍,我们知道先加载活动缓存,如果活动缓存没有在加载内存缓存,先看下代码;

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

public synchronized LoadStatus load(
…//参数
) {

//1. 生成缓存唯一 key 值,model 就是图片地址
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);

//2. 优先加载内存中的活动缓存 - ActiveResources
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;
}

//3. 如果活动缓存中没有,就加载 LRU 内存缓存中的资源数据。
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;
}

}
复制代码

Glide 为什么会设计 2 个内存缓存

不知道大家通过上面代码跟注释(2,3 ) , 有没有发现为什么 Glide 会弄 2 个内存缓存(一个 Map + 弱引用,一个 LRU 内存缓存),大家有没有想过为什么?在看 郭霖大神对 Glide 缓存机制分析 中说道, ActiveResources 就是一个弱引用的 HashMap ,用来缓存正在使用中的图片,使用 ActiveResources 来缓存正在使用中的图片,可以保护这些图片不会被 LruCache 算法回收掉 ,起初当我看见这句话的时候,我是真的很不理解,因为 Lru 是最近最少时候才会回收尾端数据,那么这里的 ActiveResources 来缓存正在使用中的图片,可以保护这些图片不会被 LruCache 算法回收掉 ,我是越想越觉得矛盾,后来中午吃饭的时候,灵光一闪突然想到了一种情况,我也不知道是不是这样,先看下面一张图。

详细举例说明: 比如我们 Lru 内存缓存 size 设置装 99 张图片,在滑动 RecycleView 的时候,如果刚刚滑动到 100 张,那么就会回收掉我们已经加载出来的第一张,这个时候如果返回滑动到第一张,会重新判断是否有内存缓存,如果没有就会重新开一个 Request 请求,很明显这里如果清理掉了第一张图片并不是我们要的效果。所以在从内存缓存中拿到资源数据的时候就主动添加到活动资源中,并且清理掉内存缓存中的资源。这么做很显然好处是 保护不想被回收掉的图片不被 LruCache 算法回收掉,充分利用了资源。 我也不知道这样理解是否正确,希望如果有其它的含义,麻烦告知一下,谢谢 !

上面我们说到了 Glide 为什么会设计 2 个内存缓存,下面我们就对这 2 个缓存的 存/取/删 来具体说明下。

ActiveResources 活动资源

获取活动资源

通过之前的介绍我们知道,活动缓存是在Engine load中获取

public synchronized LoadStatus load(
…//参数
) {

//1. 优先加载内存中的活动缓存 - ActiveResources
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
// 如果找到获取资源,就返回上层
cb.onResourceReady(active, DataSource.MEMORY_CACHE);
return null;
}

}

@Nullable
private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) { if (!isMemoryCacheable) { return null; } // 通过 活动资源的 get 函数拿到活动资源的缓存 EngineResource<?> active = activeResources.get(key);
if (active != null) {
//正在使用,引用计数 +1
active.acquire();
}

return active;
}

复制代码

继续看 get 的具体实现

final class ActiveResources {

//
@VisibleForTesting
final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
private final ReferenceQueue<EngineResource<?>> resourceReferenceQueue = new ReferenceQueue<>();

//外部调用 get 函数拿到活动资源缓存
@Nullable
synchronized EngineResource<?> get(Key key) {
//通过 HashMap + WeakReference 的存储结构
//通过 HashMap 的 get 函数拿到活动缓存
ResourceWeakReference activeRef = activeEngineResources.get(key);
if (activeRef == null) {
return null;
}

EngineResource<?> active = activeRef.get();
if (active == null) {
cleanupActiveReference(activeRef);
}
return active;
}

//继承 WeakReference 弱引用,避免内存泄漏。
@VisibleForTesting
static final class ResourceWeakReference extends WeakReference<EngineResource<?>> {

}

复制代码

通过上面代码我们知道活动缓存是 Map + WeakReference 来进行维护的,这样做的好处是避免图片资源内存泄漏。

存储活动资源

通过文章开头表格中提到,活动资源是加载内存资源之后存储的,那么我们就来看下何时加载内存资源,看下面代码。

还是在Engine load函数中

//1. 如果活动缓存中没有,就加载 LRU 内存缓存中的资源数据。
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;
}

// 加载内存中图片资源
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) { if (!isMemoryCacheable) { return null; } //2\. 拿到内存缓存,内部将当前的 key 缓存 remove 了 EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
// 3. 如果内存缓存不为空,则引用计数器 +1
cached.acquire();
//4. 添加进活动缓存中
activeResources.activate(key, cached);
}
return cached;
}
复制代码

通过注释 4 ,我们知道在拿到内存缓存的时候,先将内存缓存的当前 key 删除了,然后添加到活动缓存中。

清理活动资源

通过上一篇 Android 图片加载框架 Glide 4.9.0 (一) 从源码的角度分析一次最简单的执行流程 中得知,我们是在EngineJob中发出的通知回调,告知Engine onResourceReleased来删除 活动资源。下面我们在回顾一下执行流程,那么我们就从 EngineJob 发出通知开始看吧:

class EngineJob implements DecodeJob.Callback,
Poolable {

@Override
public void onResourceReady(Resource resource, DataSource dataSource) {
synchronized (this) {
this.resource = resource;
this.dataSource = dataSource;
}
notifyCallbacksOfResult();
}

@Synthetic
void notifyCallbacksOfResult() {

//回调上层 Engine 任务完成了
listener.onEngineJobComplete(this, localKey, localResource);

//遍历资源回调给 ImageViewTarget ,并显示
for (final ResourceCallbackAndExecutor entry : copy) {
entry.executor.execute(new CallResourceReady(entry.cb));
}
//这里是通知发出上层删除活动资源数据
decrementPendingCallbacks();
}

}

//重要的就是这里了
@Synthetic
synchronized void decrementPendingCallbacks() {
if (decremented == 0) {
if (engineResource != null) {
//如果不为空,那么就调用内部 release 函数
engineResource.release();
}
}
}

复制代码

看一下 EngineResource 的 release 函数

class EngineResource implements Resource {

void release() {
synchronized (listener) {
synchronized (this) {
if (acquired <= 0) {
throw new IllegalStateException(“Cannot release a recycled or not yet acquired resource”);
}
//这里每次调用一次 release 内部引用计数法就会减一,到没有引用也就是为 0 的时候 ,就会通知上层
if (–acquired == 0) {
//回调出去,Engine 来接收
listener.onResourceReleased(key, this);
}
}
}
}

}
复制代码

根据注释,我们知道这里用了引用计数法,有点像 GC 回收的 引用计数法的影子。也就是说,当完全没有使用这样图片的时候,就会把活动资源清理掉,接着往下看,会调用 Engine 的 onResourceReleased 函数。

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

。。。

//接收来自 EngineResource 的调用回调
@Override
public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
//1. 收到当前图片没有引用,清理图片资源
activeResources.deactivate(cacheKey);
//2. 如果开启了内存缓存
if (resource.isCacheable()) {
//3. 将缓存存储到内存缓存中。
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
。。。
}

复制代码

通过上面注释 1 可以知道,这里首先会将缓存图片从 activeResources 中移除,然后再将它 put 到 LruResourceCache 内存缓存当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用 LruCache 来进行缓存的功能,设计的真的很巧妙。

LruResourceCache 内存资源

获取内存资源

不知道有没有小伙伴注意到,其实在讲活动资源存储的时候已经涉及到了内存缓存的存储,下面我们在来看一下,具体代码如下:

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

…//忽略一些成员变量跟构造函数
public synchronized LoadStatus load(
…//忽略参数
) {


//1. 加载内存缓存
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
//如果内存缓存中有,就通知上层,最后在 SingleRequest 接收
cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey(“Loaded resource from cache”, startTime, key);
}
return null;
}

}


}

private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) { if (!isMemoryCacheable) { return null; } //2\. 通过 getEngineResourceFromCache 获取内存资源 EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {//如果内存资源存在
//则引用计数 +1
cached.acquire();
//3. 将内存资源存入活动资源
activeResources.activate(key, cached);
}
return cached;
}

private EngineResource<?> getEngineResourceFromCache(Key key) { //通过 Engine load 中传参可知,cache 就是 Lru 内存资源缓存 //2.1 这里是通过 remove 删除来拿到缓存资源 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, true /isMemoryCacheable/, true /isRecyclable/);
}
return result;
}
复制代码

通过注释1得知,这里是开始加载内存缓存中的资源;

通过注释2.1 可知,这里通过 Lru 内存缓存的 remove 来拿到内存缓存

最后将内存存储到活动缓存中,并缓存当前找到的内存缓存。

这里的活动缓存跟内存缓存紧密联系在一起,环环相扣,就像之前我们的疑问,为什么 Glide 会设计 2 个内存缓存的原因了。

存储内存资源

通过 EngineResource 的引用计数法机制 release 函数,只要没有引用那么就回调,请看下面代码:

class EngineJob implements DecodeJob.Callback,
Poolable {

@Override
public void onResourceReady(Resource resource, DataSource dataSource) {
synchronized (this) {
this.resource = resource;
this.dataSource = dataSource;
}
notifyCallbacksOfResult();
}

@Synthetic
void notifyCallbacksOfResult() {

//这里是通知发出上层删除活动资源数据
decrementPendingCallbacks();
}

}

//重要的就是这里了
@Synthetic
synchronized void decrementPendingCallbacks() {
if (decremented == 0) {
if (engineResource != null) {
//如果不为空,那么就调用内部 release 函数
engineResource.release();
}
}
}

void release() {
synchronized (listener) {
synchronized (this) {
//引用计数为 0 的时候,回调。
if (–acquired == 0) {
listener.onResourceReleased(key, this);
}
}
}
}

复制代码

最后会回调到 Engine 的 onResourceReleased 函数:

@Override
public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
//1. 清理活动缓存
activeResources.deactivate(cacheKey);
//如果开启了内存缓存
if (resource.isCacheable()) {
//2. 清理出来的活动资源,添加进内存缓存
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
复制代码

到了这里我们知道,在清理活动缓存的时候添加进了内存缓存。

清理内存缓存

这里的清理是在获取内存缓存的时候通过 remove 清理掉的,详细可以看内存缓存获取的方式。

内存缓存小结

通过上面分析可知,内存缓存有活动缓存和内存资源缓存,下面看一个图来总结下它们相互之间是怎么配合交换数据的。

总结下步骤:

  1. 第一次加载没有获取到活动缓存。
  2. 接着加载内存资源缓存,先清理掉内存缓存,在添加进行活动缓存。
  3. 第二次加载活动缓存已经存在。
  4. 当前图片引用为 0 的时候,清理活动资源,并且添加进内存资源。
  5. 又回到了第一步,然后就这样环环相扣。

磁盘缓存

在介绍磁盘缓存之前先看一张表格

缓存表示说明
DiskCacheStrategy.NONE表示不开启磁盘缓存
DiskCacheStrategy.RESOURCE表示只缓存转换之后的图片。
DiskCacheStrategy.ALL表示既缓存原始图片,也缓存转换过后的图片。
DiskCacheStrategy.DATA表示只缓存原始图片
DiskCacheStrategy.AUTOMATIC根据数据源自动选择磁盘缓存策略(默认选择)

上面这 4 中参数其实很好理解,这里有一个概念需要记住,就是当我们使用 Glide 去加载一张图片的时候,Glide 默认并不会将原始图片展示出来,而是会对图片进行压缩和转换,总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。而 Glide 默认情况下在硬盘缓存的就是 DiskCacheStrategy.AUTOMATIC

以下面的代码来开启磁盘缓存:

Glide.
with(MainActivity.this.getApplication()).
//使用磁盘资源缓存功能
diskCacheStrategy(DiskCacheStrategy.RESOURCE).
into(imageView);
复制代码

知道了怎么开启,下面我们就来看一下磁盘缓存的的加载与存储。

这 2 个加载流程几乎一模一样,只是加载的数据源不同,下面我们具体来看一下

DiskCacheStrategy.RESOURCE 资源类型

获取资源数据

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

最后

Android学习是一条漫长的道路,我们要学习的东西不仅仅只有表面的 技术,还要深入底层,弄明白下面的 原理,只有这样,我们才能够提高自己的竞争力,在当今这个竞争激烈的世界里立足。

人生不可能一帆风顺,有高峰自然有低谷,要相信,那些打不倒我们的,终将使我们更强大,要做自己的摆渡人。

资源持续更新中,欢迎大家一起学习和探讨。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img
sdnimg.cn/13f2cb2e05a14868a3f0fd6ac81d625c.png)

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
[外链图片转存中…(img-57oHkK2E-1712772000303)]

最后

Android学习是一条漫长的道路,我们要学习的东西不仅仅只有表面的 技术,还要深入底层,弄明白下面的 原理,只有这样,我们才能够提高自己的竞争力,在当今这个竞争激烈的世界里立足。

人生不可能一帆风顺,有高峰自然有低谷,要相信,那些打不倒我们的,终将使我们更强大,要做自己的摆渡人。

资源持续更新中,欢迎大家一起学习和探讨。

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-Byrdmg5k-1712772000304)]

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值