一、背景
貝殼2.6.0版本使用Glide preload方法替換了部分顯示圖片的方式, 在灰度期間發現控件顯示了錯誤的圖片或者崩潰問題。
Fatal Exception: java.lang.RuntimeException:Canvas: trying to use a recycled bitmap android.graphics.Bitmap@25e89bf 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 com.bumptech.glide.load.resource.bitmap.GlideBitmapDrawable.draw(GlideBitmapDrawable.java:101)
二、原因分析
Glide使用activityResources、LruResourceCache、LruBitmapPool等3級內存和文件緩存LazyDiskCacheProvider。
activeResources是個Map, key值(EngineKey)根據10個參數組合生成,value是ResourceWeakReference類型; resource是EngineResource類並實現引用計數。
activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
public void recycle() {
if (acquired > 0) {
throw new IllegalStateException("Cannot recycle a resource while it is still acquired");
}
if (isRecycled) {
throw new IllegalStateException("Cannot recycle a resource that has already been recycled");
}
isRecycled = true;
resource.recycle(); //實際上將Bitmap添加到BitmapPool
}
/**
* Increments the number of consumers using the wrapped resource. Must be called on the main thread.
*
* <p>
* This must be called with a number corresponding to the number of new consumers each time new consumers
* begin using the wrapped resource. It is always safer to call acquire more often than necessary. Generally
* external users should never call this method, the framework will take care of this for you.
* </p>
*/
void acquire() {
if (isRecycled) {
throw new IllegalStateException("Cannot acquire a recycled resource");
}
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new IllegalThreadStateException("Must call acquire on the main thread");
}
++acquired;
}
/**
* Decrements the number of consumers using the wrapped resource. Must be called on the main thread.
*
* <p>
* This must only be called when a consumer that called the {@link #acquire()} method is now done with the
* resource. Generally external users should never callthis method, the framework will take care of this for
* you.
* </p>
*/
void release() {
if (acquired <= 0) {
throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
}
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new IllegalThreadStateException("Must call release on the main thread");
}
if (--acquired == 0) {
listener.onResourceReleased(key, this);
}
}
LruResourceCache使用最近最少使用算法跟保存從activeResources移出的resource, 如果LruCache滿了則移除記錄並添加到LruBitmapPool。
LruBitmapPool的作用是緩存廢棄的Bitmap(包括從activeResources或者LurResourcesCache移出的), 每次解碼圖片時先從bitmappool找是否有合適的bitmap實例複用,找到了則從BitmapPool裏移出,找不到則實例化個Bitmap對象。
into方式bitmap轉換關係
使用BitmapPool
三、preload和into區別
preload會創建PreloadTarget, 在回調onResourceReady時執行了Glide.clear(this), 將當前GlideDrawable從activeResources移動到LruResourcesCache。 即執行了cache.put(cacheKey, resource);
public final class PreloadTarget<Z> extends SimpleTarget<Z> {
/**
* Returns a PreloadTarget.
*
* @param width The width in pixels of the desired resource.
* @param height The height in pixels of the desired resource.
* @param <Z> The type of the desired resource.
*/
public static <Z> PreloadTarget<Z> obtain(int width, int height) {
return new PreloadTarget<Z>(width, height);
}
private PreloadTarget(int width, int height) {
super(width, height);
}
@Override
public void onResourceReady(Z resource, GlideAnimation<? super Z> glideAnimation) {
Glide.clear(this);
}
}
劃重點: 如果當前界面在onResourceReady回調函數裏使用drawable刷新到界面, 那麼可能導致crash或者渲染錯誤圖片的問題。 原因是LruCache裏緩存的圖片可能被移出到BitmapPool; 而Glide在transform圖片時會從BitmapPool裏取Bitmap對象(UI顯示錯誤的圖片), 也可能執行bitmap的recycle方法(程序崩潰)。 即在onResourceReady裏使用的GlideDrawable內存區域可能在其它地方篡改。
into會創建GenericTarget, 在回調onResourceReady時
public final class GenericRequest<A, T, Z, R> implements Request, SizeReadyCallback,
ResourceCallback {
...
public void onResourceReady(Resource<?> resource) {
if (resource == null) {
onException(new Exception("Expected to receive a Resource<R> with an object of " + transcodeClass
+ " inside, but instead got null."));
return;
}
Object received = resource.get();
if (received == null || !transcodeClass.isAssignableFrom(received.getClass())) {
releaseResource(resource);
onException(new Exception("Expected to receive an object of " + transcodeClass
+ " but instead got " + (received != null ? received.getClass() : "") + "{" + received + "}"
+ " inside Resource{" + resource + "}."
+ (received != null ? "" : " "
+ "To indicate failure return a null Resource object, "
+ "rather than a Resource object containing null data.")
));
return;
}
if (!canSetResource()) {
releaseResource(resource);
// We can't set the status to complete before asking canSetResource().
status = Status.COMPLETE;
return;
}
onResourceReady(resource, (R) received);
}
/**
* Internal {@link #onResourceReady(Resource)} where arguments are known to be safe.
*
* @param resource original {@link Resource}, never <code>null</code>
* @param result object returned by {@link Resource#get()}, checked for type and never <code>null</code>
*/
private void onResourceReady(Resource<?> resource, R result) {
// We must call isFirstReadyResource before setting status.
boolean isFirstResource = isFirstReadyResource();
status = Status.COMPLETE;
this.resource = resource;
if (requestListener == null || !requestListener.onResourceReady(result, model, target, loadedFromMemoryCache,
isFirstResource)) {
GlideAnimation<R> animation = animationFactory.build(loadedFromMemoryCache, isFirstResource);
target.onResourceReady(result, animation); //執行GlideDrawbleImageViewTarget的setResource方法,即view.setImageDrawable
}
notifyLoadSuccess();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("Resource ready in " + LogTime.getElapsedMillis(startTime) + " size: "
+ (resource.getSize() * TO_MEGABYTE) + " fromCache: " + loadedFromMemoryCache);
}
}
....
}
四、結論
Glide的preload回調函數onResourceReady返回的resource並不可靠, 在使用時要新創建個實例或者保存成文件後使用它。
LJImageLoader.with(getContext()).url(ConstantUtil.URL_CDN_WALLET_REWARD_SELECTED)
.dontAnimate()
.listener(new ILoadListener() {
@Override public boolean onException(Exception e, String model) {
return false;
}
@Override public boolean onResourceReady(Drawable resource, String model) {
if (resource != null) {
Bitmap bmp = Tools.drawableToBitmap(resource);
if (bmp != null) {
rootView.setBackground(new BitmapDrawable(getResources(), bmp));
}
}
return false;
}
}).preload();