Glide拆解1-图片LRU缓存、复用池
Glide 使用简明的流式语法API,大多数情况下,可能完成图片的设置你只需要:
Glide.with(activity) .load(url) .into(imageView);
默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存:
- 活动资源 (Active Resources)
- 内存缓存 (Memory Cache)
- 资源类型(Resource Disk Cache)
- 原始数据 (Data Disk Cache)
image.png活动资源:如果当前对应的图片资源正在使用,则这个图片会被Glide放入活动缓存。
内存缓存:如果图片最近被加载过,并且当前没有使用这个图片,则会被放入内存中
资源类型: 被解码后的图片写入磁盘文件中,解码的过程可能修改了图片的参数(如:inSampleSize、inPreferredConfig)
原始数据: 图片原始数据在磁盘中的缓存(从网络、文件中直接获得的原始数据)
在调用into之后,Glide会首先从Active Resources查找当前是否有对应的活跃图片,没有则查找内存缓存,没有则查找资源类型,没有则查找数据来源。
相较于常见的内存+磁盘缓存,Glide将其缓存分成了4层。
第一层 活动资源
当需要加载某张图片能够从内存缓存中获得的时候,在图片加载时主动将对应图片从内存缓存中移除,加入到活动资源中。这样也可以避免因为达到内存缓存最大值或者系统内存压力导致的内存缓存清理,从而释放掉活动资源中的图片(recycle)。
活动资源中是一个”引用计数"的图片资源的弱引用集合。因为同一张图片可能在多个地方被同时使用,每一次使用都会将引用计数+1,而当引用计数为0时候,则表示这个图片没有被使用也就是没有强引用了。这样则会将图片从活动资源中移除,并加入内存缓存。
/**
* 这个资源没有正在使用了
* 将其从活动资源移除
* 重新加入到内存缓存中
*
* @param key
* @param resource
*/
@Override
public void onResourceReleased(Key key, Resource resource) {
activeResource.deactivate(key);
//从活动缓存中移除也就是活动缓存中引用计数为0的时候需要加入内存缓存
lruMemoryCache.put(key, resource);
}
第二层 内存缓存
内存缓存默认使用LRU(缓存淘汰算法/最近最少使用算法),当资源从活动资源移除的时候,会加入此缓存。使用图片的时候会主动从此缓存移除,加入活动资源。
LRU在Android support-v4中提供了LruCache工具类。
/**
* @param maxSize for caches that do not override {@link #sizeOf}, this is
* the maximum number of entries in the cache. For all other caches,
* this is the maximum sum of the sizes of the entries in this cache.
*/
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
LRUCache通过看源码即可发现,内部实现很简单,通过双向链表LinkedHashMap来进行实现的,其中设置accessOrder为true时,会进行自动排序,也就是get之后item会被提到队列头部;
LruCache会在每次get/put的时候判断数据如果达到了maxSize,则会优先删除tail尾端的数据。
image.png
第三、四层 磁盘缓存
磁盘缓存同样使用LRU算法。
Resource缓存的是经过解码后的图片,如果再使用就不需要再去进行解码配置(BitmapFactory.Options),加快获得图片速度。
比如原图是一个100x100的ARGB_8888图片,在首次使用的时候需要的是50x50的RGB_565图片,那么Resource将50x50 RGB_565缓存下来,
再次使用此图片的时候就可以从 Resource 获得。不需要去计算inSampleSize(缩放因子)。
Data 缓存的则是图像原始数据。
Bitmap复用
如果缓存都不存在,那么会从源地址获得图片(网络/文件)。而在解析图片的时候会需要可以获得BitmapPool(复用池),达到复用的效果。
image.png
复用效果如上。在未使用复用的情况下,每张图片都需要一块内存。而使用复用的时候,如果存在能被复用的图片会重复使用该图片的内存。
所以复用并不能减少程序正在使用的内存大小。Bitmap复用,解决的是减少频繁申请内存带来的性能(抖动、碎片)问题。
图片复用依赖于inBitmap和mutible,具体原理可查看google文档,后面在讲BitmapPool在Glide中的使用也会提到。
https://developer.android.google.cn/topic/performance/graphics/manage-memory#java
Google给出的案例可以看出:
使用方式为在解析的时候设置Options的inBitmap属性。
Bitmap的inMutable需要为true。
Android 4.4及以上只需要被复用的Bitmap的内存必须大于等于需要新获得Bitmap的内存,则允许复用此Bitmap。
4.4以下(3.0以上)则被复用的Bitmap与使用复用的Bitmap必须宽、高相等并且使用复用的Bitmap解码时设置的inSampleSize为1,才允许复用。
因此Glide中,在每次解析一张图片为Bitmap的时候(磁盘缓存、网络/文件)会从其BitmapPool中查找一个可被复用的Bitmap。
BitmapPool是Glide中的Bitmap复用池,同样适用LRU来进行管理。
当一个Bitmap从内存缓存 被动的被移除(内存紧张、达到maxSize)的时候并不会被recycle。而是加入这个BitmapPool,只有从这个BitmapPool 被动
被移除的时候,Bitmap的内存才会真正被recycle释放。
基础知识已经讲完了,接下来我们开始拆解Glide。
从源码层级拆解Glide的缓存结构
1、首先是活动缓存所存储的对象
public class Resource {
private Bitmap bitmap;
//引用计数
private int acquired;
//对外部的回调,当引用计数为0的时候通过该回调需要放入内存缓存
private ResourceListener listener;
private Key key;
//接下来描述一下主要方法,也就是应用计数的加减
/**
* 引用计数+1
*/
public void acquire() {
if (bitmap.isRecycled()) {
throw new IllegalStateException("Acquire a recycled resource");
}
++acquired;
}
/**
* 引用计数-1
*/
public void release() {
if (--acquired == 0) {
//如果图片不再使用需要回调出去
listener.onResourceReleased(key, this);
}
}
/**
* 当acquired 为0的时候 回调 onResourceReleased
*/
public interface ResourceListener {
void onResourceReleased(Key key, Resource resource);
}
}
2、活动缓存—ActivityResource
由于之前说到,活动缓存实现的无非就是
- 引用计数
- 弱引用
- 不再使用时回调出去
- 使用时再从内存当中put进来
看源码:
public class ActiveResource {
//队列,由于存储结构为弱引用,因此需要通过队列进行管理
private ReferenceQueue<Resource> queue;
//回调接口,当图片不再使用,从活动缓存中移除时需要回调出去
private final Resource.ResourceListener resourceListener;
// 弱应用 (强软弱虚可自行查资料了解)
private Map<Key, ResourceWeakReference> activeResources = new HashMap<>();
// 当图片被GC回收之后,会被加入queue,这个时候通过保护线程进行监听,实时更新activityResources,确保activityResources中都是有效的图片资源
private Thread cleanReferenceQueueThread;
//标志位,通过该标志位来判断是否需要关闭当前保护线程
private boolean isShutdown;
//接下来也是对主要方法做讲解
/**
* 加入活动缓存
*
* @param key
* @param resource
*/
public void activate(Key key, Resource resource) {
resource.setResourceListener(key, resourceListener);
activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
}
* 移除活动缓存
*/
public Resource deactivate(Key key) {
ResourceWeakReference reference = activeResources.remove(key);
if (reference != null) {
return reference.get();
}
return null;
}
/**
* 引用队列,通知我们弱引用被回收了
* 让我们得到通知的作用
*
* @return
*/
private ReferenceQueue<Resource> getReferenceQueue() {
if (null == queue) {
queue = new ReferenceQueue<>();
cleanReferenceQueueThread = new Thread() {
@Override
public void run() {
while (!isShutdown) {
try {
//被回收掉的引用
ResourceWeakReference ref = (ResourceWeakReference) queue.remove();
activeResources.remove(ref.key);
} catch (InterruptedException e) {
}
}
}
};
cleanReferenceQueueThread.start();
}
return queue;
}
void shutdown() {
isShutdown = true;
if (cleanReferenceQueueThread != null) {
cleanReferenceQueueThread.interrupt();
try {
//5s 必须结束掉线程
cleanReferenceQueueThread.join(TimeUnit.SECONDS.toMillis(5));
if (cleanReferenceQueueThread.isAlive()) {
throw new RuntimeException("Failed to join in time");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
static final class ResourceWeakReference extends WeakReference<Resource> {
final Key key;
public ResourceWeakReference(Key key, Resource referent,
ReferenceQueue<? super Resource> queue) {
super(referent, queue);
this.key = key;
}
}
}
3、内存缓存—MemoryCache
由于之前说到,内存缓存实现的无非就是
- 活动缓存不用了抛给内存缓存--put
- 活动缓存没找到,从内存缓存中寻找,找的之后再加入活动缓存当中---remove(该remove不进行资源释放)
- 由于内存缓存使用LRUCache,因此存在达到最大存储的时候会移除末尾的item,这个时候并不会马上执行recycler操作,而是会将其加入BitmapPool当中,进行内存复用
/**
* Created by Administrator on 2018/5/4.
*/
public interface MemoryCache {
interface ResourceRemoveListener{
void onResourceRemoved(Resource resource);
}
Resource put(Key key, Resource resource);
void setResourceRemoveListener(ResourceRemoveListener listener);
Resource remove2(Key key);
}
具体实现
public class LruMemoryCache extends LruCache<Key, Resource> implements MemoryCache {
private ResourceRemoveListener listener;
private boolean isRemoved;
public LruMemoryCache(int maxSize) {
super(maxSize);
}
//由于父类为LruCache,需要重写sizeof,因为默认返回为1
@Override
protected int sizeOf(Key key, Resource value) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
//当在4.4以上手机复用的时候 需要通过此函数获得占用内存
//由于内存复用的原因,可能当前图片占用内存大于实际图片大小,需要获取图片实际占用内存大小
return value.getBitmap().getAllocationByteCount();
}
//getByteCount返回的是图片实际大小
return value.getBitmap().getByteCount();
}
//由于父类为LruCache,需要重新entryRemoved,根据实际情况判断是否需要recycler
@Override
protected void entryRemoved(boolean evicted, Key key, Resource oldValue, Resource newValue) {
//给复用池使用
if (null != listener && null != oldValue && !isRemoved) {
listener.onResourceRemoved(oldValue);
}
}
//主动remove的情况不进行回调出去
@Override
public Resource remove2(Key key) {
// 如果是主动移除的不会掉 listener.onResourceRemoved
isRemoved = true;
Resource remove = remove(key);
isRemoved = false;
return remove;
}
/**
* 资源移除监听
*
* @param listener
*/
@Override
public void setResourceRemoveListener(ResourceRemoveListener listener) {
this.listener = listener;
}
}
4、Bitmap复用—BitmapPool
由于之前说到,内存复用实现的无非就是
- 读--get
- 写–put
接口实现为
public interface BitmapPool {
void put(Bitmap bitmap);
/**
* 获得一个可复用的Bitmap
* 三个参数计算出 内存大小
* @param width
* @param height
* @param config 主要用来区别是ARGB8888还是RGB565,分别对应4字节和2字节
* @return
*/
Bitmap get(int width,int height,Bitmap.Config config);
}
Glide当中BitmapPool的实现也是基于LRUCache
public class LruBitmapPool extends LruCache<Integer, Bitmap> implements BitmapPool {
//通过isRemoved标记位来防止主动移除的时候调用entryRemoved引起的错误逻辑调用
private boolean isRemoved;
// 负责筛选
NavigableMap<Integer, Integer> map = new TreeMap<>();
private final static int MAX_OVER_SIZE_MULTIPLE = 2;
public LruBitmapPool(int maxSize) {
super(maxSize);
}
/**
* 将Bitmap放入复用池
*
* @param bitmap
*/
@Override
public void put(Bitmap bitmap) {
//isMutable 是复用的标记 必须是true
if (!bitmap.isMutable()) {
bitmap.recycle();
return;
}
int size = bitmap.getAllocationByteCount();
//如果复用池大小不够,则舍弃当前bitmap
if (size >= maxSize()) {
bitmap.recycle();
return;
}
put(size, bitmap);
map.put(size, 0);
}
/**
* 获得一个可复用的Bitmap
*/
@Override
public Bitmap get(int width, int height, Bitmap.Config config) {
//新Bitmap需要的内存大小 (只关心 argb8888和RGB65)
int size = width * height * (config == Bitmap.Config.ARGB_8888 ? 4 : 2);
//获得等于 size或者大于size的key
Integer key = map.ceilingKey(size);
//从key集合从找到一个>=size并且 <= size*MAX_OVER_SIZE_MULTIPLE
if (null != key && key <= size * MAX_OVER_SIZE_MULTIPLE) {
isRemoved = true;
Bitmap remove = remove(key);
isRemoved = false;
return remove;
}
return null;
}
@Override
protected int sizeOf(Integer key, Bitmap value) {
return value.getAllocationByteCount();
}
@Override
protected void entryRemoved(boolean evicted, Integer key, Bitmap oldValue, Bitmap newValue) {
map.remove(key);
//非主动移除时需要recycle
if (!isRemoved)
oldValue.recycle();
}
}
到这里已经实现了Glide多级缓存所需要的相应存储结构
Demo
public class CacheTest implements Resource.ResourceListener, MemoryCache.ResourceRemoveListener {
LruMemoryCache lruMemoryCache;
ActiveResource activeResource;
BitmapPool bitmapPool;
public Resource test(Key key) {
bitmapPool = new LruBitmapPool(10);
//内存缓存
lruMemoryCache = new LruMemoryCache(10);
lruMemoryCache.setResourceRemoveListener(this);
//活动资源缓存
activeResource = new ActiveResource(this);
/**
* 第一步 从活动资源中查找是否有正在使用的图片
*/
Resource resource = activeResource.get(key);
if (null != resource) {
//当不使用的时候 release
resource.acquire();
return resource;
}
/**
* 第二步 从内存缓存中查找
*/
resource = lruMemoryCache.get(key);
if (null != resource) {
//1.为什么从内存缓存移除?
// 因为lru可能移除此图片 我们也可能recycle掉此图片
// 如果不移除,则下次使用此图片从活动资源中能找到,但是这个图片可能被recycle掉了
lruMemoryCache.remove2(key);
resource.acquire();
activeResource.activate(key, resource);
return resource;
}
return null;
}
/**
* 这个资源没有正在使用了
* 将其从活动资源移除
* 重新加入到内存缓存中
*
* @param key
* @param resource
*/
@Override
public void onResourceReleased(Key key, Resource resource) {
activeResource.deactivate(key);
lruMemoryCache.put(key, resource);
}
/**
* 从内存缓存被动移除 回调
* 放入 复用池
*
* @param resource
*/
@Override
public void onResourceRemoved(Resource resource) {
bitmapPool.put(resource.getBitmap());
}
}