/******************************************************************************* * Copyright (c) 2013 Chris Banes. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************/ package uk.co.senab.bitmapcache; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; import android.os.Handler; import android.os.Looper; import android.util.Log; public class CacheableBitmapDrawable extends BitmapDrawable { static final String LOG_TAG = "CacheableBitmapDrawable"; // URL Associated with this Bitmap private final String mUrl; private BitmapLruCache.RecyclePolicy mRecyclePolicy; // Number of Views currently displaying bitmap private int mDisplayingCount; // Has it been displayed yet private boolean mHasBeenDisplayed; // Number of caches currently referencing the wrapper private int mCacheCount; // The CheckStateRunnable currently being delayed private Runnable mCheckStateRunnable; // Throwable which records the stack trace when we recycle private Throwable mStackTraceWhenRecycled; // Handler which may be used later private static final Handler sHandler = new Handler(Looper.getMainLooper()); CacheableBitmapDrawable(String url, Resources resources, Bitmap bitmap, BitmapLruCache.RecyclePolicy recyclePolicy) { super(resources, bitmap); mUrl = url; mRecyclePolicy = recyclePolicy; mDisplayingCount = 0; mCacheCount = 0; } @Override public void draw(Canvas canvas) { try { super.draw(canvas); } catch (RuntimeException re) { // A RuntimeException has been thrown, probably due to a recycled Bitmap. If we have // one, print the method stack when the recycle() call happened if (null != mStackTraceWhenRecycled) { mStackTraceWhenRecycled.printStackTrace(); } // Finally throw the original exception throw re; } } /** * @return Amount of heap size currently being used by {@code Bitmap} */ int getMemorySize() { int size = 0; final Bitmap bitmap = getBitmap(); if (null != bitmap && !bitmap.isRecycled()) { size = bitmap.getRowBytes() * bitmap.getHeight(); } return size; } /** * @return the URL associated with the BitmapDrawable */ public String getUrl() { return mUrl; } /** * Returns true when this wrapper has a bitmap and the bitmap has not been recycled. * * @return true - if the bitmap has not been recycled. */ public synchronized boolean hasValidBitmap() { Bitmap bitmap = getBitmap(); return null != bitmap && !bitmap.isRecycled(); } /** * @return true - if the bitmap is currently being displayed by a {@link CacheableImageView}. */ public synchronized boolean isBeingDisplayed() { return mDisplayingCount > 0; } /** * @return true - if the wrapper is currently referenced by a cache. */ public synchronized boolean isReferencedByCache() { return mCacheCount > 0; } /** * Used to signal to the Drawable whether it is being used or not. * * @param beingUsed - true if being used, false if not. */ public synchronized void setBeingUsed(boolean beingUsed) { if (beingUsed) { mDisplayingCount++; mHasBeenDisplayed = true; } else { mDisplayingCount--; } checkState(); } /** * Used to signal to the wrapper whether it is being referenced by a cache or not. * * @param added - true if the wrapper has been added to a cache, false if removed. */ synchronized void setCached(boolean added) { if (added) { mCacheCount++; } else { mCacheCount--; } checkState(); } private void cancelCheckStateCallback() { if (null != mCheckStateRunnable) { if (Constants.DEBUG) { Log.d(LOG_TAG, "Cancelling checkState() callback for: " + mUrl); } sHandler.removeCallbacks(mCheckStateRunnable); mCheckStateRunnable = null; } } /** * Calls {@link #checkState(boolean)} with default parameter of <code>false</code>. */ private void checkState() { checkState(false); } /** * Checks whether the wrapper is currently referenced by a cache, and is being displayed. If * neither of those conditions are met then the bitmap is ready to be recycled. Whether this * happens now, or is delayed depends on whether the Drawable has been displayed or not. <ul> * <li>If it has been displayed, it is recycled straight away.</li> <li>If it has not been * displayed, and <code>ignoreBeenDisplayed</code> is <code>false</code>, a call to * <code>checkState(true)</code> is queued to be called after a delay.</li> <li>If it has not * been displayed, and <code>ignoreBeenDisplayed</code> is <code>true</code>, it is recycled * straight away.</li> </ul> * * @param ignoreBeenDisplayed - Whether to ignore the 'has been displayed' flag when deciding * whether to recycle() now. * @see Constants#UNUSED_DRAWABLE_RECYCLE_DELAY_MS */ private synchronized void checkState(final boolean ignoreBeenDisplayed) { if (Constants.DEBUG) { Log.d(LOG_TAG, String.format( "checkState(). Been Displayed: %b, Displaying: %d, Caching: %d, URL: %s", mHasBeenDisplayed, mDisplayingCount, mCacheCount, mUrl)); } // If the policy doesn't let us recycle, return now if (!mRecyclePolicy.canRecycle()) { return; } // Cancel the callback, if one is queued. cancelCheckStateCallback(); // We're not being referenced or used anywhere if (mCacheCount <= 0 && mDisplayingCount <= 0 && hasValidBitmap()) { /** * If we have been displayed or we don't care whether we have * been or not, then recycle() now. Otherwise, we retry after a delay. */ if (mHasBeenDisplayed || ignoreBeenDisplayed) { if (Constants.DEBUG) { Log.d(LOG_TAG, "Recycling bitmap with url: " + mUrl); } // Record the current method stack just in case mStackTraceWhenRecycled = new Throwable("Recycled Bitmap Method Stack"); getBitmap().recycle(); } else { if (Constants.DEBUG) { Log.d(LOG_TAG, "Unused Bitmap which hasn't been displayed, delaying recycle(): " + mUrl); } mCheckStateRunnable = new CheckStateRunnable(this); sHandler.postDelayed(mCheckStateRunnable, Constants.UNUSED_DRAWABLE_RECYCLE_DELAY_MS); } } } /** * Runnable which run a {@link CacheableBitmapDrawable#checkState(boolean) checkState(false)} * call. * * @author chrisbanes */ private static final class CheckStateRunnable extends WeakReferenceRunnable<CacheableBitmapDrawable> { public CheckStateRunnable(CacheableBitmapDrawable object) { super(object); } @Override public void run(CacheableBitmapDrawable object) { object.checkState(true); } } }