3 Glide 源码分析之 BitmapPool
2.Glide中的Bitmap缓存策略全面剖析
1.Glide中的缓存介绍:
======
3 Glide 源码分析之 BitmapPool
bitmap的缓存
在build Glide 时 会初始化根据版本不同会选择一个bitmap的缓存策略 LruBitmapPool
Glide createGlide() {
if (sourceService == null) {
final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());
sourceService = new FifoPriorityThreadPoolExecutor(cores);
}
if (diskCacheService == null) {
diskCacheService = new FifoPriorityThreadPoolExecutor(1);
}
MemorySizeCalculator calculator = new MemorySizeCalculator(context);
if (bitmapPool == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
int size = calculator.getBitmapPoolSize();
bitmapPool = new LruBitmapPool(size);//**注意这里** 如果大于11 就会创建一个默认的Lru算法的bitmap缓存池
} else {
bitmapPool = new BitmapPoolAdapter();
}
}
if (memoryCache == null) {
memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
}
if (diskCacheFactory == null) {
diskCacheFactory = new InternalCacheDiskCacheFactory(context);
}
if (engine == null) {
engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
}
if (decodeFormat == null) {
decodeFormat = DecodeFormat.DEFAULT;
}
return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
}
LruBitmapPool: 负责控制缓存
LruPoolStrategy : 他的实现类 , 负责真正的缓存bitmap
默认每次在put的时候 , 都会累计内存用量 , 判断如果当前使用量大于允许的最大内存就会移除链表的表尾, 最新的默认会移动到表头;这就是最近最少使用算法;
//可以设置缓存的大小maxSize , 有2个默认的策略实现,第一个真正缓存功能的实现类 , LruBitmapPool 只是转发类 , 真正实现缓存是他 LruPoolStrategy
第二个允许bitmap的类型
/**
* Constructor for LruBitmapPool.
*
* @param maxSize The initial maximum size of the pool in bytes.
*/
public LruBitmapPool(int maxSize) {
this(maxSize, getDefaultStrategy(), getDefaultAllowedConfigs());
}
// 使用 SizeConfigStrategy 策略
private static LruPoolStrategy getDefaultStrategy() {
final LruPoolStrategy strategy;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
strategy = new SizeConfigStrategy();
} else {
strategy = new AttributeStrategy();
}
return strategy;
}
//Bitmap.Config 是个枚举, 把默认bitmap类型都放到容器里
private static Set<Bitmap.Config> getDefaultAllowedConfigs() {
Set<Bitmap.Config> configs = new HashSet<Bitmap.Config>();
configs.addAll(Arrays.asList(Bitmap.Config.values()));
if (Build.VERSION.SDK_INT >= 19) {
configs.add(null);
}
return Collections.unmodifiableSet(configs);
}
public static enum Config {
ALPHA_8,
ARGB_4444,
ARGB_8888,
HARDWARE,
RGBA_F16,
RGB_565;
private Config() {
}
}
来分析下 大于19版本 SizeConfigStrategy 他的实现思路;
以put 第二行 , 以bitmap的占用大小 和 他的bitmap像素类型 ,让一个key对象来包裹 , 封装成GroupedLinkedMap的key了, 依靠 自定义的 linkedHashMap , 把匹配的取出来并设置成表头;
GroupedLinkedMap 类:
// Make the entry the most recently used item.
private void makeHead(LinkedEntry<K, V> entry) {
removeEntry(entry);
entry.prev = head;
entry.next = head.next;
updateEntry(entry);
}
private final KeyPool keyPool = new KeyPool();
private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<Key, Bitmap>();
@Override
public void put(Bitmap bitmap) {
int size = Util.getBitmapByteSize(bitmap);//bitmap 占用内存大小
Key key = keyPool.get(size, bitmap.getConfig());
groupedMap.put(key, bitmap);
NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig());
Integer current = sizes.get(key.size);
sizes.put(key.size, current == null ? 1 : current + 1);
}
//keyPool 的 get 方法 , 默认先去队列里找 , 如果不为空就取出来 ,然后从新赋值新的属性 , 以复用对象 , 真是一点内存(多创建一个对象)都不浪费;
public Key get(int size, Bitmap.Config config) {
Key result = get();
result.init(size, config);
return result;
}
//队列为空才创建新的
protected T get() {
T result = keyPool.poll();
if (result == null) {
result = create();
}
return result;
}
来看取bitmap , 计算出最匹配的key , 拿出bitmap , 在控制类 减去当前的size占用
@Override
public Bitmap get(int width, int height, Bitmap.Config config) {
int size = Util.getBitmapByteSize(width, height, config);
Key targetKey = keyPool.get(size, config);
Key bestKey = findBestKey(targetKey, size, config);
Bitmap result = groupedMap.get(bestKey);
if (result != null) {
// Decrement must be called before reconfigure.
decrementBitmapOfSize(Util.getBitmapByteSize(result), result.getConfig());
result.reconfigure(width, height,
result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888);
}
return result;
}
private Key findBestKey(Key key, int size, Bitmap.Config config) {
Key result = key;
for (Bitmap.Config possibleConfig : getInConfigs(config)) {
NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
if (possibleSize != size
|| (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
keyPool.offer(key);
result = keyPool.get(possibleSize, possibleConfig);
}
break;
}
}
return result;
}
2.Glide中的Bitmap缓存策略全面剖析
那么Glide的缓存策略相较于ImageLoader缓存有什么优势吗?它们之间有什么样的差别,或者说性能方面,那一个更好,注意咱们这里所介绍的缓存是不包括sd卡存储的。
先来看一看Glide用了哪些缓存策略模式,如下:
是不是少了点,没错和缓存有关的就这两个类,MemoryCache是缓存的接口, MemoryCacheAdapter和LruResourceCache是实现类
public class MemoryCacheAdapter implements MemoryCache {
private ResourceRemovedListener listener;
@Override
public int getCurrentSize() {
return 0;
}
@Override
public int getMaxSize() {
return 0;
}
@Override
public void setSizeMultiplier(float multiplier) {
// Do nothing.
}
@Override
public Resource<?> remove(Key key) {
return null;
}
@Override
public Resource<?> put(Key key, Resource<?> resource) {
listener.onResourceRemoved(resource);
return null;
}
@Override
public void setResourceRemovedListener(ResourceRemovedListener listener) {
this.listener = listener;
}
@Override
public void clearMemory() {
// Do nothing.
}
@Override
public void trimMemory(int level) {
// Do nothing.
}
}
但是MemoryCacheAdapter的实现方法里面并没有实现具体的业务,看名字是实现了适配器模式,目前这个类没用,是为了以后做扩展用的,先不管它。
除了它就只剩LruResourceCache这个缓存策略类了,也是用了最少使用算法(经常被使用的对象在总的对象存储的内存超过阈值时被回收的概率低)。
它也是和ImageLoader一样用了下面这个集合类,在此不再深究。
LinkedHashMap<T, Y> cache = new LinkedHashMap<>(100, 0.75f, true);
就这一个缓存策略,这样就完了吗,不不不,远没有想象的那么简单,
Glide在缓存的Bitmap所占内存的总大小 超过阈值的去除最少使用的Bitmap的时候,总是会回调下面这个方法进行Bitmap的回收保存,如下:
protected void onItemEvicted(Key key, Resource<?> item) {
if (listener != null) {
listener.onResourceRemoved(item);
}
}
为什么要保存一部分Bitmap? 而不是直接放弃它,让它回收拿?
这样的做法的好处就是避免不必要的内存申请和回收的时间上的浪费,如果内存申请和回收太频繁的话就会引起内存抖动,
内存抖动在用户看来那就是界面已经在视觉上可以看出卡顿了有木有,这个是无法忍受的,这也是我们常说的以内存换速度的方式,合理的利用内存不是内存浪费,这也是Glide强于ImageLoader的一个点了。ok,来具体看一下它是怎么重利用Bitmap内存的。
这里注意一下, Glide缓存的所有资源都是Resource的实现类,Bitmap被它封装在内部的变量中,也是说Glide没有直接操作Bitmap,
而是用了Resource这个包装类来操作Bitmap。 所以说看回收的话,我们直接看com.bumptech.glide.load.resource.bitmap下的BitmapResource这个类就好了,回收的方法如下:
public void recycle() {
bitmapPool.put(bitmap);
}
最终用的com.bumptech.glide.load.engine.bitmap_recycle包下的 LruBitmapPool 来进行回收的,继续看
public synchronized void put(Bitmap bitmap) {
if (bitmap == null) {
throw new NullPointerException("Bitmap must not be null");
}
if (bitmap.isRecycled()) {
throw new IllegalStateException("Cannot pool recycled bitmap");
}
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()));
}
bitmap.recycle();
return;
}
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();
}
这个方法的意思就是 ,首先判断需要回收的Bitmap是否已经被回收了,它所占内存总大小是否超过最大值(这里的最大值是默认屏幕width*屏幕height*Argb_8888(也就是4)*4),也就是说最大值是你当前手机满屏幕像素所占内存大小的4倍, 是否包括这个图片配置属性(默认包括android的Bitmap的所有的属性),
如果这些都不满足的话 将Bitmap缓存到回收集合中,如果满足一项,回收Bitmap所占内存,
接下来,判断所有在 回收集合中的Bitmap总占内存是否超过阈值,如果超过的话就移除最少使用的回收,实现如下:
private synchronized void trimToSize(int size) {
while (currentSize > size) {
final Bitmap removed = strategy.removeLast();
// TODO: This shouldn't ever happen, see #331.
if (removed == null) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Size mismatch, resetting");
dumpUnchecked();
}
currentSize = 0;
return;
}
tracker.remove(removed);
currentSize -= strategy.getSize(removed);
evictions++;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed));
}
dump();
removed.recycle();
}
}
接下来看一下获取的方法:
public Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(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);
} else {
result = Bitmap.createBitmap(width, height, config);
}
return result;
}
private synchronized Bitmap getDirtyOrNull(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);
normalize(result);
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config));
}
dump();
return result;
}
这两个方法的意思就是, 首先从回收池中获取,如果获取到Bitmap,则复用这个Bitmap,并把当前的bitmap的颜色值清空,
如果没有的话,直接创建一个新的Bitmap返回。
下面这个方法是用来判断这个回收池中有没有符合这个Bitmap大小的Bitmap的内存
public Bitmap get(int width, int height, Bitmap.Config config) {
final int size = Util.getBitmapByteSize(width, height, config);
Key key = keyPool.get(size);
Integer possibleSize = sortedSizes.ceilingKey(size);
if (possibleSize != null && possibleSize != size && possibleSize <= size * MAX_SIZE_MULTIPLE) {
keyPool.offer(key);
key = keyPool.get(possibleSize);
}
// Do a get even if we know we don't have a bitmap so that the key moves to the front in the
// lru pool
final Bitmap result = groupedMap.get(key);
if (result != null) {
result.reconfigure(width, height, config);
decrementBitmapOfSize(possibleSize);
}
return result;
}
为什么要传宽和高还有config,因为你在复用Bitmap的时候,必须要满足你当前要创建的Bitmap所占内存 必须小于等于你缓冲池中的BitMap大小,
并且格式也要保持一致,这样才能保证准确的复用。下面来看一下复用的方法
private static void applyMatrix(@NonNull Bitmap inBitmap, @NonNull Bitmap targetBitmap,
Matrix matrix) {
BITMAP_DRAWABLE_LOCK.lock();
try {
Canvas canvas = new Canvas(targetBitmap);
canvas.drawBitmap(inBitmap, matrix, DEFAULT_PAINT);
clear(canvas);
} finally {
BITMAP_DRAWABLE_LOCK.unlock();
}
}
这里的第一个参数是你需要显示的Bitmap,而第二参数是回收池里的Bitmap,很显然这个方法是把你需要显示的Bitmap通过回收池的方法将已有内存交给它,通俗的将就是 将显示的Bitmap画在 回收池里面Bitmap的内存上。
这里可能你会问这不是还是创建了一个Bitmap吗?只是它创建了然后马上销毁了罢了。
好,接下来重点来了,看一看它是怎么创建要显示的Bitmap的。
要显示的Bitmap的创建是在 解码器里面创建的,这里只看流式解码器,位于com.bumptech.glide.load.resource.bitmap包下的StreamBitmapDecoder类decode方法,如下:
public Resource<Bitmap> decode(InputStream source, int width, int height, Options options)
throws IOException {
// Use to fix the mark limit to avoid allocating buffers that fit entire images.
final RecyclableBufferedInputStream bufferedStream;
final boolean ownsBufferedStream;
if (source instanceof RecyclableBufferedInputStream) {
bufferedStream = (RecyclableBufferedInputStream) source;
ownsBufferedStream = false;
} else {
bufferedStream = new RecyclableBufferedInputStream(source, byteArrayPool);
ownsBufferedStream = true;
}
// Use to retrieve exceptions thrown while reading.
// TODO(#126): when the framework no longer returns partially decoded Bitmaps or provides a
// way to determine if a Bitmap is partially decoded, consider removing.
ExceptionCatchingInputStream exceptionStream =
ExceptionCatchingInputStream.obtain(bufferedStream);
// Use to read data.
// Ensures that we can always reset after reading an image header so that we can still
// attempt to decode the full image even when the header decode fails and/or overflows our read
// buffer. See #283.
MarkEnforcingInputStream invalidatingStream = new MarkEnforcingInputStream(exceptionStream);
UntrustedCallbacks callbacks = new UntrustedCallbacks(bufferedStream, exceptionStream);
try {
return downsampler.decode(invalidatingStream, width, height, options, callbacks);
} finally {
exceptionStream.release();
if (ownsBufferedStream) {
bufferedStream.release();
}
}
}
然后最终是通过Downsampler这个类来完成创建显示的Bitmap的,如下方法:
public Resource<Bitmap> decode(InputStream is, int requestedWidth, int requestedHeight,
Options options, DecodeCallbacks callbacks) throws IOException {
Preconditions.checkArgument(is.markSupported(), "You must provide an InputStream that supports"
+ " mark()");
byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
bitmapFactoryOptions.inTempStorage = bytesForOptions;
DecodeFormat decodeFormat = options.get(DECODE_FORMAT);
DownsampleStrategy downsampleStrategy = options.get(DOWNSAMPLE_STRATEGY);
boolean fixBitmapToRequestedDimensions = options.get(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS);
try {
Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
downsampleStrategy, decodeFormat, requestedWidth, requestedHeight,
fixBitmapToRequestedDimensions, callbacks);
return BitmapResource.obtain(result, bitmapPool);
} finally {
releaseOptions(bitmapFactoryOptions);
byteArrayPool.put(bytesForOptions, byte[].class);
}
}
这里注意 BitmapFactory.Options的 inTempStorage 属性的用法,这个属性设置的话Bitmap的内存占用 将保存在这个字节数组中,而看这句代码byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class), 又是从回收池里获取的有木有,
那么就算创建显示的Bitmap 也是用了原来的内存占用,这样就少了很多的内存重新申请和销毁所损耗的时间,也就减少了卡顿所发生的概率。
ok,这就是Glide强于ImageLoader的缓存的策略了。 有没有更好的策略呢,当然你可以把Bitmap占用内存改到native中,
Fresco就是这么搞的,但是android O又将Bitmap内存占用 改到native中了,这的确比较蛋疼。
1.Glide中的缓存介绍:
本文主要介绍了如何配置和管理Glide中的缓存, 这里只是进行了整理和汇总。
Glide支持图片的二级缓存(并不是三级缓存,因为从网络加载并不属于缓存),即内存缓存和磁盘缓存。
磁盘缓存
一般的图片缓存指的就是磁盘缓存,把网络上的图片缓存到本地,这样就不需要每次都从网络加载,既提高了加载速度,又为用户节省了流量。
Glide在默认情况下是开启磁盘缓存的,而且提供了丰富的API来让开发者自己配置和管理磁盘缓存。
缓存位置和大小
开发者可以通过构建一个自定义的GlideModule来配置Glide磁盘缓存的位置和大小。最简单的方法如下:
public class DiskCacheMoudle implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
builder.setDiskCache(
new InternalCacheDiskCacheFactory(context, "glide_cache", 100 * 1024 * 1024));
//builder.setDiskCache(
// new ExternalCacheDiskCacheFactory(context, "glide_cache", 100 * 1024 * 1024));
}
@Override
public void registerComponents(Context context, Glide glide) {
}
}
其中InternalCache和ExternalCache都最多接收3个参数:第一个参数为Context,没啥好说的;第二个为缓存的目录名称;第三个为缓存大小,单位是Byte。
它们之间唯一的不同就在于InternalCache构建的缓存是在应用的内部储存,而ExternalCache则是在外部储存。
内部储存中的缓存文件是其他应用程序是无法获取到的,更加安全。关于内部储存和外部储存的更多内容,请点击这里查看官方文档。
如果不想把缓存放在上面的两个位置怎么办?Glide当然也支持,具体通过DiskLruCacheFactory来实现:
builder.setDiskCache(
new DiskLruCacheFactory(new DiskLruCacheFactory.CacheDirectoryGetter() {
@Override
public File getCacheDirectory() {
return getMyCacheLocationBlockingIO();
}
}), 100 * 1024 * 1024);
Note: getMyCacheLocationBlockingIO方法返回的文件不能为空,而且必须是一个已经创建好的文件目录,不可以是文件。
缓存策略
与其他图片加载库的缓存机制不同,Glide缓存图片时 默认只缓存最终加载的那张图片。
举个栗子,你要加载的图片分辨率为1000x1000,但是最终显示该图片的ImageView大小只有500x500,那么Glide就会只缓存500x500的小图。
这也是在从磁盘缓存中加载图片时Glide比Picasso快的原因。
Glide目前提供了四种缓存策略:
- DiskCacheStrategy.NONE 不缓存文件
- DiskCacheStrategy.SOURCE 只缓存原图
- DiskCacheStrategy.RESULT 只缓存最终加载的图(默认的缓存策略)
- DiskCacheStrategy.ALL 同时缓存原图和结果图
缓存算法
在Glide中磁盘缓存默认使用的是LRU(Least Recently Used)算法。如果你想使用其他的缓存算法,就只能通过实现DiskCache接口来完成了。
内存缓存
使用内存缓存可以获得更快的图片加载速度,因为减少了耗时的IO操作。
众所周知,Bitmap是Android中的内存大户,频繁的创建和回收Bitmap必然会引起内存抖动。
Glide中有一个叫做BitmapPool的类,可以复用其中的Bitmap对象,从而避免Bitmap对象的创建,减小内存开销。
当配置内存缓存时,我们也应该同时配置BitmapPool的大小。具体方法也是通过自定义的GlideModule来实现的:
builder.setMemoryCache(new LruResourceCache(yourSizeInBytes));
builder.setBitmapPool(new LruBitmapPool(sizeInBytes));
一般情况下,开发者是不需要自己去指定它们的大小的,因为Glide已经帮我们做好了。
默认的内存缓存和bitmapPool的大小由MemorySizeCalculator根据 当前设备的屏幕大小 和可用内存计算得到。同时Glide还支持动态的缓存大小调整,
在存在大量图片的Activity/Fragment中,开发者可以通过setMemoryCategory方法, 来提高Glide的内存缓存大小,从而加快图片的加载速度。
Glide.get(context).setMemoryCategory(MemoryCategory.HIGH);
MemoryCategory有3个值可供选择:
- MemoryCategory.HIGH(初始缓存大小的1.5倍)
- MemoryCategory.NORMAL(初始缓存大小的1倍)
- MemoryCategory.LOW(初始缓存大小的0.5倍)
在有些情况下我们不希望做内存缓存(比如加载GIF图片),这个时候可以调用skipMemoryCache(true)方法跳过内存缓存。
如何缓存动态Url的图片?
一般情况下我们从网络上获取到的图片Url都是静态的,即一张图片对应一个Url。那么如果是一张图片对应多个Url呢?缓存不就没有意义了。
因为图片加载库都是拿图片的Url来作为缓存的key的,Glide也不例外,只是会更加复杂一些。如果你开启了Glide的log,就会在控制台看到Glide是如何指定缓存key的。关于如何打开log,请参考这篇文章。
一般来说,Glide的key由图片的url、view的宽和高、屏幕的尺寸大小和signature组成。在什么情况下才会出现动态的Url呢?
一个很典型的例子就是因为图片的安全问题在原来图片的Url后面加上访问凭证。访问凭证与时间关联,这样一来,在不同时间同一图片的Url就会不同,缓存就会失效。以七牛的私有空间为例,我们来看看如何去缓存这类图片。从七牛关于私有空间的文档中可以得到:最终的Url = 原Url + ?e=过期时间 + token=下载凭证。
那么就只需要在Glide缓存时将Url中“?”后面的字符串截去就可以了。
首先新建一个叫做QiNiuImage的类:
public class QiNiuImage {
private final String imageUrl;
public QiNiuImage(String imageUrl) {
this.imageUrl = imageUrl;
}
public String getImageUrl() {
return imageUrl;
}
public String getImageId() {
if (imageUrl.contains("?")) {
return imageUrl.substring(0, imageUrl.lastIndexOf("?"));
} else {
return imageUrl;
}
}
}
其中getImageUrl方法返回真实的Url,getImageId方法返回未添加下载凭证前的Url。
然后再自定义一个实现ModelLoader接口的QiNiuImageLoader:
public class QiNiuImageLoader implements StreamModelLoader<QiNiuImage> {
@Override
public DataFetcher<InputStream> getResourceFetcher(final QiNiuImage model, int width,
int height) {
return new HttpUrlFetcher(new GlideUrl(model.getImageUrl())) {
@Override
public String getId() {
return model.getImageId();
}
};
}
public static class Factory implements ModelLoaderFactory<QiNiuImage, InputStream> {
@Override
public ModelLoader<QiNiuImage, InputStream> build(Context context,
GenericLoaderFactory factories) {
return new QiNiuImageLoader();
}
@Override
public void teardown() { /* no op */ }
}
}
其中HttpUrlFetcher的getId方法就是组成缓存的key的重要部分。这也是我们的核心原理。
将这个ModelLoader注册到GlideModule中,并在AndroidManifest.xml中注册:
public class QiNiuModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
}
@Override
public void registerComponents(Context context, Glide glide) {
glide.register(QiNiuImage.class, InputStream.class, new QiNiuImageLoader.Factory());
}
}
<meta-data
android:name="com.yourpackagename.QiNiuModule"
android:value="GlideModule"/>
最后只需要在加载此类图片时,使用下面这段代码就可以了。即使图片的token更换了也不会重新从网络上下载而是直接读取本地缓存。
Glide.with(context)
.load(new QiNiuImage(imageUrl)
.into(imageView);