Glide 的坑,Canvas: trying to use a recycled bitmap android.graphics.Bitmap

起因

我司软件之前一直使用自定义的图片加载器,为了顺应时代的潮流,决定引入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() {
                    override fun onLoadFailed(errorDrawable: Drawable?) {
                        LogUtils.logd("失败")
                    }

                    override fun onResourceReady(
                        resource: Bitmap,
                        transition: Transition?
                    ) {
                        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();
  }

bitmapPoolclearMemory()方法中(实际使用的是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() {
                    override fun onResourceReady(
                        resource: Bitmap,
                        transition: Transition?
                    ) {
                        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(){
                override fun onLoadCleared(placeholder: Drawable?) {
                    
                }

                override fun onResourceReady(resource: Bitmap, transition: Transition?) {
                    
                }
            })
  • 加载bitmap成功之后,不使用加载出来的bitmap,使用copy出来的新的bitmap返回 (copy方法有可能返回null,config也有可能为空)
var copy = resource.copy(resource.config, true)
if (copy == null){
  copy = resource
}

还有其他解决方法的欢迎分享

你可能感兴趣的:(Glide 的坑,Canvas: trying to use a recycled bitmap android.graphics.Bitmap)