0%

再谈Glide的内存缓存

Glide的内存缓存有很多文章介绍,一级缓存?二级缓存?但我一直有疑惑,究竟什么样的情况把缓存放进所谓一级缓存容器?什么情况把缓存放进所谓二级缓存容器?又是什么情况需要从一级缓存获取数据?什么情况从二级缓存获取数据?另外,这个所谓一、二级缓存到底有怎样的联系?从设计一个缓存机制的角度讲,为什么要设计两个缓存容器?

被缓存的对象EngineResource

在讨论缓存前先理解究竟缓存的是什么东西,Glide设计中缓存的对象是EngineResource,EngineResource持有Resource,Resource持有真正的数据:泛型Z,以加载一张图片为例:

1
2
3
4
5
6
7
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ImageView iv = findViewById(R.id.iv);
Glide.with(this).load(R.raw.image).into(iv);
}

此时设定:加载完毕并且页面没有被销毁。使用Profiler查看此时这个Bitmap的情况,它会被四个引用关联,如图:
tu1.png

  1. 第一个是sun.misc.Cleaner,这在Bitmap的回收中介绍过,是一个幽灵引用,用于感知java Bitmap被回收后释放native Bitmap内存
  2. 第二个引用链上有ImageView,一张Bitmap要显示那么它需要被ImageView使用
  3. 第三个引用链就是Glide主动加上的了,引用链上的ActiveResource$ResourceWeakReference对象是一个弱引用,它指向EngineResource对象,EngineResource对象间接持有Bitmap
  4. 第4个是SingleRequest,在这里实际上和ImageView是一伙的,它是Glide给ImageView设置的tag

Glide的缓存对象是EngineResource,不是Bitmap,假设一种情况:EngineResource实例没有被任何对象引用了,而图片还在显示中,那么下一次GC时EngineResource实例就会被回收,而Bitmap由于被ImageView引用着,不会被回收。反之同理,如果整个ImageView被detach了,而EngineResource实例还在被Glide引用着,下次GC时ImageView被回收,Bitmap也不会被回收,因为它被EngineResource实例引用着。所以,所谓缓存其实就是使用强引用控制被缓存对象不被回收,或者这里演示的使用弱引用感知缓存对象被回收,然后再做进一步的缓存处理。

缓存容器之ActiveResource

数据结构

ActivityResource中保存缓存的是一个HashMap:

1
final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();

map的键是EngineKey,值是ResourceWeakReference,它是一个弱引用,指向EngineResource,它的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static final class ResourceWeakReference extends WeakReference<EngineResource<?>> {

final Key key;

final boolean isCacheable;

Resource<?> resource;//真正持有Bitmap对象的Resource

ResourceWeakReference(
@NonNull Key key,
@NonNull EngineResource<?> referent,
@NonNull ReferenceQueue<? super EngineResource<?>> queue,
boolean isActiveResourceRetentionAllowed) {
super(referent, queue);
this.key = Preconditions.checkNotNull(key);
//真正持有Bitmap对象的Resource,被赋值,一般情况为null
this.resource =
referent.isMemoryCacheable() && isActiveResourceRetentionAllowed
? Preconditions.checkNotNull(referent.getResource())
: null;
isCacheable = referent.isMemoryCacheable();
}

void reset() {
resource = null;
clear();
}
}

这段代码当初我看了很久😭,ResourceWeakReference虽然是一个弱引用对象,但是弱引用是对它关联的EngineResource对象来说的,ResourceWeakReference好歹也是一个对象,而且它被HashMap强引用着,这里又把真正持有Bitmap对象的Resource<?>直接引用,究竟在干什么?其实这就是设计ActiveResource的初衷,待会释放资源小节解释。

放入资源

放入资源的步骤实际就是调用ActiveResource的activate方法,
它创建一个ResourceWeakReference,放进HashMap中,调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
at com.bumptech.glide.load.engine.ActiveResources.activate(ActiveResources.java:79)
at com.bumptech.glide.load.engine.Engine.onEngineJobComplete(Engine.java:375)
at com.bumptech.glide.load.engine.EngineJob.notifyCallbacksOfResult(EngineJob.java:260)
at com.bumptech.glide.load.engine.EngineJob.onResourceReady(EngineJob.java:327)
at com.bumptech.glide.load.engine.DecodeJob.notifyComplete(DecodeJob.java:339)
at com.bumptech.glide.load.engine.DecodeJob.notifyEncodeAndRelease(DecodeJob.java:457)
at com.bumptech.glide.load.engine.DecodeJob.decodeFromRetrievedData(DecodeJob.java:436)
at com.bumptech.glide.load.engine.DecodeJob.onDataFetcherReady(DecodeJob.java:394)
at com.bumptech.glide.load.engine.ResourceCacheGenerator.onDataReady(ResourceCacheGenerator.java:129)
at com.bumptech.glide.load.model.ByteBufferFileLoader$ByteBufferFetcher.loadData(ByteBufferFileLoader.java:62)
at com.bumptech.glide.load.engine.ResourceCacheGenerator.startNext(ResourceCacheGenerator.java:105)
at com.bumptech.glide.load.engine.DecodeJob.runGenerators(DecodeJob.java:311)
at com.bumptech.glide.load.engine.DecodeJob.runWrapped(DecodeJob.java:277)
at com.bumptech.glide.load.engine.DecodeJob.run(DecodeJob.java:235)

notifyCallbacksOfResult方法里还会通知注册的callback加载完成,这个callback里调用SingleRequest,完成主线任务:通知Target图片加载完成了哦。
还有一种情况是从MemoryCache中命中了EngineResource,也会走这个逻辑,把EngineResource放入ActiveResource,并且从MemoryCache中移除。

复用资源

由于ResourceWeakReference是弱引用,所以真正的Bitmap的回收只受ImageView是否还持有Bitmap,或者说View树是否还引用到该ImageView有关,如果页面存在两个两个一模一样的ImageView,加载的图片都是一模一样的,如下:

1
2
3
4
5
6
//所有参数相同的ImageView
ImageView iv = findViewById(R.id.iv);
ImageView iv1 = findViewById(R.id.iv1);
Glide.with(this).load(R.raw.image).into(iv);
//假设延时等待上一个加载图片任务完毕
Glide.with(this).load(R.raw.image).into(iv1);

由于第一个加载已经将EngineResource放入ActiveResource了,
那么第二个加载在EngineJob的loadFromActiveResources中就会命中,所以会直接返回同一个Bitmap给第二个ImageView,达到复用资源的目的。

释放资源

主动释放资源

主动调用Glide.with().clear(target)就是主动释放资源,它会移除ActiveResource中保存的对应的EngineResource,并将资源放进MemoryCache中,调用的时机包括:

  1. 手动调用Glide.with().clear(target);
  2. 复用ImageView时Glide发现View的R.id.glide_custom_view_target_tag是自己以前设置的Request,取出来调用clear(imageView)
  3. 该Glide请求与Activity生命周期关联,关闭页面触发onDestroy,进而触发clear(imageView)
被动释放资源

多数情况下,我们并没主动调用过clear(target),要么是Glide帮我们调用了,要么就是被动释放资源,下面代码就模拟被动释放资源的情况:

1
2
3
4
5
6
7
8
9
10
ImageView iv = findViewById(R.id.iv);
Glide.with(this).load(R.raw.image).into(iv);

//假设等待加载完毕
Object o = iv1.getTag(R.id.glide_custom_view_target_tag);
if (o instanceof Request) {
iv1.setTag(R.id.glide_custom_view_target_tag, null);
iv1.setImageBitmap(null);
}

参考第一小节的引用链图,那么此时就只有一个ResourceWeakReference引用着EngineResource,这时手动触发GC,EngineResource就会被回收,此时ActiveResource中有一个后台线程正等着EngineResource被回收,后台线程会执行如下代码,小子终于等到你了😏:

1
2
3
4
5
6
7
8
9
10
11
12
13
void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
synchronized (this) {
activeEngineResources.remove(ref.key);
if (!ref.isCacheable || ref.resource == null) {//真正保存有Bitmap的resource
return;
}
}

EngineResource<?> newResource =
new EngineResource<>(
ref.resource, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ false, ref.key, listener);
listener.onResourceReleased(ref.key, newResource);
}

由于EngineResource已经被回收了,那就直接从activeEngineResources移除和这个EngineResource关联的ResourceWeakReference,等待回收,如果真正保存有Bitmap的Resource为null就返回,在第一小节代码中解释过一般都是null,至此ActiveResource一般回收逻辑就完了。 但后面的代码总不是多余的吧?什么情况Resource不会null呢?这需要我们主动配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

/**
* If set to {@code true}, allows Glide to re-capture resources that are loaded into {@link
* com.bumptech.glide.request.target.Target}s which are subsequently de-referenced and garbage
* collected without being cleared.
*
* <p>Defaults to {@code false}.
*
* <p>Glide's resource re-use system is permissive, which means that's acceptable for callers to
* load resources into {@link com.bumptech.glide.request.target.Target}s and then never clear the
* {@link com.bumptech.glide.request.target.Target}. To do so, Glide uses {@link
* java.lang.ref.WeakReference}s to track resources that belong to {@link
* com.bumptech.glide.request.target.Target}s that haven't yet been cleared. Setting this method
* to {@code true} allows Glide to also maintain a hard reference to the underlying resource so
* that if the {@link com.bumptech.glide.request.target.Target} is garbage collected, Glide can
* return the underlying resource to it's memory cache so that subsequent requests will not
* unexpectedly re-load the resource from disk or source. As a side affect, it will take the
* system slightly longer to garbage collect the underlying resource because the weak reference
* has to be cleared and processed before the hard reference is removed. As a result, setting this
* method to {@code true} may transiently increase the memory usage of an application.
*
* <p>Leaving this method at the default {@code false} value will allow the platform to garbage
* collect resources more quickly, but will lead to unexpected memory cache misses if callers load
* resources into {@link com.bumptech.glide.request.target.Target}s but never clear them.
*
* <p>If you set this method to {@code true} you <em>must not</em> call {@link Bitmap#recycle()}
* or mutate any Bitmaps returned by Glide. If this method is set to {@code false}, recycling or
* mutating Bitmaps is inefficient but safe as long as you do not clear the corresponding {@link
* com.bumptech.glide.request.target.Target} used to load the {@link Bitmap}. However, if you set
* this method to {@code true} and recycle or mutate any returned {@link Bitmap}s or other mutable
* resources, Glide may recover those resources and attempt to use them later on, resulting in
* crashes, graphical corruption or undefined behavior.
*
* <p>Regardless of what value this method is set to, it's always good practice to clear {@link
* com.bumptech.glide.request.target.Target}s when you're done with the corresponding resource.
* Clearing {@link com.bumptech.glide.request.target.Target}s allows Glide to maximize resource
* re-use, minimize memory overhead and minimize unexpected behavior resulting from edge cases. If
* you use {@link RequestManager#clear(Target)}, calling {@link Bitmap#recycle()} or mutating
* {@link Bitmap}s is not only unsafe, it's also totally unnecessary and should be avoided. In all
* cases, prefer {@link RequestManager#clear(Target)} to {@link Bitmap#recycle()}.
*
* @return This builder.
*/
// Public API.
@SuppressWarnings("unused")
@NonNull
public GlideBuilder setIsActiveResourceRetentionAllowed(
boolean isActiveResourceRetentionAllowed) {
this.isActiveResourceRetentionAllowed = isActiveResourceRetentionAllowed;
return this;
}

设置 isActiveResourceRetentionAllowed 为true后, ResourceWeakReference 会直接持有真正保存有Bitmap的Resource<>,那么当我们加载完图片时,ResourceWeakReference会强引用到Resource

tu2.png

如图,多了一个对Resource<>的引用。当 EngineResourceImageView 都不再引用Resource<>时,ResourceWeakReference 说不好意思爷不让你回收,它会重新建一个 EngineResource ,然后把它放进 MemoryCache 中。为什么要这样设计?上面的注释说的很清楚,考虑这样的情况:

1
2
3
4
5
6
7
8
9
10
ImageView iv = findViewById(R.id.iv);
Glide.with(this).load(R.raw.image).into(iv);

//假设等待加载完毕
Object o = iv1.getTag(R.id.glide_custom_view_target_tag);
if (o instanceof Request) {
iv1.setTag(R.id.glide_custom_view_target_tag, null);
iv1.setImageBitmap(null);
}

此时触发GC,Resource<>由于被 ResourceWeakReference 引用着,不会回收,想让加载的资源再缓存一会儿,就把它放进MemoryCache中再缓存一段时间,万一等会儿又有相同的加载避免重复加载。试想如果不设置 isActiveResourceRetentionAllowed 为true,因为GC不是程序控制的,我们不知道Resource<?>究竟被回收没有,就有可能导致重复创建资源。

为什么要ActiveResource

它是对正在使用中的Resource的一种弱引用缓存,一方面,避免一个页面中重复加载相同的Bitmap。另一方面,由于使用Glide时没有clear(Target)的习惯,GC触发时机不对,导致虽然ActiveResource移除了EngineResource,但是Bitmap可能还在内存中,这时相同的Glide加载会重新创建一个新的Bitmap,为了避免这种情况,提供了 isActiveResourceRetentionAllowed开关。

ActiveResource主要逻辑图

tu3.png

缓存容器之MemoryCache

MemoryCache比起ActiveResource简单很多,它的实现类的是LruResourceCache,在上面已经讲过,把资源放进MemoryCache需要调用clear(target),每次加载也会拿着Key去MemoryCache找是否有cache可以用。这种情况一般是重复进入一个页面,destroy时Glide clear(target),立即重新进入时资源还在MemoryCache中,命中复用。