原文链接:Glide核心设计二:缓存管理
引言
Glide作为一个优秀的图片加载框架,缓存管理是必不可少的一部分,这篇文章主要通过各个角度、从整体设计到代码实现,深入的分析Glide的缓存管理模块,力求在同类分析Glide缓存的分析文章中脱颖而出。关于Glide的生命周期绑定,可查看Glide系列文章Glide核心设计一:皮皮虾,我们走。
前提
- 本文分析Glide缓存管理,将以使用Glide加载网络图片为例子,如加载本地图片、Gif资源等使用不是本文的重点。因不管是何种使用方式,缓存模块都是一样的,只抓住网络加载图片这条主线,逻辑会更清晰。
- 本文将先给出Glide缓存管理整体设计的结论,然后再分析源码。
整体设计
缓存类型
Glide的缓存类型分为两大类,一类是Resource缓存,一类是Bitmap缓存。
Resource缓存
为什么需要缓存图片Resource,很好理解,因为图片从网络加载,将图片缓存到本地,当需要再次使用时,直接从缓存中取出而无需再次请求网络。
三层缓存
Glide在缓存Resource使用三层缓存,包括:
- 一级缓存:缓存被回收的资源,使用LRU算法(Least Frequently Used,最近最少使用算法)。当需要再次使用到被回收的资源,直接从内存返回。
- 二级缓存:使用弱引用缓存正在使用的资源。当系统执行gc操作时,会回收没有强引用的资源。使用弱引用缓存资源,既可以缓存正在使用的强引用资源,也不阻碍系统需要回收无引用资源。
- 三级缓存:磁盘缓存。网络图片下载成功后将以文件的形式缓存到磁盘中。
Bitmap缓存
Bitmap所占内存大小
Bitmap所占的内存大小由三部分组成:图片的宽度分辨率、高度分辨率和Bitmap质量参数。公式是:Bitmap内存大小 = (宽pix长pix)质量参数所占的位数。单位是字节B。
Bitmap压缩质量参数
质量参数决定每一个像素点用多少位(bit)来显示:
- ALPHA_8就是Alpha由8位组成(1B)
- ARGB_4444就是由4个4位组成即16位(2B)
- ARGB_8888就是由4个8位组成即32位(4B)
- RGB_565就是R为5位,G为6位,B为5位共16位(2B)
Glide默认使用RGB_565,比系统默认使用的ARGB_8888节省一半的资源,但RGB_565无法显示透明度。
举个例子:在手机上显示100pix*200pix的图片,解压前15KB,是使用Glide加载(默认RGB_565)Bitmap所占用的内存是:(100x200)x2B = 40000B≈40Kb,比以文件的形成存储的增加不少,因为png、jpg等格式的图片经过压缩。正因为Bitmap比较消耗内存,例如使用Recyclerview等滑动控件显示大量图片时,将大量的创建和回收Bitmap,导致内存波动影响性能。
Bitmap缓存算法
在Glide中,使用BitmapPool来缓存Bitmap,使用的也是LRU算法。当需要使用Bitmap时,从Bitmap的池子中取出合适的Bitmap,若取不到合适的,则再新创建。当Bitmap使用完后,不直接调用Bitmap.recycler()回收,而是放入Bitmap的池子。
缓存的Key类型
Glide的缓存使用
- 从对比中可看出,Resource三层缓存所使用的key的构造形式是一样的,包括图片id(图片的Url地址),宽高等参数来标识。对于其他参数,举一个例子理解:图片资源从网络加载后,经过解码(decode)、缓存到磁盘、从磁盘中取出、变换资源(加圆角等,transformation)、磁盘缓存变换后的图片资源、转码(transcode)显示。
- Bitmap的缓存Key的构造相对简单得多,由长、宽的分辨率以及图片压缩参数即可唯一标示一个回收的Bitmap。当需要使用的bitmap时,在BitmapPool中查找对应的长、宽和config都一样的Bitmap并返回,而无需重新创建。
Resource缓存流程
Resource包括三层缓存,通过流程图看它们之间的关系:
因为内存缓存优于磁盘缓存,所以当需要使用资源时,先从内存缓存中查找(一级缓存和二级缓存都是内存缓存,其功能不一样,一级缓存用于在内存中缓存不是正在使用的资源,二级缓存是保存正在使用的资源),再从磁盘缓存中查找。若都找不到,则从网络加载。
滑动控件多图的性能优化
不论是Resource还是Bitmap缓存,若显示的仅是部分照片,并且不存在频繁使用的场景,则使用Glide没有太大的优势。设计缓存的目的就是为了在重复显示时,更快、更省的显示图片资源。Glide有针对ListView、Recyclerview等控件加载多图时进行优化。此处讨论最常见的场景:Recyclerview显示多图,简略图如下。
如上图所示,当图5划入界面时,会复用图一的Item,设置新的图片之前,会先清空原有图片的资源,清空时会把Resource资源放入一级缓存待将来复用,同时会将回收的Bitmap放入BitmapPool中;当图5向下隐藏,图一出现时,图5的资源会放到一级缓存中,图一的资源则从一级缓存中取出,无须重新网络请求,同时所需要的Bitmap也无须重新创建,直接复用。
LRU算法
BitmapPool的LRU算法流程图如下:
类图
在进行代码分析前,先给出跟Glide缓存管理相关的类图(省略类的大部分变量和方法)。
Glide缓存管理类图大图地址
代码实现
根据以上的Glide缓存管理的结论及类图,可自主跟源码,跳过以下内容。
Glide.with(Context).load(String).into(ImageView)
Glide.with(Context)
返回RequestManager,主要实现和Fragment、Activity生命周期的绑定,详情请看Glide核心设计一:皮皮虾,我们走。
.load(String)
RequestManager的load(String)方法返回DrawableTypeRequest,根据图片地址返回一个用于创建图片请求的Request的Builder,代码如下:
public DrawableTypeRequest<String> load(String string) {
return (DrawableTypeRequest<String>) fromString().load(string); //调用fromString()和load()方法
}
fromString()方法调用loadGeneric()方法,代码如下:
public DrawableTypeRequest<String> fromString() {
return loadGeneric(String.class);
}
private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
ModelLoader<T, InputStream> streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
ModelLoader<T, ParcelFileDescriptor> fileDescriptorModelLoader =
Glide.buildFileDescriptorModelLoader(modelClass, context);
if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
throw new IllegalArgumentException("Unknown type " + modelClass + ". You must provide a Model of a type for"
+ " which there is a registered ModelLoader, if you are using a custom model, you must first call"
+ " Glide#register with a ModelLoaderFactory for your custom model class");
}
return optionsApplier.apply( //传递的参数中创建了一个DrawableTypeRequest并返回该对象
new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,
glide, requestTracker, lifecycle, optionsApplier));
}
DrawableTypeRequest的load()方法如下:
public DrawableRequestBuilder<ModelType> load(ModelType model) {
super.load(model);
return this;
}
DrawableTypeRequest父类是DrawableRequestBuilder,父类的父类是GenericRequestBuilder,调用super.load()方法如下:
public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> load(ModelType model) {
this.model = model;
isModelSet = true;
return this;
}
以上代码可知,缓存管理的主要实现代码并不在.load(Sting)代码,接下来继续分析.into(ImageView)代码。
.into(ImageView)
GenericRequestBuilder的into(ImageView)代码如下:
public Target<TranscodeType> into(ImageView view) {
Util.assertMainThread();
if (view == null) {
throw new IllegalArgumentException("You must pass in a non null View");
}
if (!isTransformationSet && view.getScaleType() != null) {
switch (view.getScaleType()) { //根据图片的scaleType做相应处理
case CENTER_CROP:
applyCenterCrop();
break;
case FIT_CENTER:
case FIT_START:
case FIT_END:
applyFitCenter();
break;
//$CASES-OMITTED$
default:
// Do nothing.
}
}
//调用buildImageViewTarget()方法创建了一个Target类型的对象
return into(glide.buildImageViewTarget(view, transcodeClass));
}
以上代码主要有两个功能:
- 根据ScaleType进行图片的变换
- 将ImageView转换成一个Target
继续查看into(Target)的代码:
public <Y extends Target<TranscodeType>> Y into(Y target) {
Util.assertMainThread();
if (target == null) {
throw new IllegalArgumentException("You must pass in a non null Target");
}
if (!isModelSet) {
throw new IllegalArgumentException("You must first set a model (try #load())");
}
Request previous = target.getRequest(); //获取请求体Request
if (previous != null) { //若ImageView是复用过的,则previous不为空
previous.clear(); //调用clear()方法清空ImageView上的图片资源,此方法会将回收的Resource放入内存缓存中,并不在内存中清空该资源。
requestTracker.removeRequest(previous); //移除老的请求
previous.recycle(); //回收Request使用
}
Request request = buildRequest(target); //获取新的Request
target.setRequest(request); //将新的request设置到target中
lifecycle.addListener(target); //添加生命周期的监听
requestTracker.runRequest(request); //启动Request
return target;
}
以上代码,主要将图片加载的Request绑定到Target中,若原有Target具有旧的Request,得先处理旧的Request,再绑定上新的Request。target.setRequest()和target.getRequest()最终会调用ViewTarget的setRequest()方法和getRequest()方法,代码如下:
public void setRequest(Request request) {
setTag(request);
}
private void setTag(Object tag) {
if (tagId == null) {
isTagUsedAtLeastOnce = true;
view.setTag(tag);//调用view的setTag方法,将Request和view做绑定
} else {
view.setTag(tagId, tag);//调用view的setTag方法,将Request和view做绑定
}
}
public Request getRequest() {
Object tag = getTag(); //获取view 的tag
Request request = null;
if (tag != null) {
if (tag instanceof Request) { //若该tag是Request的一个实例
request = (Request) tag;
} else { //用户不能给view设置tag,因为该view的tag要用于保存Glide的Request对象,否则抛出异常
throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting");
}
}
return request;
}
以上代码可知,Request通过setTag的方式和View进行绑定,当View是复用时,则Request不为空,通过Request可对原来的资源进行缓存与回收。此处通过View的setTag()方法绑定Request,可谓妙用。
以上代码创建了一个Request,requestTracker.runRequest(request);启动了Request,调用Request的begin()方法,该Request实例是GenericRequest,begin()代码如下:
public void begin() {
startTime = LogTime.getLogTime();
if (model == null) {
onException(null);
return;
}
status = Status.WAITING_FOR_SIZE; //设置等待图片size的宽高状态
if (Util.isValidDimensions(overrideWidth, overrideHeight)) { //必须要确定图片的宽高,确定了则调用onSizeReady
onSizeReady(overrideWidth, overrideHeight);
} else { //设置回调,监听界面的绘制,当检测到宽高有效时,回调onSizeReady方法
target.getSize(this);
}
if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
target.onLoadStarted(getPlaceholderDrawable());
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished run method in " + LogTime.getElapsedMillis(startTime));
}
}
加载图片前,必须要确定图片的宽高,因为需要根据确定的宽高来获取资源。onSizeReady代码如下:
public void onSizeReady(int width, int height) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
if (status != Status.WAITING_FOR_SIZE) {//宽高没准备好,返回
return;
}
status = Status.RUNNING; //状态改为加载运行中
width = Math.round(sizeMultiplier * width);
height = Math.round(sizeMultiplier * height);
ModelLoader<A, T> modelLoader = loadProvider.getModelLoader();
final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);
if (dataFetcher == null) {
onException(new Exception("Failed to load model: \'" + model + "\'"));
return;
}
ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
}
loadedFromMemoryCache = true;
//真正的加载任务交给engine
loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
priority, isMemoryCacheable, diskCacheStrategy, this);
loadedFromMemoryCache = resource != null;
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
}
以上代码可知,在确定宽高后,将图片加载的任务交给类型为Engine的对象engine,并调用其load方法,代码如下:
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) {
Util.assertMainThread();
long startTime = LogTime.getLogTime();
final String id = fetcher.getId(); //该id为图片的网络地址
//缓存key的组成部分,使用工厂模式
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
//使用一级缓存,从回收的内存缓存中查找EngineResource
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;
}
EngineJob current = jobs.get(key);
if (current != null) {//该任务已经在执行,只需要添加回调接口,在任务执行完后调用接口告知即可
current.addCallback(cb);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
//一级缓存和二级缓存都不命中的情况下,启动新的任务
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);//创建EngineJob
DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
transcoder, diskCacheProvider, diskCacheStrategy, priority); //创建DecodeJob
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable); //启动EngineRunnable runnable,使用线程池FifoPriorityThreadPoolExecutor管理
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}
分析至此,我们终于看到实现一级缓存和二级缓存的相关代码,可以猜测三级缓存的实现跟EngineRunnable有关。engineJob.start(runnable)会启动EngineRunnable的start()方法。代码如下:
public void run() {
if (isCancelled) {
return;
}
Exception exception = null;
Resource<?> resource = null;
try {
resource = decode(); //调用decode()方法
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Exception decoding", e);
}
exception = e;
}
if (isCancelled) { //请求被取消
if (resource != null) {
resource.recycle();
}
return;
}
if (resource == null) { //加载失败
onLoadFailed(exception);
} else { //加载成功
onLoadComplete(resource);
}
}
查看decode()方法如下:
private Resource<?> decode() throws Exception {
if (isDecodingFromCache()) {
return decodeFromCache(); //从磁盘缓存中获取
} else {
return decodeFromSource(); //从网络中获取资源
}
}
至此,我们看到磁盘缓存和网络请求获取图片资源的代码。查看onLoadFailed()的代码逻辑可知,默认先从磁盘获取,失败则从网络获取。
BitmapPool缓存逻辑
以上就是Resource三层缓存的代码,接下来看BitmapPool的缓存实现代码。
在decodeFromSource()的代码中,会返回一个类型为BitmapResource的对象。在RecyclerView的例子中,当ImageView被复用时,会在Tag中取出Request,调用request.clear()代码。该方法最终会调用BitmapResource的recycler()方法,代码如下:
public void recycle() {
if (!bitmapPool.put(bitmap)) {
bitmap.recycle();
}
}
该代码调用bitmapPool.put(bitmap),bitmapPool的实例是LruBitmapPool代码如下:
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);//该strategy的实例是Lru算法
tracker.add(bitmap); //log跟踪
puts++; //缓存的bitmap数量标记加一
currentSize += size;//缓存bitmap的总大小
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
}
dump(); //仅用于Log
evict(); //判断是否超出指定的内存大小,若超出则移除
return true;
}
可以看出,正常情况下调用put方法返回true,证明缓存该Bitmap成功,缓存成功则不调用bitmap.recycler()方法。当需要使用Bitmap时,先从Bitmap中查找是否有符合条件的Bitmap。在RecyclerView中使用Glide的例子中,将大量复用宽高及Bitmap.Config都相等的Bitmap,极大的优化系统内存性能,减少频繁的创建回收Bitmap。
小结
Glide的缓存管理至此就分析完了,主要抓住Resource和Bitmap的缓存来讲解。在代码的阅读中还发现了工厂、装饰者等设计模式。Glide的解耦给开发者提供很大的便利性,可根据自身需求设置缓存参数,例如默认Bitmap.Config、BitmapPool缓存大小等。最后,针对Glide的缓存设计,提出几点小建议:
- Glide虽然默认使用的Bitmap.Config是RGB_565,但在进行transform(例如圆角显示图片)时往往默认是ARGB_8888,因为RGB_565没有透明色,此时可重写圆角变换的代码,继续使用RGB_565,同时给canvas设置背景色。
- BitmapPool缓存的Bitmap大小跟Bitmap的分辨率也有关系,在加载图片的过程中,可调用.override(width, height)指定图片的宽高,再调整ImageView控件的大小适应布局。
- Resource的一级缓存和Bitmap都是内存缓存,虽然极大的提升了复用,但也会导致部分内存在系统执行GC时无法释放。若内存达到手机性能瓶颈,应在合适的时机调用Glide.get(this).clearMemory()释放内存。