Fresco Bitmap 潜在问题
最近发现线上有一些 used recycled bitmap crash
似乎是问题一直存在,但是近期版本增多了,不过不是我负责,也不好多说什么
其实我觉得主要是造轮子的对于轮子本身理解不够深刻,导致的问题
Recycled Bitmap
/**
* Returns true if this bitmap has been recycled. If so, then it is an error
* to try to access its pixels, and the bitmap will not draw.
*
* @return true if the bitmap has been recycled
*/
public final boolean isRecycled() {
return mRecycled;
}
所以当一个 bitmap recycled 再次使用,则会发生问题
比如 Canvas drawBitmap 的时候会进行 bitmap 检查
protected void throwIfCannotDraw(Bitmap bitmap) {
if (bitmap.isRecycled()) {
// crash
throw new RuntimeException("Canvas: trying to use a recycled bitmap " + bitmap);
}
if (!bitmap.isPremultiplied() && bitmap.getConfig() == Bitmap.Config.ARGB_8888 &&
bitmap.hasAlpha()) {
throw new RuntimeException("Canvas: trying to use a non-premultiplied bitmap "
+ bitmap);
}
throwIfHwBitmapInSwMode(bitmap);
}
Fresco 获取 Bitmap
公司用的 Fresco 版本 2.1.0
Fresco 获取 Bitmap 大致都会搜到如下代码:
val dataSource = Fresco.getImagePipeline().fetchDecodedImage(request, null)
dataSource.subscribe(object : BaseBitmapDataSubscriber() {
override fun onFailureImpl(source: DataSource<CloseableReference<CloseableImage>>) {
}
override fun onNewResultImpl(@Nullable bitmap: Bitmap?) {
// 此处可以拿到 Bitmap
// 丢给其他 cache 存储,却是不安全的
LocalImageCache.trySaveBitmap(requiredParams, bitmap)
}
}, executor)
}
这段代码可以看到在 onNewResultImpl 中 Fresco 的回调会返给我们 Bitmap 对象,进而有些人会把此处的 Bitmap 存到一个缓存中去,以备后续使用,然而是大错特错
我们来看一下,BaseBitmapDataSubscriber 的代码
public abstract class BaseBitmapDataSubscriber extends
BaseDataSubscriber<CloseableReference<CloseableImage>> {
@Override
public void onNewResultImpl(DataSource<CloseableReference<CloseableImage>> dataSource) {
if (!dataSource.isFinished()) {
return;
}
CloseableReference<CloseableImage> closeableImageRef = dataSource.getResult();
Bitmap bitmap = null;
if (closeableImageRef != null &&
closeableImageRef.get() instanceof CloseableBitmap) {
bitmap = ((CloseableBitmap) closeableImageRef.get()).getUnderlyingBitmap();
}
try {
// 此处回调
onNewResultImpl(bitmap);
} finally {
// 此处释放 CloseableReference
CloseableReference.closeSafely(closeableImageRef);
}
}
/**
* The bitmap provided to this method is only guaranteed to be around for the lifespan of the
* method.
*
* <p>The framework will free the bitmap's memory after this method has completed.
* @param bitmap
*/
protected abstract void onNewResultImpl(@Nullable Bitmap bitmap);
}
我们可以看到,在回调给我们 Bitmap 以后,紧接着会调用到 CloseableReference.close 方法,可能会触发释放逻辑
注意
后来仔细研究发现,dataSource.getResult(); 时候其实是 refCount + 1 了,所以此处如果从缓存中获取, close 后理论上不会出现释放的情况。
但是,如果缓存不足,后续加载的 Bitmap 还是可能存在被释放的风险。
总之,从这里获取 Bitmap 对象时,如果缓存中也有一份,则应该没问题。反之则不一定安全,特别是缓存满,或者内存紧张触发释放缓存逻辑的时候,引用计数一旦为0,Bitmap 状态无法被保证,如果缓存此处的 Bitmap 就有几率触发 crash。
CloseableReference 及其一系列问题
先看一下我们即将牵扯进来的几个类图
CloseableReference 内部持有一个 SharedReference
SharedReference 类似 shared_ptr,内部是一个引用计数,如果计数为 0,则会触发其内部对应的 ResourceReleaser 做对应的 release 操作
而我们前面看到的 BaseBitmapDataSubscriber 其实一开始拿到的事 CloseableReference 引用,只不过他帮我们解引用拿到了 bitmap,回调结束以后直接 close
close 会触发一系列的操作,内部 SharedReference 计数 -1,如果为 0,触发 release,进而走到 CloseableReference 引用的对象的相应方法
而一般我们加载图片时,Fresco 往往返回的是一个 DefaultCloseableReference<CloseableStaticBitmap>
CloseableStaticBitmap 的 close 方法进一步会释放其内部持有的 CloseableReference<Bitmap>,进而进入其 SharedReference 的 ResourceReleaser(BucketsBitmapPool),进而触发其 release 方法,最终如果无法复用,则会调用 free,进而调用 Bitmap.recycle()
大致如下:
然而,之前的错误代码缺将 Bitmap 对象缓存,之后一旦被使用,肯定是存在 recycled 风险的
总结
CloseableReference 和 SharedReference 结合可以做一些自动释放逻辑
CloseableReference.close() 可能会触发资源的释放