• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 page.title=Caching Bitmaps
2 parent.title=Displaying Bitmaps Efficiently
3 parent.link=index.html
4 
5 trainingnavtop=true
6 next.title=Displaying Bitmaps in Your UI
7 next.link=display-bitmap.html
8 previous.title=Processing Bitmaps Off the UI Thread
9 previous.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
39 complicated if you need to load a larger set of images at once. In many cases (such as with
40 components like {@link android.widget.ListView}, {@link android.widget.GridView} or {@link
41 android.support.v4.view.ViewPager }), the total number of images on-screen combined with images that
42 might 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
45 off-screen. The garbage collector also frees up your loaded bitmaps, assuming you don't keep any
46 long lived references. This is all good and well, but in order to keep a fluid and fast-loading UI
47 you want to avoid continually processing these images each time they come back on-screen. A memory
48 and 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
51 and 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
56 memory. The {@link android.util.LruCache} class (also available in the <a
57 href="{@docRoot}reference/android/support/v4/util/LruCache.html">Support Library</a> for use back
58 to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently
59 referenced objects in a strong referenced {@link java.util.LinkedHashMap} and evicting the least
60 recently 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
64 this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more
65 aggressive with collecting soft/weak references which makes them fairly ineffective. In addition,
66 prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which
67 is not released in a predictable manner, potentially causing an application to briefly exceed its
68 memory limits and crash.</p>
69 
70 <p>In order to choose a suitable size for a {@link android.util.LruCache}, a number of factors
71 should 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
92 usage and come up with a suitable solution. A cache that is too small causes additional overhead with
93 no benefit, a cache that is too large can once again cause {@code java.lang.OutOfMemory} exceptions
94 and 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>
99 private LruCache<String, Bitmap> mMemoryCache;
100 
101 &#64;Override
102 protected 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<String, Bitmap>(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 
122 public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
123     if (getBitmapFromMemCache(key) == null) {
124         mMemoryCache.put(key, bitmap);
125     }
126 }
127 
128 public 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
134 allocated for our cache. On a normal/hdpi device this is a minimum of around 4MB (32/8). A full
135 screen {@link android.widget.GridView} filled with images on a device with 800x480 resolution would
136 use around 1.5MB (800*480*4 bytes), so this would cache a minimum of around 2.5 pages of images in
137 memory.</p>
138 
139 <p>When loading a bitmap into an {@link android.widget.ImageView}, the {@link android.util.LruCache}
140 is checked first. If an entry is found, it is used immediately to update the {@link
141 android.widget.ImageView}, otherwise a background thread is spawned to process the image:</p>
142 
143 <pre>
144 public 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
159 updated to add entries to the memory cache:</p>
160 
161 <pre>
162 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
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
179 rely on images being available in this cache. Components like {@link android.widget.GridView} with
180 larger datasets can easily fill up a memory cache. Your application could be interrupted by another
181 task like a phone call, and while in the background it might be killed and the memory cache
182 destroyed. Once the user resumes, your application it 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
185 times where images are no longer available in a memory cache. Of course, fetching images from disk
186 is slower than loading from memory and should be done in a background thread, as disk read times can
187 be unpredictable.</p>
188 
189 <p class="note"><strong>Note:</strong> A {@link android.content.ContentProvider} might be a more
190 appropriate place to store cached images if they are accessed more frequently, for example in an
191 image gallery application.</p>
192 
193 <p>Included in the sample code of this class is a basic {@code DiskLruCache} implementation.
194 However, a more robust and recommended {@code DiskLruCache} solution is included in the Android 4.0
195 source code ({@code libcore/luni/src/main/java/libcore/io/DiskLruCache.java}). Back-porting this
196 class for use on previous Android releases should be fairly straightforward (a <a
197 href="http://www.google.com/search?q=disklrucache">quick search</a> shows others who have already
198 implemented this solution).</p>
199 
200 <p>Here’s updated example code that uses the simple {@code DiskLruCache} included in the sample
201 application of this class:</p>
202 
203 <pre>
204 private DiskLruCache mDiskCache;
205 private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
206 private static final String DISK_CACHE_SUBDIR = "thumbnails";
207 
208 &#64;Override
209 protected void onCreate(Bundle savedInstanceState) {
210     ...
211     // Initialize memory cache
212     ...
213     File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
214     mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
215     ...
216 }
217 
218 class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
219     ...
220     // Decode image in background.
221     &#64;Override
222     protected Bitmap doInBackground(Integer... params) {
223         final String imageKey = String.valueOf(params[0]);
224 
225         // Check disk cache in background thread
226         Bitmap bitmap = getBitmapFromDiskCache(imageKey);
227 
228         if (bitmap == null) { // Not found in disk cache
229             // Process as normal
230             final Bitmap bitmap = decodeSampledBitmapFromResource(
231                     getResources(), params[0], 100, 100));
232         }
233 
234         // Add final bitmap to caches
235         addBitmapToCache(String.valueOf(imageKey, bitmap);
236 
237         return bitmap;
238     }
239     ...
240 }
241 
242 public void addBitmapToCache(String key, Bitmap bitmap) {
243     // Add to memory cache as before
244     if (getBitmapFromMemCache(key) == null) {
245         mMemoryCache.put(key, bitmap);
246     }
247 
248     // Also add to disk cache
249     if (!mDiskCache.containsKey(key)) {
250         mDiskCache.put(key, bitmap);
251     }
252 }
253 
254 public Bitmap getBitmapFromDiskCache(String key) {
255     return mDiskCache.get(key);
256 }
257 
258 // Creates a unique subdirectory of the designated app cache directory. Tries to use external
259 // but if not mounted, falls back on internal storage.
260 public static File getCacheDir(Context context, String uniqueName) {
261     // Check if media is mounted or storage is built-in, if so, try and use external cache dir
262     // otherwise use internal cache dir
263     final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
264             || !Environment.isExternalStorageRemovable() ?
265                     context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
266 
267     return new File(cachePath + File.separator + uniqueName);
268 }
269 </pre>
270 
271 <p>While the memory cache is checked in the UI thread, the disk cache is checked in the background
272 thread. Disk operations should never take place on the UI thread. When image processing is
273 complete, the final bitmap is added to both the memory and disk cache for future use.</p>
274 
275 <h2 id="config-changes">Handle Configuration Changes</h2>
276 
277 <p>Runtime configuration changes, such as a screen orientation change, cause Android to destroy and
278 restart the running activity with the new configuration (For more information about this behavior,
279 see <a href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a>).
280 You want to avoid having to process all your images again so the user has a smooth and fast
281 experience when a configuration change occurs.</p>
282 
283 <p>Luckily, you have a nice memory cache of bitmaps that you built in the <a
284 href="#memory-cache">Use a Memory Cache</a> section. This cache can be passed through to the new
285 activity instance using a {@link android.app.Fragment} which is preserved by calling {@link
286 android.app.Fragment#setRetainInstance setRetainInstance(true)}). After the activity has been
287 recreated, this retained {@link android.app.Fragment} is reattached and you gain access to the
288 existing cache object, allowing images to be quickly fetched and re-populated into the {@link
289 android.widget.ImageView} objects.</p>
290 
291 <p>Here’s an example of retaining a {@link android.util.LruCache} object across configuration
292 changes using a {@link android.app.Fragment}:</p>
293 
294 <pre>
295 private LruCache<String, Bitmap> mMemoryCache;
296 
297 &#64;Override
298 protected void onCreate(Bundle savedInstanceState) {
299     ...
300     RetainFragment mRetainFragment =
301             RetainFragment.findOrCreateRetainFragment(getFragmentManager());
302     mMemoryCache = RetainFragment.mRetainedCache;
303     if (mMemoryCache == null) {
304         mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
305             ... // Initialize cache here as usual
306         }
307         mRetainFragment.mRetainedCache = mMemoryCache;
308     }
309     ...
310 }
311 
312 class RetainFragment extends Fragment {
313     private static final String TAG = "RetainFragment";
314     public LruCache<String, Bitmap> mRetainedCache;
315 
316     public RetainFragment() {}
317 
318     public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
319         RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
320         if (fragment == null) {
321             fragment = new RetainFragment();
322         }
323         return fragment;
324     }
325 
326     &#64;Override
327     public void onCreate(Bundle savedInstanceState) {
328         super.onCreate(savedInstanceState);
329         <strong>setRetainInstance(true);</strong>
330     }
331 }
332 </pre>
333 
334 <p>To test this out, try rotating a device both with and without retaining the {@link
335 android.app.Fragment}. You should notice little to no lag as the images populate the activity almost
336 instantly from memory when you retain the cache. Any images not found in the memory cache are
337 hopefully available in the disk cache, if not, they are processed as usual.</p>
338