目录

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 及其一系列问题

先看一下我们即将牵扯进来的几个类图

/img/in-post/fresco_class.png

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()

大致如下:

/img/in-post/fresco_flow.png

然而,之前的错误代码缺将 Bitmap 对象缓存,之后一旦被使用,肯定是存在 recycled 风险的

总结

CloseableReference 和 SharedReference 结合可以做一些自动释放逻辑

CloseableReference.close() 可能会触发资源的释放