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

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<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
}

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容