Android Bug記:'Canvas:trying to use a recycled bitmap'

Bug日誌

最近一個項目中遇到一個詭異Bug,詳細日誌以下:html

E/MainActivity: executeTextView: test for get drawable:  last source: android.graphics.drawable.BitmapDrawable@8c352b2
    executeTextView: test for get drawable: isVisible true alpha:  255 last source: android.graphics.drawable.BitmapDrawable@8c352b2
W/Bitmap: Called hasAlpha() on a recycle()'d bitmap! This is undefined behavior!
    Called hasAlpha() on a recycle()'d bitmap! This is undefined behavior!
    Called hasAlpha() on a recycle()'d bitmap! This is undefined behavior!
W/Bitmap: Called hasAlpha() on a recycle()'d bitmap! This is undefined behavior!
E/MainActivity: Glide結束
    executeImageView: ...
E/MainActivity: showQrCode: 舉報二維碼1:
    showQrCode: 舉報二維碼2: https://xxx.com/upload/equipmentWxQRCode/15776698271ada7952f9ead4d5.jpg
D/AndroidRuntime: Shutting down VM
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.rootrl.adviewer, PID: 29128
    java.lang.RuntimeException: Canvas: trying to use a recycled bitmap android.graphics.Bitmap@ac257b9
        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:1298)
        at android.view.View.draw(View.java:17201)
        at android.view.View.updateDisplayListIfDirty(View.java:16183)
        at android.view.View.draw(View.java:16967)
        at android.view.ViewGroup.drawChild(ViewGroup.java:3727)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3513)
        at androidx.constraintlayout.widget.ConstraintLayout.dispatchDraw(ConstraintLayout.java:2023)
        at android.view.View.updateDisplayListIfDirty(View.java:16178)
        at android.view.View.draw(View.java:16967)
        at android.view.ViewGroup.drawChild(ViewGroup.java:3727)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3513)
        at androidx.constraintlayout.widget.ConstraintLayout.dispatchDraw(ConstraintLayout.java:2023)
        at android.view.View.draw(View.java:17204)
        at android.view.View.updateDisplayListIfDirty(View.java:16183)
        at android.view.View.draw(View.java:16967)
        at android.view.ViewGroup.drawChild(ViewGroup.java:3727)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3513)
        at android.view.View.updateDisplayListIfDirty(View.java:16178)
        at android.view.View.draw(View.java:16967)
        at android.view.ViewGroup.drawChild(ViewGroup.java:3727)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3513)
        at android.view.View.updateDisplayListIfDirty(View.java:16178)
        at android.view.View.draw(View.java:16967)
        at android.view.ViewGroup.drawChild(ViewGroup.java:3727)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3513)
        at android.view.View.updateDisplayListIfDirty(View.java:16178)
        at android.view.View.draw(View.java:16967)
        at android.view.ViewGroup.drawChild(ViewGroup.java:3727)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3513)
        at android.view.View.updateDisplayListIfDirty(View.java:16178)
        at android.view.View.draw(View.java:16967)
        at android.view.ViewGroup.drawChild(ViewGroup.java:3727)
        at android.view.ViewGroup.dispatchDraw(ViewGroup.java:3513)
        at android.view.View.draw(View.java:17204)
        at com.android.internal.policy.DecorView.draw(DecorView.java:754)
        at android.view.View.updateDisplayListIfDirty(View.java:16183)
        at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java:648)
        at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java:654)
        at android.view.ThreadedRenderer.draw(ThreadedRenderer.java:762)
        at android.view.ViewRootImpl.draw(ViewRootImpl.java:2800)
        at android.view.ViewRootImpl.performDraw(ViewRootImpl.java:2608)
        at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2215)
        at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1254)
        at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6338)
        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:755)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6121)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:905)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:795)
I/Process: Sending signal. PID: 29128 SIG: 9
Process 29128 terminated.

Bug初步分析

其實字面上看上去很簡單。可是詭異在發生場景:java

  • 只在安卓橫屏模式下發生,豎屏模式下正常。
  • 有一張特定圖片纔會出現問題,其餘圖片均不會。而這個圖片不管分辨率大小仍是文件大小均不大,其餘比它大十幾倍的都正常運行。

報錯緣由從日誌上看到很簡單:使用了一個已經被回收的bitmap資源(我這裏使用的是Glide圖片處理庫)。可是結合個人使用場景和發生場景(只在橫屏下),再加上Glide對於我來講是一個黑箱。 種種緣由結合看來是一個難調的bug。android

後來發現發生的地方是imageView的Placeholder設置階段。代碼以下:git

if (currentView == AdConstant.VIEW_TYPE_TEXT_VIEW) {
    if (adImageView.getDrawable() != null) {
        requestOptions.placeholder(adImageView.getDrawable());
    }
}

設置這個Placeholder是爲了解決圖片切換時的閃黑屏問題,一是去掉Glide的Animate,二是設置這個Placeholder,把當前Image View的Drawable做爲默認圖片。而因爲個人業務邏輯複雜,有圖片和視頻的輪播,有可能在設置時找不到這個Drawable的Bitmap資源,好吧,說有多是由於我也不能給個具體的緣由-_-'',由於結合我上面提到的兩個特定發生場景,實在是太詭異了。github

Bug深刻分析

後來我看到github上官方bumptech/glide也有一大堆issues,有人說是glide版本問題,可是我更新到最新的4.10.0依舊無解。緩存

最後看到官方的Common errors文檔,http://bumptech.github.io/gli...安全

Glide’s BitmapPool has a fixed size. When Bitmaps are evicted from the pool without being re-used, Glide will call recycle(). If an application inadvertently continues to hold on to the Bitmap even after indicating to Glide that it is safe to recycle it, the application may then attempt to draw the Bitmap, resulting in a crash in onDraw().

This problem could be due to the fact that one target is being used for two ImageViews, and one of the ImageViews still tries to access the recycled Bitmap after it has been put into the BitmapPool. This recycling error can be hard to reproduce, due to several factors: 1) when the bitmap is put into the pool, 2) when the bitmap is recycled, and 3) what the size of the BitmapPool and memory cache are that leads to the recycling of the Bitmap. The following snippet can be put into your GlideModule to help making this problem easier to reproduce:

@Override
public void applyOptions(Context context, GlideBuilder builder) {
    int bitmapPoolSizeBytes = 1024 * 1024 * 0; // 0mb
    int memoryCacheSizeBytes = 1024 * 1024 * 0; // 0mb
    builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes));
    builder.setBitmapPool(new LruBitmapPool(bitmapPoolSizeBytes));
}
The above code makes sure that there is no memory caching and the size of the BitmapPool is zero; so Bitmap, if happened to be not used, will be recycled right away. The problem will surface much quicker for debugging purposes.

第一段說明了真正緣由,Bitmap在BitmapPool中被剔除而沒有被重用時,Glide會調用recycle(),可是若是Application在被告知安全回收了Bitmap以後仍是保留這個Bitmap,繼而繪製Bitmap時,在onDraw中就會崩潰。bash

我這個Placeholder就發生在這種狀況下。app

Bug解決

我這邊解決思路是從新設置BitmapPool的大小,這須要重寫AppGlideModule,代碼以下:ide

package com.rootrl.adviewer.glide;

import android.content.Context;

import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.annotation.GlideModule;
import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool;
import com.bumptech.glide.load.engine.cache.LruResourceCache;
import com.bumptech.glide.module.AppGlideModule;

@GlideModule
public class AdImageGlideModule extends AppGlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {
        int bitmapPoolSizeBytes = 1024 * 1024 * 200; // 200mb
        int memoryCacheSizeBytes = 1024 * 1024 * 200; // 200mb
        builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes));
        builder.setBitmapPool(new LruBitmapPool(bitmapPoolSizeBytes));
    }
}

這裏有幾點要注意,否則項目中沒有GlideApp對象。

  • 類中添加@GlideModule註解
  • 如同package com.rootrl.adviewer.glide,這個Module放在項目路徑的glide package目錄(需新建)
  • 改下build.grdle配置

其中第三條具體以下,注意除了glide依賴,還需annotationProcessor項:

implementation 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'

而後,點AS的Build => Make Project,以後就能夠在項目中使用集成本身GlideModule的GlideAPP了。

使用方式也是用GlideAPP替換原來的Glide就能夠。

// 替換前
Glide.with(MainActivity.this).listener(...).load(uri).apply(requestOptions).into(adImageView);

// 替換後
GlideApp.with(MainActivity.this).listener(...).load(uri).apply(requestOptions).into(adImageView);

總結

其實這裏尚未具體深刻,由於安卓對我來講仍是一個實用爲主階段。最後強調是圖片處理庫很是推薦Glide,它的緩存機制很實用。而後視頻的緩存推薦danikula:videocache庫。

相關文章
相關標籤/搜索