1page.title=Caching Bitmaps
2parent.title=Displaying Bitmaps Efficiently
3parent.link=index.html
4
5trainingnavtop=true
6
7@jd:body
8
9<div id="tb-wrapper">
10<div id="tb">
11
12<h2>This lesson teaches you to</h2>
13<ol>
14  <li><a href="#memory-cache">Use a Memory Cache</a></li>
15  <li><a href="#disk-cache">Use a Disk Cache</a></li>
16  <li><a href="#config-changes">Handle Configuration Changes</a></li>
17</ol>
18
19<h2>You should also read</h2>
20<ul>
21  <li><a href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a></li>
22</ul>
23
24<h2>Try it out</h2>
25
26<div class="download-box">
27  <a href="{@docRoot}downloads/samples/DisplayingBitmaps.zip" class="button">Download the sample</a>
28  <p class="filename">DisplayingBitmaps.zip</p>
29</div>
30
31</div>
32</div>
33
34<p>Loading a single bitmap into your user interface (UI) is straightforward, however things get more
35complicated if you need to load a larger set of images at once. In many cases (such as with
36components like {@link android.widget.ListView}, {@link android.widget.GridView} or {@link
37android.support.v4.view.ViewPager }), the total number of images on-screen combined with images that
38might soon scroll onto the screen are essentially unlimited.</p>
39
40<p>Memory usage is kept down with components like this by recycling the child views as they move
41off-screen. The garbage collector also frees up your loaded bitmaps, assuming you don't keep any
42long lived references. This is all good and well, but in order to keep a fluid and fast-loading UI
43you want to avoid continually processing these images each time they come back on-screen. A memory
44and disk cache can often help here, allowing components to quickly reload processed images.</p>
45
46<p>This lesson walks you through using a memory and disk bitmap cache to improve the responsiveness
47and fluidity of your UI when loading multiple bitmaps.</p>
48
49<h2 id="memory-cache">Use a Memory Cache</h2>
50
51<p>A memory cache offers fast access to bitmaps at the cost of taking up valuable application
52memory. The {@link android.util.LruCache} class (also available in the <a
53href="{@docRoot}reference/android/support/v4/util/LruCache.html">Support Library</a> for use back
54to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently
55referenced objects in a strong referenced {@link java.util.LinkedHashMap} and evicting the least
56recently used member before the cache exceeds its designated size.</p>
57
58<p class="note"><strong>Note:</strong> In the past, a popular memory cache implementation was a
59{@link java.lang.ref.SoftReference} or {@link java.lang.ref.WeakReference} bitmap cache, however
60this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more
61aggressive with collecting soft/weak references which makes them fairly ineffective. In addition,
62prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which
63is not released in a predictable manner, potentially causing an application to briefly exceed its
64memory limits and crash.</p>
65
66<p>In order to choose a suitable size for a {@link android.util.LruCache}, a number of factors
67should be taken into consideration, for example:</p>
68
69<ul>
70  <li>How memory intensive is the rest of your activity and/or application?</li>
71  <li>How many images will be on-screen at once? How many need to be available ready to come
72  on-screen?</li>
73  <li>What is the screen size and density of the device? An extra high density screen (xhdpi) device
74  like <a href="http://www.android.com/devices/detail/galaxy-nexus">Galaxy Nexus</a> will need a
75  larger cache to hold the same number of images in memory compared to a device like <a
76  href="http://www.android.com/devices/detail/nexus-s">Nexus S</a> (hdpi).</li>
77  <li>What dimensions and configuration are the bitmaps and therefore how much memory will each take
78  up?</li>
79  <li>How frequently will the images be accessed? Will some be accessed more frequently than others?
80  If so, perhaps you may want to keep certain items always in memory or even have multiple {@link
81  android.util.LruCache} objects for different groups of bitmaps.</li>
82  <li>Can you balance quality against quantity? Sometimes it can be more useful to store a larger
83  number of lower quality bitmaps, potentially loading a higher quality version in another
84  background task.</li>
85</ul>
86
87<p>There is no specific size or formula that suits all applications, it's up to you to analyze your
88usage and come up with a suitable solution. A cache that is too small causes additional overhead with
89no benefit, a cache that is too large can once again cause {@code java.lang.OutOfMemory} exceptions
90and leave the rest of your app little memory to work with.</p>
91
92<p>Here’s an example of setting up a {@link android.util.LruCache} for bitmaps:</p>
93
94<pre>
95private LruCache&lt;String, Bitmap&gt; mMemoryCache;
96
97&#64;Override
98protected void onCreate(Bundle savedInstanceState) {
99    ...
100    // Get max available VM memory, exceeding this amount will throw an
101    // OutOfMemory exception. Stored in kilobytes as LruCache takes an
102    // int in its constructor.
103    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
104
105    // Use 1/8th of the available memory for this memory cache.
106    final int cacheSize = maxMemory / 8;
107
108    mMemoryCache = new LruCache&lt;String, Bitmap&gt;(cacheSize) {
109        &#64;Override
110        protected int sizeOf(String key, Bitmap bitmap) {
111            // The cache size will be measured in kilobytes rather than
112            // number of items.
113            return bitmap.getByteCount() / 1024;
114        }
115    };
116    ...
117}
118
119public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
120    if (getBitmapFromMemCache(key) == null) {
121        mMemoryCache.put(key, bitmap);
122    }
123}
124
125public Bitmap getBitmapFromMemCache(String key) {
126    return mMemoryCache.get(key);
127}
128</pre>
129
130<p class="note"><strong>Note:</strong> In this example, one eighth of the application memory is
131allocated for our cache. On a normal/hdpi device this is a minimum of around 4MB (32/8). A full
132screen {@link android.widget.GridView} filled with images on a device with 800x480 resolution would
133use around 1.5MB (800*480*4 bytes), so this would cache a minimum of around 2.5 pages of images in
134memory.</p>
135
136<p>When loading a bitmap into an {@link android.widget.ImageView}, the {@link android.util.LruCache}
137is checked first. If an entry is found, it is used immediately to update the {@link
138android.widget.ImageView}, otherwise a background thread is spawned to process the image:</p>
139
140<pre>
141public void loadBitmap(int resId, ImageView imageView) {
142    final String imageKey = String.valueOf(resId);
143
144    final Bitmap bitmap = getBitmapFromMemCache(imageKey);
145    if (bitmap != null) {
146        mImageView.setImageBitmap(bitmap);
147    } else {
148        mImageView.setImageResource(R.drawable.image_placeholder);
149        BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
150        task.execute(resId);
151    }
152}
153</pre>
154
155<p>The <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> also needs to be
156updated to add entries to the memory cache:</p>
157
158<pre>
159class BitmapWorkerTask extends AsyncTask&lt;Integer, Void, Bitmap&gt; {
160    ...
161    // Decode image in background.
162    &#64;Override
163    protected Bitmap doInBackground(Integer... params) {
164        final Bitmap bitmap = decodeSampledBitmapFromResource(
165                getResources(), params[0], 100, 100));
166        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
167        return bitmap;
168    }
169    ...
170}
171</pre>
172
173<h2 id="disk-cache">Use a Disk Cache</h2>
174
175<p>A memory cache is useful in speeding up access to recently viewed bitmaps, however you cannot
176rely on images being available in this cache. Components like {@link android.widget.GridView} with
177larger datasets can easily fill up a memory cache. Your application could be interrupted by another
178task like a phone call, and while in the background it might be killed and the memory cache
179destroyed. Once the user resumes, your application has to process each image again.</p>
180
181<p>A disk cache can be used in these cases to persist processed bitmaps and help decrease loading
182times where images are no longer available in a memory cache. Of course, fetching images from disk
183is slower than loading from memory and should be done in a background thread, as disk read times can
184be unpredictable.</p>
185
186<p class="note"><strong>Note:</strong> A {@link android.content.ContentProvider} might be a more
187appropriate place to store cached images if they are accessed more frequently, for example in an
188image gallery application.</p>
189
190<p>The sample code of this class uses a {@code DiskLruCache} implementation that is pulled from the
191<a href="https://android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java">Android source</a>.
192Here’s updated example code that adds a disk cache in addition to the existing memory cache:</p>
193
194<pre>
195private DiskLruCache mDiskLruCache;
196private final Object mDiskCacheLock = new Object();
197private boolean mDiskCacheStarting = true;
198private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
199private static final String DISK_CACHE_SUBDIR = "thumbnails";
200
201&#64;Override
202protected void onCreate(Bundle savedInstanceState) {
203    ...
204    // Initialize memory cache
205    ...
206    // Initialize disk cache on background thread
207    File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
208    new InitDiskCacheTask().execute(cacheDir);
209    ...
210}
211
212class InitDiskCacheTask extends AsyncTask&lt;File, Void, Void&gt; {
213    &#64;Override
214    protected Void doInBackground(File... params) {
215        synchronized (mDiskCacheLock) {
216            File cacheDir = params[0];
217            mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
218            mDiskCacheStarting = false; // Finished initialization
219            mDiskCacheLock.notifyAll(); // Wake any waiting threads
220        }
221        return null;
222    }
223}
224
225class BitmapWorkerTask extends AsyncTask&lt;Integer, Void, Bitmap&gt; {
226    ...
227    // Decode image in background.
228    &#64;Override
229    protected Bitmap doInBackground(Integer... params) {
230        final String imageKey = String.valueOf(params[0]);
231
232        // Check disk cache in background thread
233        Bitmap bitmap = getBitmapFromDiskCache(imageKey);
234
235        if (bitmap == null) { // Not found in disk cache
236            // Process as normal
237            final Bitmap bitmap = decodeSampledBitmapFromResource(
238                    getResources(), params[0], 100, 100));
239        }
240
241        // Add final bitmap to caches
242        addBitmapToCache(imageKey, bitmap);
243
244        return bitmap;
245    }
246    ...
247}
248
249public void addBitmapToCache(String key, Bitmap bitmap) {
250    // Add to memory cache as before
251    if (getBitmapFromMemCache(key) == null) {
252        mMemoryCache.put(key, bitmap);
253    }
254
255    // Also add to disk cache
256    synchronized (mDiskCacheLock) {
257        if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
258            mDiskLruCache.put(key, bitmap);
259        }
260    }
261}
262
263public Bitmap getBitmapFromDiskCache(String key) {
264    synchronized (mDiskCacheLock) {
265        // Wait while disk cache is started from background thread
266        while (mDiskCacheStarting) {
267            try {
268                mDiskCacheLock.wait();
269            } catch (InterruptedException e) {}
270        }
271        if (mDiskLruCache != null) {
272            return mDiskLruCache.get(key);
273        }
274    }
275    return null;
276}
277
278// Creates a unique subdirectory of the designated app cache directory. Tries to use external
279// but if not mounted, falls back on internal storage.
280public static File getDiskCacheDir(Context context, String uniqueName) {
281    // Check if media is mounted or storage is built-in, if so, try and use external cache dir
282    // otherwise use internal cache dir
283    final String cachePath =
284            Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
285                    !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
286                            context.getCacheDir().getPath();
287
288    return new File(cachePath + File.separator + uniqueName);
289}
290</pre>
291
292<p class="note"><strong>Note:</strong> Even initializing the disk cache requires disk operations
293and therefore should not take place on the main thread. However, this does mean there's a chance
294the cache is accessed before initialization. To address this, in the above implementation, a lock
295object ensures that the app does not read from the disk cache until the cache has been
296initialized.</p>
297
298<p>While the memory cache is checked in the UI thread, the disk cache is checked in the background
299thread. Disk operations should never take place on the UI thread. When image processing is
300complete, the final bitmap is added to both the memory and disk cache for future use.</p>
301
302<h2 id="config-changes">Handle Configuration Changes</h2>
303
304<p>Runtime configuration changes, such as a screen orientation change, cause Android to destroy and
305restart the running activity with the new configuration (For more information about this behavior,
306see <a href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a>).
307You want to avoid having to process all your images again so the user has a smooth and fast
308experience when a configuration change occurs.</p>
309
310<p>Luckily, you have a nice memory cache of bitmaps that you built in the <a
311href="#memory-cache">Use a Memory Cache</a> section. This cache can be passed through to the new
312activity instance using a {@link android.app.Fragment} which is preserved by calling {@link
313android.app.Fragment#setRetainInstance setRetainInstance(true)}). After the activity has been
314recreated, this retained {@link android.app.Fragment} is reattached and you gain access to the
315existing cache object, allowing images to be quickly fetched and re-populated into the {@link
316android.widget.ImageView} objects.</p>
317
318<p>Here’s an example of retaining a {@link android.util.LruCache} object across configuration
319changes using a {@link android.app.Fragment}:</p>
320
321<pre>
322private LruCache&lt;String, Bitmap&gt; mMemoryCache;
323
324&#64;Override
325protected void onCreate(Bundle savedInstanceState) {
326    ...
327    RetainFragment retainFragment =
328            RetainFragment.findOrCreateRetainFragment(getFragmentManager());
329    mMemoryCache = retainFragment.mRetainedCache;
330    if (mMemoryCache == null) {
331        mMemoryCache = new LruCache&lt;String, Bitmap&gt;(cacheSize) {
332            ... // Initialize cache here as usual
333        }
334        retainFragment.mRetainedCache = mMemoryCache;
335    }
336    ...
337}
338
339class RetainFragment extends Fragment {
340    private static final String TAG = "RetainFragment";
341    public LruCache&lt;String, Bitmap&gt; mRetainedCache;
342
343    public RetainFragment() {}
344
345    public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
346        RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
347        if (fragment == null) {
348            fragment = new RetainFragment();
349            fm.beginTransaction().add(fragment, TAG).commit();
350        }
351        return fragment;
352    }
353
354    &#64;Override
355    public void onCreate(Bundle savedInstanceState) {
356        super.onCreate(savedInstanceState);
357        <strong>setRetainInstance(true);</strong>
358    }
359}
360</pre>
361
362<p>To test this out, try rotating a device both with and without retaining the {@link
363android.app.Fragment}. You should notice little to no lag as the images populate the activity almost
364instantly from memory when you retain the cache. Any images not found in the memory cache are
365hopefully available in the disk cache, if not, they are processed as usual.</p>
366