am f4535aee: Merge "Update bitmapfun sample to call recycle()" into jb-mr1-dev

# Via Android (Google) Code Review (1) and Chris Banes (1)
* commit 'f4535aee56bf564f962dbcbdc0ce03af98f0ebd4':
  Update bitmapfun sample to call recycle()
This commit is contained in:
Chris Banes
2013-02-07 09:15:44 -08:00
committed by Android Git Automerger
7 changed files with 274 additions and 48 deletions

View File

@@ -25,7 +25,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" /> android:layout_gravity="center" />
<ImageView <com.example.android.bitmapfun.ui.RecyclingImageView
android:id="@+id/imageView" android:id="@+id/imageView"
android:layout_width="fill_parent" android:layout_width="fill_parent"
android:layout_height="fill_parent" android:layout_height="fill_parent"

View File

@@ -18,7 +18,6 @@ package com.example.android.bitmapfun.ui;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;

View File

@@ -270,7 +270,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
// Now handle the main ImageView thumbnails // Now handle the main ImageView thumbnails
ImageView imageView; ImageView imageView;
if (convertView == null) { // if it's not recycled, instantiate and initialize if (convertView == null) { // if it's not recycled, instantiate and initialize
imageView = new ImageView(mContext); imageView = new RecyclingImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP); imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setLayoutParams(mImageViewLayoutParams); imageView.setLayoutParams(mImageViewLayoutParams);
} else { // Otherwise re-use the converted view } else { // Otherwise re-use the converted view

View File

@@ -0,0 +1,89 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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 com.example.android.bitmapfun.ui;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
import android.util.AttributeSet;
import android.widget.ImageView;
import com.example.android.bitmapfun.util.RecyclingBitmapDrawable;
/**
* Sub-class of ImageView which automatically notifies the drawable when it is
* being displayed.
*/
public class RecyclingImageView extends ImageView {
public RecyclingImageView(Context context) {
super(context);
}
public RecyclingImageView(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* @see android.widget.ImageView#onDetachedFromWindow()
*/
@Override
protected void onDetachedFromWindow() {
// This has been detached from Window, so clear the drawable
setImageDrawable(null);
super.onDetachedFromWindow();
}
/**
* @see android.widget.ImageView#setImageDrawable(android.graphics.drawable.Drawable)
*/
@Override
public void setImageDrawable(Drawable drawable) {
// Keep hold of previous Drawable
final Drawable previousDrawable = getDrawable();
// Call super to set new Drawable
super.setImageDrawable(drawable);
// Notify new Drawable that it is being displayed
notifyDrawable(drawable, true);
// Notify old Drawable so it is no longer being displayed
notifyDrawable(previousDrawable, false);
}
/**
* Notifies the drawable that it's displayed state has changed.
*
* @param drawable
* @param isDisplayed
*/
private static void notifyDrawable(Drawable drawable, final boolean isDisplayed) {
if (drawable instanceof RecyclingBitmapDrawable) {
// The drawable is a CountingBitmapDrawable, so notify it
((RecyclingBitmapDrawable) drawable).setIsDisplayed(isDisplayed);
} else if (drawable instanceof LayerDrawable) {
// The drawable is a LayerDrawable, so recurse on each layer
LayerDrawable layerDrawable = (LayerDrawable) drawable;
for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) {
notifyDrawable(layerDrawable.getDrawable(i), isDisplayed);
}
}
}
}

View File

@@ -17,10 +17,10 @@
package com.example.android.bitmapfun.util; package com.example.android.bitmapfun.util;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.ActivityManager;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat; import android.graphics.Bitmap.CompressFormat;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
@@ -63,7 +63,7 @@ public class ImageCache {
private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false; private static final boolean DEFAULT_INIT_DISK_CACHE_ON_CREATE = false;
private DiskLruCache mDiskLruCache; private DiskLruCache mDiskLruCache;
private LruCache<String, Bitmap> mMemoryCache; private LruCache<String, BitmapDrawable> mMemoryCache;
private ImageCacheParams mCacheParams; private ImageCacheParams mCacheParams;
private final Object mDiskCacheLock = new Object(); private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true; private boolean mDiskCacheStarting = true;
@@ -126,14 +126,28 @@ public class ImageCache {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")"); Log.d(TAG, "Memory cache created (size = " + mCacheParams.memCacheSize + ")");
} }
mMemoryCache = new LruCache<String, Bitmap>(mCacheParams.memCacheSize) { mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
/**
* Notify the removed entry that is no longer being cached
*/
@Override
protected void entryRemoved(boolean evicted, String key,
BitmapDrawable oldValue, BitmapDrawable newValue) {
if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
// The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache
((RecyclingBitmapDrawable) oldValue).setIsCached(false);
}
}
/** /**
* Measure item size in kilobytes rather than units which is more practical * Measure item size in kilobytes rather than units which is more practical
* for a bitmap cache * for a bitmap cache
*/ */
@Override @Override
protected int sizeOf(String key, Bitmap bitmap) { protected int sizeOf(String key, BitmapDrawable value) {
final int bitmapSize = getBitmapSize(bitmap) / 1024; final int bitmapSize = getBitmapSize(value) / 1024;
return bitmapSize == 0 ? 1 : bitmapSize; return bitmapSize == 0 ? 1 : bitmapSize;
} }
}; };
@@ -184,16 +198,21 @@ public class ImageCache {
/** /**
* Adds a bitmap to both memory and disk cache. * Adds a bitmap to both memory and disk cache.
* @param data Unique identifier for the bitmap to store * @param data Unique identifier for the bitmap to store
* @param bitmap The bitmap to store * @param value The bitmap drawable to store
*/ */
public void addBitmapToCache(String data, Bitmap bitmap) { public void addBitmapToCache(String data, BitmapDrawable value) {
if (data == null || bitmap == null) { if (data == null || value == null) {
return; return;
} }
// Add to memory cache // Add to memory cache
if (mMemoryCache != null && mMemoryCache.get(data) == null) { if (mMemoryCache != null) {
mMemoryCache.put(data, bitmap); if (RecyclingBitmapDrawable.class.isInstance(value)) {
// The removed entry is a recycling drawable, so notify it
// that it has been added into the memory cache
((RecyclingBitmapDrawable) value).setIsCached(true);
}
mMemoryCache.put(data, value);
} }
synchronized (mDiskCacheLock) { synchronized (mDiskCacheLock) {
@@ -207,7 +226,7 @@ public class ImageCache {
final DiskLruCache.Editor editor = mDiskLruCache.edit(key); final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) { if (editor != null) {
out = editor.newOutputStream(DISK_CACHE_INDEX); out = editor.newOutputStream(DISK_CACHE_INDEX);
bitmap.compress( value.getBitmap().compress(
mCacheParams.compressFormat, mCacheParams.compressQuality, out); mCacheParams.compressFormat, mCacheParams.compressQuality, out);
editor.commit(); editor.commit();
out.close(); out.close();
@@ -234,19 +253,20 @@ public class ImageCache {
* Get from memory cache. * Get from memory cache.
* *
* @param data Unique identifier for which item to get * @param data Unique identifier for which item to get
* @return The bitmap if found in cache, null otherwise * @return The bitmap drawable if found in cache, null otherwise
*/ */
public Bitmap getBitmapFromMemCache(String data) { public BitmapDrawable getBitmapFromMemCache(String data) {
BitmapDrawable memValue = null;
if (mMemoryCache != null) { if (mMemoryCache != null) {
final Bitmap memBitmap = mMemoryCache.get(data); memValue = mMemoryCache.get(data);
if (memBitmap != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Memory cache hit");
}
return memBitmap;
}
} }
return null;
if (BuildConfig.DEBUG && memValue != null) {
Log.d(TAG, "Memory cache hit");
}
return memValue;
} }
/** /**
@@ -453,12 +473,14 @@ public class ImageCache {
} }
/** /**
* Get the size in bytes of a bitmap. * Get the size in bytes of a bitmap in a BitmapDrawable.
* @param bitmap * @param value
* @return size in bytes * @return size in bytes
*/ */
@TargetApi(12) @TargetApi(12)
public static int getBitmapSize(Bitmap bitmap) { public static int getBitmapSize(BitmapDrawable value) {
Bitmap bitmap = value.getBitmap();
if (Utils.hasHoneycombMR1()) { if (Utils.hasHoneycombMR1()) {
return bitmap.getByteCount(); return bitmap.getByteCount();
} }

View File

@@ -76,15 +76,15 @@ public abstract class ImageWorker {
return; return;
} }
Bitmap bitmap = null; BitmapDrawable value = null;
if (mImageCache != null) { if (mImageCache != null) {
bitmap = mImageCache.getBitmapFromMemCache(String.valueOf(data)); value = mImageCache.getBitmapFromMemCache(String.valueOf(data));
} }
if (bitmap != null) { if (value != null) {
// Bitmap found in memory cache // Bitmap found in memory cache
imageView.setImageBitmap(bitmap); imageView.setImageDrawable(value);
} else if (cancelPotentialWork(data, imageView)) { } else if (cancelPotentialWork(data, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView); final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable = final AsyncDrawable asyncDrawable =
@@ -222,7 +222,7 @@ public abstract class ImageWorker {
/** /**
* The actual AsyncTask that will asynchronously process the image. * The actual AsyncTask that will asynchronously process the image.
*/ */
private class BitmapWorkerTask extends AsyncTask<Object, Void, Bitmap> { private class BitmapWorkerTask extends AsyncTask<Object, Void, BitmapDrawable> {
private Object data; private Object data;
private final WeakReference<ImageView> imageViewReference; private final WeakReference<ImageView> imageViewReference;
@@ -234,7 +234,7 @@ public abstract class ImageWorker {
* Background processing. * Background processing.
*/ */
@Override @Override
protected Bitmap doInBackground(Object... params) { protected BitmapDrawable doInBackground(Object... params) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(TAG, "doInBackground - starting work"); Log.d(TAG, "doInBackground - starting work");
} }
@@ -242,6 +242,7 @@ public abstract class ImageWorker {
data = params[0]; data = params[0];
final String dataString = String.valueOf(data); final String dataString = String.valueOf(data);
Bitmap bitmap = null; Bitmap bitmap = null;
BitmapDrawable drawable = null;
// Wait here if work is paused and the task is not cancelled // Wait here if work is paused and the task is not cancelled
synchronized (mPauseWorkLock) { synchronized (mPauseWorkLock) {
@@ -274,39 +275,50 @@ public abstract class ImageWorker {
// bitmap to the cache for future use. Note we don't check if the task was cancelled // bitmap to the cache for future use. Note we don't check if the task was cancelled
// here, if it was, and the thread is still running, we may as well add the processed // here, if it was, and the thread is still running, we may as well add the processed
// bitmap to our cache as it might be used again in the future // bitmap to our cache as it might be used again in the future
if (bitmap != null && mImageCache != null) { if (bitmap != null) {
mImageCache.addBitmapToCache(dataString, bitmap); if (Utils.hasHoneycomb()) {
// Running on Honeycomb or newer, so wrap in a standard BitmapDrawable
drawable = new BitmapDrawable(mResources, bitmap);
} else {
// Running on Gingerbread or older, so wrap in a RecyclingBitmapDrawable
// which will recycle automagically
drawable = new RecyclingBitmapDrawable(mResources, bitmap);
}
if (mImageCache != null) {
mImageCache.addBitmapToCache(dataString, drawable);
}
} }
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(TAG, "doInBackground - finished work"); Log.d(TAG, "doInBackground - finished work");
} }
return bitmap; return drawable;
} }
/** /**
* Once the image is processed, associates it to the imageView * Once the image is processed, associates it to the imageView
*/ */
@Override @Override
protected void onPostExecute(Bitmap bitmap) { protected void onPostExecute(BitmapDrawable value) {
// if cancel was called on this task or the "exit early" flag is set then we're done // if cancel was called on this task or the "exit early" flag is set then we're done
if (isCancelled() || mExitTasksEarly) { if (isCancelled() || mExitTasksEarly) {
bitmap = null; value = null;
} }
final ImageView imageView = getAttachedImageView(); final ImageView imageView = getAttachedImageView();
if (bitmap != null && imageView != null) { if (value != null && imageView != null) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(TAG, "onPostExecute - setting bitmap"); Log.d(TAG, "onPostExecute - setting bitmap");
} }
setImageBitmap(imageView, bitmap); setImageDrawable(imageView, value);
} }
} }
@Override @Override
protected void onCancelled(Bitmap bitmap) { protected void onCancelled(BitmapDrawable value) {
super.onCancelled(bitmap); super.onCancelled(value);
synchronized (mPauseWorkLock) { synchronized (mPauseWorkLock) {
mPauseWorkLock.notifyAll(); mPauseWorkLock.notifyAll();
} }
@@ -349,18 +361,19 @@ public abstract class ImageWorker {
} }
/** /**
* Called when the processing is complete and the final bitmap should be set on the ImageView. * Called when the processing is complete and the final drawable should be
* set on the ImageView.
* *
* @param imageView * @param imageView
* @param bitmap * @param drawable
*/ */
private void setImageBitmap(ImageView imageView, Bitmap bitmap) { private void setImageDrawable(ImageView imageView, Drawable drawable) {
if (mFadeInBitmap) { if (mFadeInBitmap) {
// Transition drawable with a transparent drwabale and the final bitmap // Transition drawable with a transparent drawable and the final drawable
final TransitionDrawable td = final TransitionDrawable td =
new TransitionDrawable(new Drawable[] { new TransitionDrawable(new Drawable[] {
new ColorDrawable(android.R.color.transparent), new ColorDrawable(android.R.color.transparent),
new BitmapDrawable(mResources, bitmap) drawable
}); });
// Set background to loading bitmap // Set background to loading bitmap
imageView.setBackgroundDrawable( imageView.setBackgroundDrawable(
@@ -369,7 +382,7 @@ public abstract class ImageWorker {
imageView.setImageDrawable(td); imageView.setImageDrawable(td);
td.startTransition(FADE_IN_TIME); td.startTransition(FADE_IN_TIME);
} else { } else {
imageView.setImageBitmap(bitmap); imageView.setImageDrawable(drawable);
} }
} }

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2013 The Android Open Source Project
*
* 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 com.example.android.bitmapfun.util;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
import com.example.android.bitmapfun.BuildConfig;
/**
* A BitmapDrawable that keeps track of whether it is being displayed or cached.
* When the drawable is no longer being displayed or cached,
* {@link Bitmap#recycle() recycle()} will be called on this drawable's bitmap.
*/
public class RecyclingBitmapDrawable extends BitmapDrawable {
static final String LOG_TAG = "CountingBitmapDrawable";
private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
private boolean mHasBeenDisplayed;
public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) {
super(res, bitmap);
}
/**
* Notify the drawable that the displayed state has changed. Internally a
* count is kept so that the drawable knows when it is no longer being
* displayed.
*
* @param isDisplayed - Whether the drawable is being displayed or not
*/
public void setIsDisplayed(boolean isDisplayed) {
synchronized (this) {
if (isDisplayed) {
mDisplayRefCount++;
mHasBeenDisplayed = true;
} else {
mDisplayRefCount--;
}
}
// Check to see if recycle() can be called
checkState();
}
/**
* Notify the drawable that the cache state has changed. Internally a count
* is kept so that the drawable knows when it is no longer being cached.
*
* @param isCached - Whether the drawable is being cached or not
*/
public void setIsCached(boolean isCached) {
synchronized (this) {
if (isCached) {
mCacheRefCount++;
} else {
mCacheRefCount--;
}
}
// Check to see if recycle() can be called
checkState();
}
private synchronized void checkState() {
// If the drawable cache and display ref counts = 0, and this drawable
// has been displayed, then recycle
if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
&& hasValidBitmap()) {
if (BuildConfig.DEBUG) {
Log.d(LOG_TAG, "No longer being used or cached so recycling. "
+ toString());
}
getBitmap().recycle();
}
}
private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}
}