最近一个项目中遇到一个诡异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.
其实字面上看上去很简单。可是诡异在发生场景: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
后来我看到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
我这边解决思路是从新设置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对象。
其中第三条具体以下,注意除了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库。