|
| 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 | +@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 | + @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 | + @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 | +@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 | + @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 | +@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 | + @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> |
0 commit comments