Merge "Updates to "Displaying Bitmaps Efficiently" class. Changes: -Updated code sample (see http://ag/214812) -Updated code snippets to match updated sample -Fixed <> in code snippets -Updated disk cache section -Some other minor updates" into jb-dev

This commit is contained in:
Adam Koch
2012-09-07 13:15:01 -07:00
committed by Android (Google) Code Review
5 changed files with 81 additions and 49 deletions

View File

@ -96,7 +96,7 @@ and leave the rest of your app little memory to work with.</p>
<p>Heres an example of setting up a {@link android.util.LruCache} for bitmaps:</p> <p>Heres an example of setting up a {@link android.util.LruCache} for bitmaps:</p>
<pre> <pre>
private LruCache<String, Bitmap> mMemoryCache; private LruCache&lt;String, Bitmap&gt; mMemoryCache;
&#64;Override &#64;Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -109,7 +109,7 @@ protected void onCreate(Bundle savedInstanceState) {
// Use 1/8th of the available memory for this memory cache. // Use 1/8th of the available memory for this memory cache.
final int cacheSize = 1024 * 1024 * memClass / 8; final int cacheSize = 1024 * 1024 * memClass / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { mMemoryCache = new LruCache&lt;String, Bitmap&gt;(cacheSize) {
&#64;Override &#64;Override
protected int sizeOf(String key, Bitmap bitmap) { protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in bytes rather than number of items. // The cache size will be measured in bytes rather than number of items.
@ -159,7 +159,7 @@ public void loadBitmap(int resId, ImageView imageView) {
updated to add entries to the memory cache:</p> updated to add entries to the memory cache:</p>
<pre> <pre>
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { class BitmapWorkerTask extends AsyncTask&lt;Integer, Void, Bitmap&gt; {
... ...
// Decode image in background. // Decode image in background.
&#64;Override &#64;Override
@ -179,7 +179,7 @@ class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
rely on images being available in this cache. Components like {@link android.widget.GridView} with rely on images being available in this cache. Components like {@link android.widget.GridView} with
larger datasets can easily fill up a memory cache. Your application could be interrupted by another larger datasets can easily fill up a memory cache. Your application could be interrupted by another
task like a phone call, and while in the background it might be killed and the memory cache task like a phone call, and while in the background it might be killed and the memory cache
destroyed. Once the user resumes, your application it has to process each image again.</p> destroyed. Once the user resumes, your application has to process each image again.</p>
<p>A disk cache can be used in these cases to persist processed bitmaps and help decrease loading <p>A disk cache can be used in these cases to persist processed bitmaps and help decrease loading
times where images are no longer available in a memory cache. Of course, fetching images from disk times where images are no longer available in a memory cache. Of course, fetching images from disk
@ -190,18 +190,14 @@ be unpredictable.</p>
appropriate place to store cached images if they are accessed more frequently, for example in an appropriate place to store cached images if they are accessed more frequently, for example in an
image gallery application.</p> image gallery application.</p>
<p>Included in the sample code of this class is a basic {@code DiskLruCache} implementation. <p>The sample code of this class uses a {@code DiskLruCache} implementation that is pulled from the
However, a more robust and recommended {@code DiskLruCache} solution is included in the Android 4.0 <a href="https://android.googlesource.com/platform/libcore/+/master/luni/src/main/java/libcore/io/DiskLruCache.java">Android source</a>. Heres updated example code that adds a disk cache in addition
source code ({@code libcore/luni/src/main/java/libcore/io/DiskLruCache.java}). Back-porting this to the existing memory cache:</p>
class for use on previous Android releases should be fairly straightforward (a <a
href="http://www.google.com/search?q=disklrucache">quick search</a> shows others who have already
implemented this solution).</p>
<p>Heres updated example code that uses the simple {@code DiskLruCache} included in the sample
application of this class:</p>
<pre> <pre>
private DiskLruCache mDiskCache; private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails"; private static final String DISK_CACHE_SUBDIR = "thumbnails";
@ -210,12 +206,26 @@ protected void onCreate(Bundle savedInstanceState) {
... ...
// Initialize memory cache // Initialize memory cache
... ...
File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR); // Initialize disk cache on background thread
mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE); File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
... ...
} }
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { class InitDiskCacheTask extends AsyncTask&lt;File, Void, Void&gt; {
&#64;Override
protected Void doInBackground(File... params) {
synchronized (mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting = false; // Finished initialization
mDiskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask&lt;Integer, Void, Bitmap&gt; {
... ...
// Decode image in background. // Decode image in background.
&#64;Override &#64;Override
@ -232,7 +242,7 @@ class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
} }
// Add final bitmap to caches // Add final bitmap to caches
addBitmapToCache(String.valueOf(imageKey, bitmap); addBitmapToCache(imageKey, bitmap);
return bitmap; return bitmap;
} }
@ -246,28 +256,48 @@ public void addBitmapToCache(String key, Bitmap bitmap) {
} }
// Also add to disk cache // Also add to disk cache
if (!mDiskCache.containsKey(key)) { synchronized (mDiskCacheLock) {
mDiskCache.put(key, bitmap); if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
} }
} }
public Bitmap getBitmapFromDiskCache(String key) { public Bitmap getBitmapFromDiskCache(String key) {
return mDiskCache.get(key); synchronized (mDiskCacheLock) {
// Wait while disk cache is started from background thread
while (mDiskCacheStarting) {
try {
mDiskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (mDiskLruCache != null) {
return mDiskLruCache.get(key);
}
}
return null;
} }
// Creates a unique subdirectory of the designated app cache directory. Tries to use external // Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage. // but if not mounted, falls back on internal storage.
public static File getCacheDir(Context context, String uniqueName) { public static File getDiskCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir // Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir // otherwise use internal cache dir
final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED final String cachePath =
|| !Environment.isExternalStorageRemovable() ? Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
context.getExternalCacheDir().getPath() : context.getCacheDir().getPath(); !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName); return new File(cachePath + File.separator + uniqueName);
} }
</pre> </pre>
<p class="note"><strong>Note:</strong> Even initializing the disk cache requires disk operations
and therefore should not take place on the main thread. However, this does mean there's a chance
the cache is accessed before initialization. To address this, in the above implementation, a lock
object ensures that the app does not read from the disk cache until the cache has been
initialized.</p>
<p>While the memory cache is checked in the UI thread, the disk cache is checked in the background <p>While the memory cache is checked in the UI thread, the disk cache is checked in the background
thread. Disk operations should never take place on the UI thread. When image processing is thread. Disk operations should never take place on the UI thread. When image processing is
complete, the final bitmap is added to both the memory and disk cache for future use.</p> complete, the final bitmap is added to both the memory and disk cache for future use.</p>
@ -292,7 +322,7 @@ android.widget.ImageView} objects.</p>
changes using a {@link android.app.Fragment}:</p> changes using a {@link android.app.Fragment}:</p>
<pre> <pre>
private LruCache<String, Bitmap> mMemoryCache; private LruCache&lt;String, Bitmap&gt; mMemoryCache;
&#64;Override &#64;Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@ -301,7 +331,7 @@ protected void onCreate(Bundle savedInstanceState) {
RetainFragment.findOrCreateRetainFragment(getFragmentManager()); RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = RetainFragment.mRetainedCache; mMemoryCache = RetainFragment.mRetainedCache;
if (mMemoryCache == null) { if (mMemoryCache == null) {
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { mMemoryCache = new LruCache&lt;String, Bitmap&gt;(cacheSize) {
... // Initialize cache here as usual ... // Initialize cache here as usual
} }
mRetainFragment.mRetainedCache = mMemoryCache; mRetainFragment.mRetainedCache = mMemoryCache;
@ -311,7 +341,7 @@ protected void onCreate(Bundle savedInstanceState) {
class RetainFragment extends Fragment { class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment"; private static final String TAG = "RetainFragment";
public LruCache<String, Bitmap> mRetainedCache; public LruCache&lt;String, Bitmap&gt; mRetainedCache;
public RetainFragment() {} public RetainFragment() {}

View File

@ -103,7 +103,8 @@ public class ImageDetailActivity extends FragmentActivity {
} }
</pre> </pre>
<p>The details {@link android.app.Fragment} holds the {@link android.widget.ImageView} children:</p> <p>Here is an implementation of the details {@link android.app.Fragment} which holds the {@link android.widget.ImageView} children. This might seem like a perfectly reasonable approach, but can
you see the drawbacks of this implementation? How could it be improved?</p>
<pre> <pre>
public class ImageDetailFragment extends Fragment { public class ImageDetailFragment extends Fragment {
@ -146,11 +147,11 @@ public class ImageDetailFragment extends Fragment {
} }
</pre> </pre>
<p>Hopefully you noticed the issue with this implementation; The images are being read from <p>Hopefully you noticed the issue: the images are being read from resources on the UI thread,
resources on the UI thread which can lead to an application hanging and being force closed. Using an which can lead to an application hanging and being force closed. Using an
{@link android.os.AsyncTask} as described in the <a href="process-bitmap.html">Processing Bitmaps Off {@link android.os.AsyncTask} as described in the <a href="process-bitmap.html">Processing Bitmaps
the UI Thread</a> lesson, its straightforward to move image loading and processing to a background Off the UI Thread</a> lesson, its straightforward to move image loading and processing to a
thread:</p> background thread:</p>
<pre> <pre>
public class ImageDetailActivity extends FragmentActivity { public class ImageDetailActivity extends FragmentActivity {
@ -190,7 +191,7 @@ modifications for a memory cache:</p>
<pre> <pre>
public class ImageDetailActivity extends FragmentActivity { public class ImageDetailActivity extends FragmentActivity {
... ...
private LruCache<String, Bitmap> mMemoryCache; private LruCache&lt;String, Bitmap&gt; mMemoryCache;
&#64;Override &#64;Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
@ -229,7 +230,8 @@ UI remains fluid, memory usage remains under control and concurrency is handled
the way {@link android.widget.GridView} recycles its children views).</p> the way {@link android.widget.GridView} recycles its children views).</p>
<p>To start with, here is a standard {@link android.widget.GridView} implementation with {@link <p>To start with, here is a standard {@link android.widget.GridView} implementation with {@link
android.widget.ImageView} children placed inside a {@link android.app.Fragment}:</p> android.widget.ImageView} children placed inside a {@link android.app.Fragment}. Again, this might
seem like a perfectly reasonable approach, but what would make it better?</p>
<pre> <pre>
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener { public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
@ -261,7 +263,7 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
} }
&#64;Override &#64;Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) { public void onItemClick(AdapterView&lt;?&gt; parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class); final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position); i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
startActivity(i); startActivity(i);
@ -345,13 +347,13 @@ public class ImageGridFragment extends Fragment implements AdapterView.OnItemCli
} }
static class AsyncDrawable extends BitmapDrawable { static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; private final WeakReference&lt;BitmapWorkerTask&gt; bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap, public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) { BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap); super(res, bitmap);
bitmapWorkerTaskReference = bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); new WeakReference&lt;BitmapWorkerTask&gt;(bitmapWorkerTask);
} }
public BitmapWorkerTask getBitmapWorkerTask() { public BitmapWorkerTask getBitmapWorkerTask() {

View File

@ -43,8 +43,8 @@ exception:<br />{@code java.lang.OutofMemoryError: bitmap size exceeds VM budget
perform under this minimum memory limit. However, keep in mind many devices are configured with perform under this minimum memory limit. However, keep in mind many devices are configured with
higher limits.</li> higher limits.</li>
<li>Bitmaps take up a lot of memory, especially for rich images like photographs. For example, the <li>Bitmaps take up a lot of memory, especially for rich images like photographs. For example, the
camera on the <a href="http://www.google.com/nexus/">Galaxy Nexus</a> takes photos up to 2592x1936 camera on the <a href="http://www.android.com/devices/detail/galaxy-nexus">Galaxy Nexus</a> takes
pixels (5 megapixels). If the bitmap configuration used is {@link photos up to 2592x1936 pixels (5 megapixels). If the bitmap configuration used is {@link
android.graphics.Bitmap.Config ARGB_8888} (the default from the Android 2.3 onward) then loading android.graphics.Bitmap.Config ARGB_8888} (the default from the Android 2.3 onward) then loading
this image into memory takes about 19MB of memory (2592*1936*4 bytes), immediately exhausting the this image into memory takes about 19MB of memory (2592*1936*4 bytes), immediately exhausting the
per-app limit on some devices.</li> per-app limit on some devices.</li>
@ -75,4 +75,4 @@ exception:<br />{@code java.lang.OutofMemoryError: bitmap size exceeds VM budget
components like {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView} components like {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView}
using a background thread and bitmap cache.</dd> using a background thread and bitmap cache.</dd>
</dl> </dl>

View File

@ -62,13 +62,13 @@ decodeSampledBitmapFromResource()}</a>: </p>
<a name="BitmapWorkerTask"></a> <a name="BitmapWorkerTask"></a>
<pre> <pre>
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { class BitmapWorkerTask extends AsyncTask&lt;Integer, Void, Bitmap&gt; {
private final WeakReference<ImageView> imageViewReference; private final WeakReference&lt;ImageView&gt; imageViewReference;
private int data = 0; private int data = 0;
public BitmapWorkerTask(ImageView imageView) { public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected // Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView); imageViewReference = new WeakReference&lt;ImageView&gt;(imageView);
} }
// Decode image in background. // Decode image in background.
@ -133,13 +133,13 @@ completes:</p>
<a name="AsyncDrawable"></a> <a name="AsyncDrawable"></a>
<pre> <pre>
static class AsyncDrawable extends BitmapDrawable { static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference; private final WeakReference&lt;BitmapWorkerTask&gt; bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap, public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) { BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap); super(res, bitmap);
bitmapWorkerTaskReference = bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask); new WeakReference&lt;BitmapWorkerTask&gt;(bitmapWorkerTask);
} }
public BitmapWorkerTask getBitmapWorkerTask() { public BitmapWorkerTask getBitmapWorkerTask() {
@ -211,7 +211,7 @@ one associated with the {@link android.widget.ImageView}:</p>
<a name="BitmapWorkerTaskUpdated"></a> <a name="BitmapWorkerTaskUpdated"></a>
<pre> <pre>
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { class BitmapWorkerTask extends AsyncTask&lt;Integer, Void, Bitmap&gt; {
... ...
&#64;Override &#64;Override
@ -236,4 +236,4 @@ class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
android.widget.GridView} components as well as any other components that recycle their child android.widget.GridView} components as well as any other components that recycle their child
views. Simply call {@code loadBitmap} where you normally set an image to your {@link views. Simply call {@code loadBitmap} where you normally set an image to your {@link
android.widget.ImageView}. For example, in a {@link android.widget.GridView} implementation this android.widget.ImageView}. For example, in a {@link android.widget.GridView} implementation this
would be in the {@link android.widget.Adapter#getView getView()} method of the backing adapter.</p> would be in the {@link android.widget.Adapter#getView getView()} method of the backing adapter.</p>