问题描述
背景
最近看了我司某产品的 Bugly 上的崩溃分析统计,注意到 Canvas: trying to use a recycled bitmap android.graphics.Bitmap@e9a8c74
异常有点醒目。
问题调用栈
1 | java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap |
查看对应版本的 mapping 文件确认 app.abk
对应 com.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable
问题分析
提取主要信息
正常来说根据异常信息的堆栈很容易定位到相关业务代码的出错位置,而该堆栈信息乍一看有点懵
首先对于 Canvas: trying to use a recycled bitmap android.graphics.Bitmap@xxxxxxxx;
1 | protected void throwIfCannotDraw(Bitmap bitmap) { |
原因是 ImageView#onDraw
的时候尝试使用已回收的 bitmap
进而造成该崩溃。
知道上述原因对问题的分析无实质性帮助,根据 com.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable
信息首先可以确认是用到 Glide 加载图片相关的业务代码,堆栈信息中有 AbsListView
可以确认和其相关子类的控件有关,如 ListView
和 GridView
。结合这两点能很快确认和某一块的业务代码相关。
检索类似异常
既然和 Glide 相关,所以直接去 github 上 glide 的官方项目检索相关 issue,发现该问题的反馈还挺多,查看了几个类似问题后最终指向了官方关于常见错误一个说明文档 common-errors
其中关于 Cannot draw a recycled Bitmap
官方的说明如下
Glide 的
BitmapPool
是固定大小的。当Bitmap
从中被踢出而没有被重用时,Glide 将会调用recycle()
。如果应用在向 Glide 指出可以安全地回收之后 “不经意间” 继续持有Bitmap
,则应用可能尝试绘制这个Bitmap
,进而在onDraw
方法中造成崩溃。
一种可能的情况是,一个目标被用于两个ImageView
,而其中一个在Bitmap
被放到BitmapPool
中后仍然试图访问被回收后的Bitmap
。基于以下因素,要复现这种复用错误可能很困难:1)Bitmap
何时被放入池中,2)Bitmap
何时被回收,3)何种尺寸的BitmapPool
和内存缓存会导致Bitmap
的回收。可以在你的GlideModule
中加入下面的代码片段,以使这个问题更容易复现:
1 |
|
上面的代码确保没有内存缓存,且
BitmapPool
的尺寸为 0;因此Bitmap
如果恰好没有被使用,它将立刻被回收。这是为了调试目的让它更快出现。
尝试复现
结合上述说明我们可以尝试在项目代码里面对怀疑的地方进行相关日志添加再模拟场景尝试复现。实际的业务代码场景入手其实可以先定位大致位置,尝试触发大量图片加载触发 Glide BitmapPool
进行资源回收后来进行场景复现。这里我就简单通过 Demo 来复现该崩溃。
首先按照官网说明配置 MemoryCache
和 BitmapPool
大小为 0
1 |
|
需要注意的一点是:
如果你在 Kotlin 编写的类里使用 Glide 注解,你需要引入一个 kapt 依赖,以代替常规的
annotationProcessor
依赖。见下载和设置
Demo 尝试复现首次实现如下:
1 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { |
发现一直无法复现相应崩溃场景,虽然在此之前已修复对应我司产品项目中的崩溃问题。看到这篇 Blog 后才发现当时虽然解决了问题,但是深挖的还不够。
如该博主所说,当使用 into(imageView)
的方式加载图片,不会抛出异常。into(imageView)
的方式后面会setResourceInternal(null)
,最终会调用到 ImageView.setImageDrawable(null)
。这样在 ImageView onDraw
时判断 mDrawable == null
时直接返回了。
1 |
|
当使用 into(Target)
的方式则可能会导致抛出该异常,看了下我们的项目代码确实最终封装用到的是 SimpleTarget
。
1 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { |
使用 into(target)
的方式复现了该崩溃
1 | io.github.shumxin.glideresreuseerror E/AndroidRuntime: FATAL EXCEPTION: main |
原因分析
这里 ImageView
在 onDraw
阶段用到的 mDrawable
实际是 BitmapDrawable
,其持有了对应的 Bitmap
对象,该 Bitmap
对象在 Glide 的 LruBitmapPool#put
方法中当不满足缓存条件时则会调用 bitmap.recycle()
进行回收。RecyclerView
滑动时当出现 ViewHolder
复用时,新的图片资源还未获取到时,该 ViewHolder
中的 ImageView
用之前请求的图片资源进行绘制时,对应该图片资源的 mDrawable
中的 Bitmap
已经被回收,遂会抛出该异常。
解决方案
结合原因分析,最初我在工程项目里面的实现方案则是通过对崩溃的地方的自定义 ImageView,并对其 onDraw()
方法进行重写。判断如果当前持有的 mDrawable
是 BitmapDrawable
,当其持有的 bitmap.isRecycled
,则不触发最终的绘制操作。
1 | override fun onDraw(canvas: Canvas?) { |
上述方式可以解决该问题,但是其中的一个缺陷是需要知道哪些类型的 drawable
会持有 bitmap
,比如 3.x 版本的 Glide 有 GlideBitmapDrawable
,其内部也持有 bitmap
,对于上述的处理就需要再增加一个分支处理。
那有没有更合适的方式来处理该问题呢,对于实现 CustomViewTarget 中 onLoadFailed(@Nullable Drawable errorDrawable)、onResourceReady(@NonNull R resource, @Nullable Transition<? super R> transition)、onResourceCleared(@Nullable Drawable placeholder)
三个方法时,其中 onResourceCleared
很容易被忽略。
关于 onResourceCleared(@Nullable Drawable placeholder)
的说明如下
A required callback invoked when the resource is no longer valid and must be freed. You must ensure that any current Drawable received in onResourceReady(Object, Transition) is no longer used before redrawing the container (usually a View) or changing its visibility. Not doing so will result in crashes in your app.
onResourceCleared(@Nullable Drawable placeholder)
是由 onLoadCleared(@Nullable Drawable placeholder)
调用
关于 onLoadCleared(@Nullable Drawable placeholder)
的说明如下
A mandatory lifecycle callback that is called when a load is cancelled and its resources are freed.
You must ensure that any current Drawable received in onResourceReady(Object, Transition) is no longer used before redrawing the container (usually a View) or changing its visibility.
再看下官方文档相关的说明 链接
往Target中加载资源,清除或重用Target,并继续引用该资源
最简单的比较这个错误的办法是确保所有对资源的引用都在
onLoadCleared()
调用时置空。通常,加载一个Bitmap
然后对Target
解引用,并且不要再次调用into()
或clear()
,这样是安全的。然而,加载了一个Bitmap
,清除这个Target
,并在之后继续持有Bitmap
引用是不安全的。类似地,加载资源到一个View
上然后从View
中获取这个资源 (通过getImageDrawable()
或任何其他手段),并在其他某个地方继续引用它,也是不安全的。
当 Glide 回调 onResourceCleared
后即准备将相关的 bitmap
进行回收,所以我们只需要在 onResourceCleared
的时候主动 setImageDrawable(null)
不再持有接下来将被回收的 Bitmap
即可解决问题
1 | override fun onResourceCleared(placeholder: Drawable?) { |