blob: 94abe2186561bf1d8dc231b05e0f5419aeb5eadb [file] [log] [blame]
Scott Main153f8fe2012-04-04 17:45:24 -07001page.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<String, Bitmap> 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<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
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<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
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 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
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>Included in the sample code of this class is a basic {@code DiskLruCache} implementation.
194However, a more robust and recommended {@code DiskLruCache} solution is included in the Android 4.0
195source code ({@code libcore/luni/src/main/java/libcore/io/DiskLruCache.java}). Back-porting this
196class for use on previous Android releases should be fairly straightforward (a <a
197href="http://www.google.com/search?q=disklrucache">quick search</a> shows others who have already
198implemented this solution).</p>
199
200<p>Here’s updated example code that uses the simple {@code DiskLruCache} included in the sample
201application of this class:</p>
202
203<pre>
204private DiskLruCache mDiskCache;
205private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
206private static final String DISK_CACHE_SUBDIR = "thumbnails";
207
208&#64;Override
209protected 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
218class 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
242public 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
254public 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.
260public 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
272thread. Disk operations should never take place on the UI thread. When image processing is
273complete, 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
278restart the running activity with the new configuration (For more information about this behavior,
279see <a href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a>).
280You want to avoid having to process all your images again so the user has a smooth and fast
281experience when a configuration change occurs.</p>
282
283<p>Luckily, you have a nice memory cache of bitmaps that you built in the <a
284href="#memory-cache">Use a Memory Cache</a> section. This cache can be passed through to the new
285activity instance using a {@link android.app.Fragment} which is preserved by calling {@link
286android.app.Fragment#setRetainInstance setRetainInstance(true)}). After the activity has been
287recreated, this retained {@link android.app.Fragment} is reattached and you gain access to the
288existing cache object, allowing images to be quickly fetched and re-populated into the {@link
289android.widget.ImageView} objects.</p>
290
291<p>Here’s an example of retaining a {@link android.util.LruCache} object across configuration
292changes using a {@link android.app.Fragment}:</p>
293
294<pre>
295private LruCache<String, Bitmap> mMemoryCache;
296
297&#64;Override
298protected 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
312class 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
335android.app.Fragment}. You should notice little to no lag as the images populate the activity almost
336instantly from memory when you retain the cache. Any images not found in the memory cache are
337hopefully available in the disk cache, if not, they are processed as usual.</p>