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<String, Bitmap> mMemoryCache; 100 101@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 @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 @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@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<File, Void, Void> { 216 @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<Integer, Void, Bitmap> { 229 ... 230 // Decode image in background. 231 @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<String, Bitmap> mMemoryCache; 326 327@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<String, Bitmap>(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<String, Bitmap> 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 @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