起因
我司软件之前一直使用自定义的图片加载器,为了顺应时代的潮流,决定引入Glide图片加载框架,然而自从上线之日起就一直收到这个异常,虽然量不大,但是不能忍啊。
Canvas: trying to use a recycled bitmap android.graphics.Bitmap
分析
- Glide 返回一个被回收的bitmap
- Glide 在某种情境下自动回收了bitmap
Glide作为一个非常成熟的框架,应该不会存在返回一个被回收的bitmap的bug,那么最有可能的应该是Glide自动回收了bitmap(讨论Glide到底应不应该自动回收bitmap
)
Glide 加载bitmap 模拟代码。
val target = Glide.with(this)
.asBitmap()
.load("https://pics6.baidu.com/feed/8718367adab44aedf2d6d8842904c104a08bfbd0.jpeg?token=65aad28f9a5456d50e9e0b19bb19f2c1&s=3B994587400DC9431003B4EE0300F019")
.into(object : SimpleTarget<Bitmap>() {
override fun onLoadFailed(errorDrawable: Drawable?) {
LogUtils.logd("失败")
}
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
bitmap = resource
iv_test.setImageBitmap(resource)
}
})
Glide在内存紧张的时候会调用 clearMemory()
方法
public void clearMemory() {
// Engine asserts this anyway when removing resources, fail faster and consistently
Util.assertMainThread();
// memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687.
memoryCache.clearMemory();
bitmapPool.clearMemory();
arrayPool.clearMemory();
}
而bitmapPool
的clearMemory()
方法中(实际使用的是LruBitmapPool
)遍历了缓存池中的bitmap,进行了removed.recycle();
操作,页面进行重绘的时候引起上述bug
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();
}
}
bitmap 又是在什么时机进入bitmaPool
中的呢?
首先加载图片的请求会被包装成SingleRequest
,当生命周期结束会调用
public synchronized void clear() {
assertNotCallingCallbacks();
stateVerifier.throwIfRecycled();
if (status == Status.CLEARED) {
return;
}
cancel();
// Resource must be released before canNotifyStatusChanged is called.
if (resource != null) {
releaseResource(resource);
}
if (canNotifyCleared()) {
target.onLoadCleared(getPlaceholderDrawable());
}
status = Status.CLEARED;
}
进行释放资源,将图片缓存到memoryCache
中,memoryCache
的具体实现类为LruResourceCache
。缓存代码在Engine
类的onResourceReleased
方法中。
@Override
public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
activeResources.deactivate(cacheKey);
if (resource.isCacheable()) {
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
当系统内存紧张或主动调用Glide.get(this).clearMemory()
时,首先会回调Engine
类的onResourceRemoved
方法,最终走到ResourceRecycler
类的
synchronized void recycle(Resource<?> resource) {
if (isRecycling) {
// If a resource has sub-resources, releasing a sub resource can cause it's parent to be
// synchronously evicted which leads to a recycle loop when the parent releases it's children.
// Posting breaks this loop.
handler.obtainMessage(ResourceRecyclerCallback.RECYCLE_RESOURCE, resource).sendToTarget();
} else {
isRecycling = true;
resource.recycle();
isRecycling = false;
}
}
resource
实际是BitmapResource
的实现类,recycle()
方法中将bitmap放入bitmapPool
中。
@Override
public void recycle() {
bitmapPool.put(bitmap);
}
顺序执行Glide.get(this).clearMemory()
,进行bitmapPool
清空操作,回收bitmap,引发崩溃。
模拟复现代码
val target = Glide.with(this)
.asBitmap()
.load("https://pics6.baidu.com/feed/8718367adab44aedf2d6d8842904c104a08bfbd0.jpeg?token=65aad28f9a5456d50e9e0b19bb19f2c1&s=3B994587400DC9431003B4EE0300F019")
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
bitmap = resource
iv_test.setImageBitmap(resource)
}
})
iv_test.postDelayed({
Glide.with(this).clear(target)
iv_test.postDelayed({
Glide.get(this).clearMemory()
iv_test.postDelayed({
LogUtils.logd(bitmap?.isRecycled)
iv_test.invalidate()
}, 3000)
}, 3000)
}, 3000)
解决
- 升级 Glide到
4.9.0
版本,使用CustomTarget
,onLoadCleared
方法会在资源回收时进行回调,这个时候的bitmap实际上已经不是安全的了。回调参见SingleRequest
类的clear()
方法的target.onLoadCleared(getPlaceholderDrawable());
这一行代码
Glide.with(this)
.asBitmap()
.load("")
.into(object : CustomTarget<Bitmap>(){
override fun onLoadCleared(placeholder: Drawable?) {
}
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
}
})
- 加载bitmap成功之后,不使用加载出来的bitmap,使用copy出来的新的bitmap返回
(copy方法有可能返回null,config也有可能为空)
var copy = resource.copy(resource.config, true)
if (copy == null){
copy = resource
}