这篇文章,专门吐槽下glide的三点不合理设计(至少个人认为不合理)
相信有不少项目,在线上环境,都有碰到类似的崩溃吧
java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@f08cd43
at android.graphics.Canvas.throwIfCannotDraw(Canvas.java:1270)
at android.graphics.Canvas.drawBitmap(Canvas.java:1404)
at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:544)
at android.widget.ImageView.onDraw(ImageView.java:1244)
at android.view.View.draw(View.java:16669)
at android.view.View.updateDisplayListIfDirty(View.java:15622)
崩溃log只有系统层面的堆栈,这个问题在我之前文章已经有分析过了,原因是因为glide主动回收了bitmap导致的(当然也有可能是其他代码异常,不过我之前项目线上的这种崩溃,最终排查,都是glide导致的)
先来看下glide内部回收bitmap的代码
# com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool#trimToSize
private synchronized void trimToSize(long size) {
while (currentSize > size) {
final Bitmap removed = strategy.removeLast();
if (removed == null) {
currentSize = 0;
return;
}
tracker.remove(removed);
currentSize -= strategy.getSize(removed);
evictions++;
dump();
//就是这里,主动recycle
removed.recycle();
}
}
当glide的内存缓存池满掉后,就会释放多余的bitmap,而被释放的bitmap,会被主动recycle,但业务层因为一些原因,不小心持有glide加载的bitmap,而这个bitmap又被glide回收了,就会报上面的trying to use a recycled bitmap
崩溃了
这种崩溃的,大概率是采用下面的写法导致的
Glide.with(context).asBitmap().load(imageUrl).into(object :CustomTarget<Bitmap>(){
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
//这里拿到了bitmap,去做自己业务的事情
}
override fun onLoadCleared(placeholder: Drawable?) {
//这里回调没有实现,去释放上面引用的bitmap
}
})
修复措施:在onLoadCleared
回调里释放在onResourceReady
拿到的Bitmap的引用
其实glide内部的文档,也有对onLoadCleared
回调做了清晰的说明,不过很容易被人遗漏
You must ensure that any current Drawable received in onResourceReady(Object, Transition) is no longer used,说明文档,还专门黑体强调了must,需要释放对bitmap的引用
不要主动recycle bitmap,把bitmap引用置空,剩余的交给GC去回收就好
bitmap其实可以不用主动recycle,就算调用了recycle也不会立即马上被recycle,官方的文档也有说明,recycle一般没必要主动调用,GC系统会自行处理
这个问题,也是线上经常发生的,在一些极端情况下,页面被回收了,调用glide去加载图片,产生了崩溃
IllegalArgumentException: You cannot start a load for a destroyed activity
at com.b.a.e.m.b(RequestManagerRetriever.java:311)
at com.b.a.e.m.a(RequestManagerRetriever.java:130)
at com.b.a.e.m.a(RequestManagerRetriever.java:114)
这个崩溃的产生的代码如下
# com.bumptech.glide.manager.RequestManagerRetriever#assertNotDestroyed
private static void assertNotDestroyed(@NonNull Activity activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1 && activity.isDestroyed()) {
throw new IllegalArgumentException("You cannot start a load for a destroyed activity");
}
}
加载图片的时候,glide内部判断页面已经销毁,就直接触发崩溃。
其实线上环境复杂,还有不保留等场景,在一些极端情况下,还是很容易出现页面销毁后,还去加载图片的情况
修复措施:加载图片前,主动做下判断
/**
* 新增判断当前的页面是否已经销毁了
*/
fun Context.isValid(): Boolean {
val context = this
if (context is FragmentActivity) {
if (context.isFinishing || context.isDestroyed) {
return false
}
}
return true
}
最好是所有的glide加载,走统一封装的方法,这样的话,也好统一做容错
页面销毁后,加载图片不响应并且增加warning的log,没必要触发崩溃
我们都知道glide加载的bitmap会自动适应目标imageview的大小,不过如果是小图加载到一个大的imageview上面呢?
glide会默认把bitmap放大,用以填充Imageview,增加了内存占用,特别是长图,会导致内存极大增加
比如一张800*2400的bitmap,原始大小为7M,加载在一个宽度为1440的手机尺寸上,高度自适应,那实际imageview的尺寸为:1440 *4320,glide会把biamap缩放到跟imageview一样大,加载后的bitmap大小为23M
本来是一张7M的图片,实际在手机内存中,占用了23M的内存,根本原因是glide内部的DownsampleStrategy
决定的,相关的代码如下
# com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.CenterOutside#getScaleFactor
public float getScaleFactor(
int sourceWidth, int sourceHeight, int requestedWidth, int requestedHeight) {
float widthPercentage = requestedWidth / (float) sourceWidth;
float heightPercentage = requestedHeight / (float) sourceHeight;
return Math.max(widthPercentage, heightPercentage);
}
这里会计算bitmap的缩放值,sourceWidth、sourceHeight是bitmap的原始宽高,requestedWidth、requestedHeight是imageview的宽高,当imageview比bitmap大的时候,bitmap会被放大
修复措施:加载小图到大的imageview,增加加载配置,避免图片被放大
# 配置方式1,增加override(Target.SIZE_ORIGINAL)
Glide.with(this).override(Target.SIZE_ORIGINAL).load(imageUrl).into(target)
# 配置方式2,采用其他DownsampleStrategy
Glide.with(this).downsample(DownsampleStrategy.CENTER_INSIDE).load(imageUrl).into(target)
这样的话,就可以避免bitmap被放大了;不过这个场景,适合明确知道图片bitmap的尺寸比imageview的尺寸小才适合
imageview如果比bitmap大,默认不要放大bitmap,用原始bitmap尺寸展示就好
以上是个人对glide三点吐槽,希望后续版本可以优化