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