Glide源码分析-缓存

基本概念

每一个优秀的图片图片框架都会有完善的缓存管理机制,Glide当然不例外。这一部分就是分析它的缓存相关模块。流程图就不需要画了,缓存的本质无非就是putget,而且Glide的所有缓存的实现都是Lru,与我们常见的LruCache并没有本质区别,所以我们只分析Glide的缓存思想和优化工作。

相关类的介绍

Glide的缓存分为两大部分,一类是常规的内存和磁盘缓存,另一类是Glide独有的BitmapPool缓存。

  • MemoryCache: 传统的内存缓存接口
  • activeResources: 在我们之前分析Engine时提到过的一个弱引用为值得Map,作为第二层内存缓存
  • DishCache: 传统的磁盘缓存接口
  • BitmapPool: Bitmap池接口,用于避免反复创建相同尺寸和属性的Bitmap而消耗性能

源码分析

Glide的缓存机制有两类,我们先分析比较传统的一类,也就是大家都会用到的内存缓存和磁盘缓存。在我们之前分析过的Engineload()方法代码里,我们能看到内存缓存的身影:

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
52

public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb)
{



EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}

EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
}

private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}

EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
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, true /*isCacheable*/);
}
return result;
}

我们由之前的分析可以知道,在真正发起一个EngineRunnable请求之前,会先从内存缓存和activeResources里尝试取数据,内存缓存其实不用多做分析,与我们平时用的没有本质区别。关键是activeResources:

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

private final Map<Key, WeakReference<EngineResource<?>>> activeResources;

private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}

EngineResource<?> active = null;
WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
if (activeRef != null) {
active = activeRef.get();
if (active != null) {
active.acquire();
} else {
activeResources.remove(key);
}
}
return active;
}

private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}

EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
return cached;
}

public void onEngineJobComplete(Key key, EngineResource<?> resource) {
Util.assertMainThread();
// A null resource indicates that the load failed, usually due to an exception.
if (resource != null) {
resource.setResourceListener(key, this);

if (resource.isCacheable()) {
activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
}
}
// TODO: should this check that the engine job is still current?
jobs.remove(key);
}

activeResources是一个以弱引用Resource为值的Map,只要这次请求获取到数据时,它都会将结果缓存进去,作为第二级缓存备用。比一般内存缓存额外多一级缓存的意义在于,当内存不足时清理内存缓存中的资源时,不会对使用中的Resource造成影响。

而磁盘缓存我们在上一部分分析的时候已经分析过了,在一个DecodeJob中,会首先从磁盘缓存中获取数据;如果没有命中,则从数据源取到数据之后会写入磁盘缓存备用。
通过以上的逻辑,Glide就实现了一个传统的缓存结构。但是Glide之所以在推出的时候,宣传的是它在列表里表现的”丝般顺滑”,主要还是源于它的BitmapPool设计。我们首先看下BitmapPool的代码:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153

public class LruBitmapPool implements BitmapPool {
private static final String TAG = "LruBitmapPool";
private static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.ARGB_8888;

private final LruPoolStrategy strategy;
private final Set<Bitmap.Config> allowedConfigs;
private final int initialMaxSize;
private final BitmapTracker tracker;

private int maxSize;
private int currentSize;
private int hits;
private int misses;
private int puts;
private int evictions;

// Exposed for testing only.
LruBitmapPool(int maxSize, LruPoolStrategy strategy, Set<Bitmap.Config> allowedConfigs) {
this.initialMaxSize = maxSize;
this.maxSize = maxSize;
this.strategy = strategy;
this.allowedConfigs = allowedConfigs;
this.tracker = new NullBitmapTracker();
}

/**
* Constructor for LruBitmapPool.
*
* @param maxSize The initial maximum size of the pool in bytes.
*/

public LruBitmapPool(int maxSize) {
this(maxSize, getDefaultStrategy(), getDefaultAllowedConfigs());
}

/**
* Constructor for LruBitmapPool.
*
* @param maxSize The initial maximum size of the pool in bytes.
* @param allowedConfigs A white listed set of {@link android.graphics.Bitmap.Config} that are allowed to be put
* into the pool. Configs not in the allowed set will be rejected.
*/

public LruBitmapPool(int maxSize, Set<Bitmap.Config> allowedConfigs) {
this(maxSize, getDefaultStrategy(), allowedConfigs);
}

@Override
public int getMaxSize() {
return maxSize;
}

@Override
public synchronized void setSizeMultiplier(float sizeMultiplier) {
maxSize = Math.round(initialMaxSize * sizeMultiplier);
evict();
}

@Override
public synchronized boolean put(Bitmap bitmap) {
if (bitmap == null) {
throw new NullPointerException("Bitmap must not be null");
}
if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize || !allowedConfigs.contains(bitmap.getConfig())) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Reject bitmap from pool"
+ ", bitmap: " + strategy.logBitmap(bitmap)
+ ", is mutable: " + bitmap.isMutable()
+ ", is allowed config: " + allowedConfigs.contains(bitmap.getConfig()));
}
return false;
}

final int size = strategy.getSize(bitmap);
strategy.put(bitmap);
tracker.add(bitmap);

puts++;
currentSize += size;

if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
}
dump();

evict();
return true;
}

private void evict() {
trimToSize(maxSize);
}

@Override
public synchronized Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap result = getDirty(width, height, config);
if (result != null) {
// Bitmaps in the pool contain random data that in some cases must be cleared for an image to be rendered
// correctly. we shouldn't force all consumers to independently erase the contents individually, so we do so
// here. See issue #131.
result.eraseColor(Color.TRANSPARENT);
}

return result;
}

@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
@Override
public synchronized Bitmap getDirty(int width, int height, Bitmap.Config config) {
// Config will be null for non public config types, which can lead to transformations naively passing in
// null as the requested config here. See issue #194.
final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
if (result == null) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config));
}
misses++;
} else {
hits++;
currentSize -= strategy.getSize(result);
tracker.remove(result);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
result.setHasAlpha(true);
}
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config));
}
dump();

return result;
}

@Override
public void clearMemory() {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "clearMemory");
}
trimToSize(0);
}

@SuppressLint("InlinedApi")
@Override
public void trimMemory(int level) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "trimMemory, level=" + level);
}
if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_MODERATE) {
clearMemory();
} else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
trimToSize(maxSize / 2);
}
}
}

上面是BitmapPool的主要实现类LruBitmapPool关键的一部分代码。我们可以看到在put()方法里,并没有常见的key值,key是完全根据Bitmap的尺寸和属性生成的。在get()方法里,也是通过传入尺寸和属性获取对象的。其实不需要看BitmapPool是怎么使用的,光看这部分其实就可以知道它的设计思路了。在Andorid的ListViewRecyclerView中,如果item里有图片的话,快速滑动的过程中无法避免不断创建新的Bitmap对象供item使用,在ListViewRecyclerViewView是可复用的,Bitmap并不是,而在Android中Bitmap的创建是一件非常耗费自愿的事情,所有因此衍生出这个BitmapPool,当已经有尺寸和属性相同的Bitmap被创建出来后就不用再创建新的了,直接使用之前的就行。我们可以看某个地方使用BitmapPool的代码:

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

public class CenterCrop extends BitmapTransformation {

public CenterCrop(Context context) {
super(context);
}

public CenterCrop(BitmapPool bitmapPool) {
super(bitmapPool);
}

// Bitmap doesn't implement equals, so == and .equals are equivalent here.
@SuppressWarnings("PMD.CompareObjectsWithEquals")
@Override
protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
final Bitmap toReuse = pool.get(outWidth, outHeight, toTransform.getConfig() != null
? toTransform.getConfig() : Bitmap.Config.ARGB_8888);
Bitmap transformed = TransformationUtils.centerCrop(toReuse, toTransform, outWidth, outHeight);
if (toReuse != null && toReuse != transformed && !pool.put(toReuse)) {
toReuse.recycle();
}
return transformed;
}

@Override
public String getId() {
return "CenterCrop.com.bumptech.glide.load.resource.bitmap";
}
}

在用于生成centerCrop效果的Transformation里,我们能看到在transform方法里就传入了全局的BitmapPool,在构建目标的Bitmap时,会从BitmapPool中取对应的Bitmap以复用。这样就避免了每次都必须创建一个新的Bitmap进而消耗太多资源。

总结

Glide的缓存机制的优秀之处,一是BitmapPool机制,二是activeResources这个第二级缓存增加了内存缓存的命中率。Glide通过缓存机制达到了它所宣传的顺滑的特点。当然,BitmapPool肯定会占用部分内存,但是与提升体验相比瑕不掩瑜。如果某些低端机型出现内存不够的情况,可能需要手动监听内存使用情况并及时调用Glide.get(context).clearMemory()

Glide的源码分析基本就是这样,但是还是建议完整地阅读源码,因为分析只能了解Glide整体的结构,对debug有帮助,但是如果想完全发挥Glide的巨大潜能,还是应该阅读源码并结合官方文档,充分利用ModelLoaderTransformation以及Target这些可扩展的接口打造适合自己项目的框架。