美文网首页Bug捕手
[一千个Bug] | 尝试使用已经回收的Bitmap异常

[一千个Bug] | 尝试使用已经回收的Bitmap异常

作者: 猫克杯 | 来源:发表于2017-05-11 11:43 被阅读310次
    • 本文约1500字,建议阅读时间12分钟
    Missing Bitmap

    Android开发者官方文档的Managing Bitmap Memory一节中有这样一处描述:

    Caution: You should use recycle() only when you are sure that the bitmap is no longer being used. If you call recycle() and later attempt to draw the bitmap, you will get the error: "Canvas: trying to use a recycled bitmap".

    这段话的含义是:只有当你确定bitmap已经不再使用的时候才可以对它使用recycle。如果你已经执行了recycle,之后又试图绘制那个bitmap,将会得到一个"Canvas: trying to use a recycled bitmap"的错误。这是一个经典的异常,遭遇过它的同学想必都有怎么避免这种异常的经验。其中最常见的手段包括在使用bitmap之前对它做<code>isRecycled()</code>判定,如果发现它已经被回收则不使用。

    那么这个手段是够用的吗?答案是否定的。文猫君举一个<code>ImageView</code>使用bitmap的例子。场景是这样的:首先执行一个<code>ImageView#setImageBitmap(bitmapA)</code>的调用,然后立刻执行一个包含recycle bitmapA的调用。通过继承<code>ImageView</code>重写<code>onDraw()</code>方法并打印日志。

    05-10 16:59:59.241 9419-9419/com.demo.thdbugs D/CustomImageView: ## setImageBitmap -> bitmapA
    05-10 16:59:59.243 9419-9419/com.demo.thdbugs D/CustomImageView: ## recyle -> bitmapA
    05-10 16:59:59.268 9419-9419/com.demo.thdbugs D/CustomImageView: ## onDraw
    05-10 16:59:59.380 9419-9419/com.demo.thdbugs E/AndroidRuntime: FATAL EXCEPTION: main
        Process: com.demo.thdbugs, PID: 9419
        java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@5d740c5
            at android.graphics.Canvas.throwIfCannotDraw(Canvas.java:1271)
            at android.view.DisplayListCanvas.throwIfCannotDraw(DisplayListCanvas.java:257)
            at android.graphics.Canvas.drawBitmap(Canvas.java:1415)
            at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:528)
            at android.widget.ImageView.onDraw(ImageView.java:1316)
            at com.demo.test.CustomImageView.onDraw(CustomImageView.java:33)
            at android.view.View.draw(View.java:17185)
            at android.view.View.updateDisplayListIfDirty(View.java:16167)
            at android.view.View.draw(View.java:16951)
            ...
    

    我们发现从<code>ImageView#setImageBitmap</code>到bitmap真正被用于绘制这中间存在27毫秒的时间差。10毫秒级的延迟区间足够bitmap被回收掉,尤其是当你可能在某些异步线程里操作相关的bitmap。这虽然是一个极端的bitmap用例,但却不失代表性,供你参考。

    如何解决?

    解决问题的先决条件当然是先找到问题出在哪里。出现此类异常时,我们实际能得到的堆栈信息往往可能是如下这样的:只有系统源码级的调用层级,没有具体的指向?那么如何找到具体的那个出问题的bitmap呢?

    // 遇到这种只有系统层级而没有自己的app代码调用层级的堆栈信息要怎么办呢?
    
    java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@b5a72c5
        at android.graphics.Canvas.throwIfCannotDraw(Canvas.java:1271)
        at android.view.DisplayListCanvas.throwIfCannotDraw(DisplayListCanvas.java:257)
        at android.graphics.Canvas.drawBitmap(Canvas.java:1415)
        at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:528)
        at android.widget.ImageView.onDraw(ImageView.java:1316)
        at android.view.View.draw(View.java:17185)
        at android.view.View.updateDisplayListIfDirty(View.java:16167)
        at android.view.View.draw(View.java:16951)
        at android.view.ViewGroup.drawChild(ViewGroup.java:3727)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3513)
        at android.view.View.updateDisplayListIfDirty(View.java:16162)
        at android.view.View.draw(View.java:16951)
        ... // 中间可能不止一次View.draw,因为View可以嵌套,这里节约篇幅省略掉
        at com.android.internal.policy.DecorView.draw(DecorView.java:753)
        ... // 节约篇幅,省略掉
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6337)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:874)
        at android.view.Choreographer.doCallbacks(Choreographer.java:686)
        at android.view.Choreographer.doFrame(Choreographer.java:621)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:860)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6119)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
    

    解决问题的前提:找到问题发生的精确位置

    • 情形1:问题复现很容易,你自己就可以找出在哪个界面出现的这个异常。非常好,这种情况我们可以直接进入下一个环节了。
    • 情形2:与情形1截然相反,这种情况假设作为开发人员的你复现问题的条件十分有限。只能被动的等待测试团队或者来自最终用户的反馈。当问题出现时,如果你能拿到手的仍然只有上面那个没有明确指向的堆栈信息,那么你可能仍旧无法找到问题的根源。

    针对情形2,文猫君提供一个主动搜集信息的方法供你参考。具体怎么做呢?——答案是利用<code>Thread.UncaughtExceptionHandler</code>。关于Thread.UncaughtExceptionHandler如何使用,不了解的朋友可以参考简书上这位作者的文章《android 异常捕获-UncaughtExceptionHandler》。具体代码如下:

    public class AppCrashHandler implements Thread.UncaughtExceptionHandler {
        // 与本文无关的部分,节省篇幅略过
        ...
    
        // 系统默认的UncaughtException处理类
        private Thread.UncaughtExceptionHandler mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
    
        /** 关键方法:处理未捕获的异常 */
        @Override
        public void uncaughtException(Thread thread, Throwable throwable) {
            // 找出我们要关注的这个异常
            if (throwable instanceof RuntimeException
                && throwable.getMessage().contains("Canvas: trying to use a recycled bitmap")) {
                // 找出异常发生时处于前台的activity的类名
                ActivityManager am =
                    (ActivityManager) mContext.getSystemService(Context.ACTIVITY_SERVICE);
                List<ActivityManager.RunningTaskInfo> taskInfo = am.getRunningTasks(1);
                String activityClassName = taskInfo.get(0).topActivity.getClassName();
    
                // 拿到随后会被系统输出的堆栈信息
                StackTraceElement[] originalElements = throwable.getStackTrace();
                // 建立一个长度+1的堆栈信息复制,预留出栈顶
                StackTraceElement[] modifiedElements = new StackTraceElement[originalElements.length + 1];
                System.arraycopy(originalElements, 0, modifiedElements, 1, originalElements.length);
                // 栈顶的位置填上处于前台的activity的类名
                modifiedElements[0] = new StackTraceElement(activityClassName, "", null, 0);
             
                // 用修改过的堆栈信息替换掉异常里原来的信息
                throwable.setStackTrace(modifiedElements);
            }
    
            if (mDefaultHandler != null) {
                // 如果用户没有处理则让系统默认的异常处理器来处理
                mDefaultHandler.uncaughtException(thread, ex);
            }
        }
    }
    

    应用了上面的未捕获异常处理逻辑,当问题再次出现时,你得到的堆栈信息将变成如下这个样子: 其中的<code>com.demo.thdbugs.SomeActivityUseBitmap</code>会是你自己的类名。这样一来你至少就可以确认问题是出在哪个界面了。这个技巧同样适用于其他获取不到明确代码指向的堆栈信息。

    java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@b5a72c5
        at com.demo.thdbugs.SomeActivityUseBitmap
        at android.graphics.Canvas.throwIfCannotDraw(Canvas.java:1271)
        at android.view.DisplayListCanvas.throwIfCannotDraw(DisplayListCanvas.java:257)
        at android.graphics.Canvas.drawBitmap(Canvas.java:1415)
        at android.graphics.drawable.BitmapDrawable.draw(BitmapDrawable.java:528)
        at android.widget.ImageView.onDraw(ImageView.java:1316)
        at android.view.View.draw(View.java:17185)
        at android.view.View.updateDisplayListIfDirty(View.java:16167)
        at android.view.View.draw(View.java:16951)
        at android.view.ViewGroup.drawChild(ViewGroup.java:3727)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3513)
        at android.view.View.updateDisplayListIfDirty(View.java:16162)
        at android.view.View.draw(View.java:16951)
        ... // 中间可能不止一次View.draw,因为View可以嵌套,这里节约篇幅省略掉
        at com.android.internal.policy.DecorView.draw(DecorView.java:753)
        ... // 节约篇幅,省略掉
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6337)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:874)
        at android.view.Choreographer.doCallbacks(Choreographer.java:686)
        at android.view.Choreographer.doFrame(Choreographer.java:621)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:860)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6119)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
    

    定位到出现问题的具体Activity,接下的事情就好办多了。排查你的界面布局,静态分析你的代码,找到所有可能用到bitmap的地方,检查它们被recycle的路径。

    比解决问题更要紧的是,如何避免?

    回到Android官方文档Managing Bitmap Memory这一节的内容。官方给出的建议总结起来就是:如果你需要支持的Android系统版本小于等于2.3,你确实需要自行管理bitmap。并且文档还给出了一个基于引用计数来管理bitmap的范例app。如果你需要支持的Android系统版本大于等于3.0,那么垃圾回收器会在bitmap不再使用的时候帮助你回收掉它们。如此说来,是不是我们就不再需要手动调用<code>recycle</code>方法了呢?答案显示是否定的,如果大家都这么做,显然就不会遭遇本文提到的异常了。因为在某些场景中,我们需要手动执行bitmap的<code>recycle</code>。当然,我们仍然可以从这段描述中得到一个结论:在非必要的情况下,不要自己去管理bitmap。相应的,因为错误时机释放bitmap而导致异常的概率自然也就降低了。

    那么当我们确有必要自行管理bitmap的时候,如何把握释放时机以避免出现像“trying to use a recycled bitmap”这样的异常呢? 要说明这个问题太依赖具体场景来分析了。一个推荐的做法是:使用成熟的图片加载库,例如Glide,UIL等来做bitmap的获取,解码和显示。如果你一定要撸起袖子自己来,文猫君提供两个实践经验供你参考:第一,如果出现bitmap替换的步骤,总是等到新的bitmap实装到位了再去回收旧的bitmap;第二,如果有跨越两个activity界面的bitmap用法,可以约定统一在其中一处管理bitmap。如果无法保证交付出去的bitmap不被回收,在不影响性能的前提下我们还可以考虑做必要的拷贝以保证原始的bitmap是安全的。

    相关文章

      网友评论

      • 5eed3da9faa9:要是还有如何避免,或解决方法就更好了
        猫克杯:@寂寞黑莓 补充了解决方法和如何避免。:smile:
        猫克杯:@寂寞黑莓 很好的建议:+1:我会在后续的分享中增加这样的章节

      本文标题:[一千个Bug] | 尝试使用已经回收的Bitmap异常

      本文链接:https://www.haomeiwen.com/subject/mkugtxtx.html