bitmapfun: Add support to use inBitmap option

This adds support for devices running Honeycomb or newer
to use the inBitmap BitmapFactory option when decoding images. The way
it accomplishes this is by adding a second level 'memory cache' using
SoftReferences. When an item is removed from the LruCache it is added
to this SoftReference Set. The next time an image is decoded, the Set
is searched to see if it contains a bitmap that can be re-used.

Change-Id: I8f980160ddc7116731b25e6f3f110c38f207f286
Signed-off-by: Chris Banes <chrisbanes@google.com>
This commit is contained in:
Chris Banes
2013-02-07 14:02:46 +00:00
parent 4d6574e878
commit 3b7f23f881
4 changed files with 132 additions and 9 deletions

View File

@@ -20,8 +20,8 @@ import android.annotation.TargetApi;
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.graphics.drawable.BitmapDrawable;
import android.os.Bundle; import android.os.Bundle;
import android.os.Environment; import android.os.Environment;
import android.os.StatFs; import android.os.StatFs;
@@ -33,11 +33,16 @@ import android.util.Log;
import com.example.android.bitmapfun.BuildConfig; import com.example.android.bitmapfun.BuildConfig;
import java.io.File; import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Iterator;
/** /**
* This class holds our bitmap caches (memory and disk). * This class holds our bitmap caches (memory and disk).
@@ -68,6 +73,8 @@ public class ImageCache {
private final Object mDiskCacheLock = new Object(); private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true; private boolean mDiskCacheStarting = true;
private HashSet<SoftReference<Bitmap>> mReusableBitmaps;
/** /**
* Creating a new ImageCache object using the specified parameters. * Creating a new ImageCache object using the specified parameters.
* *
@@ -126,6 +133,12 @@ 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 + ")");
} }
// If we're running on Honeycomb or newer, then
if (Utils.hasHoneycomb()) {
mReusableBitmaps = new HashSet<SoftReference<Bitmap>>();
}
mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) { mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
/** /**
@@ -138,6 +151,14 @@ public class ImageCache {
// The removed entry is a recycling drawable, so notify it // The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache // that it has been removed from the memory cache
((RecyclingBitmapDrawable) oldValue).setIsCached(false); ((RecyclingBitmapDrawable) oldValue).setIsCached(false);
} else {
// The removed entry is a standard BitmapDrawable
if (Utils.hasHoneycomb()) {
// We're running on Honeycomb or later, so add the bitmap
// to a SoftRefrence set for possible use with inBitmap later
mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
} }
} }
@@ -277,6 +298,8 @@ public class ImageCache {
*/ */
public Bitmap getBitmapFromDiskCache(String data) { public Bitmap getBitmapFromDiskCache(String data) {
final String key = hashKeyForDisk(data); final String key = hashKeyForDisk(data);
Bitmap bitmap = null;
synchronized (mDiskCacheLock) { synchronized (mDiskCacheLock) {
while (mDiskCacheStarting) { while (mDiskCacheStarting) {
try { try {
@@ -293,8 +316,12 @@ public class ImageCache {
} }
inputStream = snapshot.getInputStream(DISK_CACHE_INDEX); inputStream = snapshot.getInputStream(DISK_CACHE_INDEX);
if (inputStream != null) { if (inputStream != null) {
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream); FileDescriptor fd = ((FileInputStream) inputStream).getFD();
return bitmap;
// Decode bitmap, but we don't want to sample so give
// MAX_VALUE as the target dimensions
bitmap = ImageResizer.decodeSampledBitmapFromDescriptor(
fd, Integer.MAX_VALUE, Integer.MAX_VALUE, this);
} }
} }
} catch (final IOException e) { } catch (final IOException e) {
@@ -307,10 +334,43 @@ public class ImageCache {
} catch (IOException e) {} } catch (IOException e) {}
} }
} }
return null; return bitmap;
} }
} }
/**
* @param options - BitmapFactory.Options with out* options populated
* @return Bitmap that case be used for inBitmap
*/
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;
if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (null != item && item.isMutable()) {
// Check to see it the item can be used for inBitmap
if (canUseForInBitmap(item, options)) {
bitmap = item;
// Remove from reusable set so it can't be used again
iterator.remove();
break;
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove();
}
}
}
return bitmap;
}
/** /**
* Clears both the memory and disk cache associated with this ImageCache object. Note that * Clears both the memory and disk cache associated with this ImageCache object. Note that
* this includes disk access so this should not be executed on the main/UI thread. * this includes disk access so this should not be executed on the main/UI thread.
@@ -425,6 +485,20 @@ public class ImageCache {
} }
} }
/**
* @param candidate - Bitmap to check
* @param targetOptions - Options that have the out* value populated
* @return true if <code>candidate</code> can be used for inBitmap re-use with
* <code>targetOptions</code>
*/
private static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
return candidate.getWidth() == width && candidate.getHeight() == height;
}
/** /**
* Get a usable cache directory (external if available, internal otherwise). * Get a usable cache directory (external if available, internal otherwise).
* *

View File

@@ -241,7 +241,8 @@ public class ImageFetcher extends ImageResizer {
Bitmap bitmap = null; Bitmap bitmap = null;
if (fileDescriptor != null) { if (fileDescriptor != null) {
bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth, mImageHeight); bitmap = decodeSampledBitmapFromDescriptor(fileDescriptor, mImageWidth,
mImageHeight, getImageCache());
} }
if (fileInputStream != null) { if (fileInputStream != null) {
try { try {

View File

@@ -16,10 +16,12 @@
package com.example.android.bitmapfun.util; package com.example.android.bitmapfun.util;
import android.annotation.TargetApi;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory; import android.graphics.BitmapFactory;
import android.os.Build;
import android.util.Log; import android.util.Log;
import com.example.android.bitmapfun.BuildConfig; import com.example.android.bitmapfun.BuildConfig;
@@ -90,7 +92,8 @@ public class ImageResizer extends ImageWorker {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Log.d(TAG, "processBitmap - " + resId); Log.d(TAG, "processBitmap - " + resId);
} }
return decodeSampledBitmapFromResource(mResources, resId, mImageWidth, mImageHeight); return decodeSampledBitmapFromResource(mResources, resId, mImageWidth,
mImageHeight, getImageCache());
} }
@Override @Override
@@ -105,11 +108,12 @@ public class ImageResizer extends ImageWorker {
* @param resId The resource id of the image data * @param resId The resource id of the image data
* @param reqWidth The requested width of the resulting bitmap * @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap
* @param cache The ImageCache used to find candidate bitmaps for use with inBitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height * that are equal to or greater than the requested width and height
*/ */
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) { int reqWidth, int reqHeight, ImageCache cache) {
// First decode with inJustDecodeBounds=true to check dimensions // First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options(); final BitmapFactory.Options options = new BitmapFactory.Options();
@@ -119,6 +123,11 @@ public class ImageResizer extends ImageWorker {
// Calculate inSampleSize // Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// If we're running on Honeycomb or newer, try to use inBitmap
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
// Decode bitmap with inSampleSize set // Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false; options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options); return BitmapFactory.decodeResource(res, resId, options);
@@ -130,11 +139,12 @@ public class ImageResizer extends ImageWorker {
* @param filename The full path of the file to decode * @param filename The full path of the file to decode
* @param reqWidth The requested width of the resulting bitmap * @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap
* @param cache The ImageCache used to find candidate bitmaps for use with inBitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height * that are equal to or greater than the requested width and height
*/ */
public static Bitmap decodeSampledBitmapFromFile(String filename, public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight) { int reqWidth, int reqHeight, ImageCache cache) {
// First decode with inJustDecodeBounds=true to check dimensions // First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options(); final BitmapFactory.Options options = new BitmapFactory.Options();
@@ -144,6 +154,11 @@ public class ImageResizer extends ImageWorker {
// Calculate inSampleSize // Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// If we're running on Honeycomb or newer, try to use inBitmap
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
// Decode bitmap with inSampleSize set // Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false; options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(filename, options); return BitmapFactory.decodeFile(filename, options);
@@ -155,11 +170,12 @@ public class ImageResizer extends ImageWorker {
* @param fileDescriptor The file descriptor to read from * @param fileDescriptor The file descriptor to read from
* @param reqWidth The requested width of the resulting bitmap * @param reqWidth The requested width of the resulting bitmap
* @param reqHeight The requested height of the resulting bitmap * @param reqHeight The requested height of the resulting bitmap
* @param cache The ImageCache used to find candidate bitmaps for use with inBitmap
* @return A bitmap sampled down from the original with the same aspect ratio and dimensions * @return A bitmap sampled down from the original with the same aspect ratio and dimensions
* that are equal to or greater than the requested width and height * that are equal to or greater than the requested width and height
*/ */
public static Bitmap decodeSampledBitmapFromDescriptor( public static Bitmap decodeSampledBitmapFromDescriptor(
FileDescriptor fileDescriptor, int reqWidth, int reqHeight) { FileDescriptor fileDescriptor, int reqWidth, int reqHeight, ImageCache cache) {
// First decode with inJustDecodeBounds=true to check dimensions // First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options(); final BitmapFactory.Options options = new BitmapFactory.Options();
@@ -171,9 +187,34 @@ public class ImageResizer extends ImageWorker {
// Decode bitmap with inSampleSize set // Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false; options.inJustDecodeBounds = false;
// If we're running on Honeycomb or newer, try to use inBitmap
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options); return BitmapFactory.decodeFileDescriptor(fileDescriptor, null, options);
} }
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
// inBitmap only works with mutable bitmaps so force the decoder to
// return mutable bitmaps.
options.inMutable = true;
if (cache != null) {
// Try and find a bitmap to use for inBitmap
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
if (BuildConfig.DEBUG) {
Log.d(TAG, "Found bitmap to use for inBitmap");
}
options.inBitmap = inBitmap;
}
}
}
/** /**
* Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding * Calculate an inSampleSize for use in a {@link BitmapFactory.Options} object when decoding
* bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates * bitmaps using the decode* methods from {@link BitmapFactory}. This implementation calculates

View File

@@ -164,6 +164,13 @@ public abstract class ImageWorker {
*/ */
protected abstract Bitmap processBitmap(Object data); protected abstract Bitmap processBitmap(Object data);
/**
* @return The {@link ImageCache} object currently being used by this ImageWorker.
*/
protected ImageCache getImageCache() {
return mImageCache;
}
/** /**
* Cancels any pending work attached to the provided ImageView. * Cancels any pending work attached to the provided ImageView.
* @param imageView * @param imageView