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<String, Bitmap> mMemoryCache; 96 97@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<String, Bitmap>(cacheSize) { 109 @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<Integer, Void, Bitmap> { 160 ... 161 // Decode image in background. 162 @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@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<File, Void, Void> { 213 @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<Integer, Void, Bitmap> { 226 ... 227 // Decode image in background. 228 @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<String, Bitmap> mMemoryCache; 323 324@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<String, Bitmap>(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<String, Bitmap> 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 @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