Home | History | Annotate | Download | only in displaying-bitmaps
      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>Heres 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>Heres 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>Heres 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