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 
      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}shareables/training/BitmapFun.zip" class="button">Download the sample</a>
     28   <p class="filename">BitmapFun.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
     35 complicated if you need to load a larger set of images at once. In many cases (such as with
     36 components like {@link android.widget.ListView}, {@link android.widget.GridView} or {@link
     37 android.support.v4.view.ViewPager }), the total number of images on-screen combined with images that
     38 might 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
     41 off-screen. The garbage collector also frees up your loaded bitmaps, assuming you don't keep any
     42 long lived references. This is all good and well, but in order to keep a fluid and fast-loading UI
     43 you want to avoid continually processing these images each time they come back on-screen. A memory
     44 and 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
     47 and 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
     52 memory. The {@link android.util.LruCache} class (also available in the <a
     53 href="{@docRoot}reference/android/support/v4/util/LruCache.html">Support Library</a> for use back
     54 to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently
     55 referenced objects in a strong referenced {@link java.util.LinkedHashMap} and evicting the least
     56 recently 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
     60 this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more
     61 aggressive with collecting soft/weak references which makes them fairly ineffective. In addition,
     62 prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which
     63 is not released in a predictable manner, potentially causing an application to briefly exceed its
     64 memory limits and crash.</p>
     65 
     66 <p>In order to choose a suitable size for a {@link android.util.LruCache}, a number of factors
     67 should 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
     88 usage and come up with a suitable solution. A cache that is too small causes additional overhead with
     89 no benefit, a cache that is too large can once again cause {@code java.lang.OutOfMemory} exceptions
     90 and leave the rest of your app little memory to work with.</p>
     91 
     92 <p>Heres an example of setting up a {@link android.util.LruCache} for bitmaps:</p>
     93 
     94 <pre>
     95 private LruCache&lt;String, Bitmap&gt; mMemoryCache;
     96 
     97 &#64;Override
     98 protected 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&lt;String, Bitmap&gt;(cacheSize) {
    109         &#64;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 
    119 public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    120     if (getBitmapFromMemCache(key) == null) {
    121         mMemoryCache.put(key, bitmap);
    122     }
    123 }
    124 
    125 public 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
    131 allocated for our cache. On a normal/hdpi device this is a minimum of around 4MB (32/8). A full
    132 screen {@link android.widget.GridView} filled with images on a device with 800x480 resolution would
    133 use around 1.5MB (800*480*4 bytes), so this would cache a minimum of around 2.5 pages of images in
    134 memory.</p>
    135 
    136 <p>When loading a bitmap into an {@link android.widget.ImageView}, the {@link android.util.LruCache}
    137 is checked first. If an entry is found, it is used immediately to update the {@link
    138 android.widget.ImageView}, otherwise a background thread is spawned to process the image:</p>
    139 
    140 <pre>
    141 public 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
    156 updated to add entries to the memory cache:</p>
    157 
    158 <pre>
    159 class BitmapWorkerTask extends AsyncTask&lt;Integer, Void, Bitmap&gt; {
    160     ...
    161     // Decode image in background.
    162     &#64;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
    176 rely on images being available in this cache. Components like {@link android.widget.GridView} with
    177 larger datasets can easily fill up a memory cache. Your application could be interrupted by another
    178 task like a phone call, and while in the background it might be killed and the memory cache
    179 destroyed. 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
    182 times where images are no longer available in a memory cache. Of course, fetching images from disk
    183 is slower than loading from memory and should be done in a background thread, as disk read times can
    184 be unpredictable.</p>
    185 
    186 <p class="note"><strong>Note:</strong> A {@link android.content.ContentProvider} might be a more
    187 appropriate place to store cached images if they are accessed more frequently, for example in an
    188 image 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/+/master/luni/src/main/java/libcore/io/DiskLruCache.java">Android source</a>. Heres updated example code that adds a disk cache in addition
    192 to the existing memory cache:</p>
    193 
    194 <pre>
    195 private DiskLruCache mDiskLruCache;
    196 private final Object mDiskCacheLock = new Object();
    197 private boolean mDiskCacheStarting = true;
    198 private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
    199 private static final String DISK_CACHE_SUBDIR = "thumbnails";
    200 
    201 &#64;Override
    202 protected 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 
    212 class InitDiskCacheTask extends AsyncTask&lt;File, Void, Void&gt; {
    213     &#64;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 
    225 class BitmapWorkerTask extends AsyncTask&lt;Integer, Void, Bitmap&gt; {
    226     ...
    227     // Decode image in background.
    228     &#64;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 
    249 public 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 
    263 public 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.
    280 public 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
    293 and therefore should not take place on the main thread. However, this does mean there's a chance
    294 the cache is accessed before initialization. To address this, in the above implementation, a lock
    295 object ensures that the app does not read from the disk cache until the cache has been
    296 initialized.</p>
    297 
    298 <p>While the memory cache is checked in the UI thread, the disk cache is checked in the background
    299 thread. Disk operations should never take place on the UI thread. When image processing is
    300 complete, 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
    305 restart the running activity with the new configuration (For more information about this behavior,
    306 see <a href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a>).
    307 You want to avoid having to process all your images again so the user has a smooth and fast
    308 experience when a configuration change occurs.</p>
    309 
    310 <p>Luckily, you have a nice memory cache of bitmaps that you built in the <a
    311 href="#memory-cache">Use a Memory Cache</a> section. This cache can be passed through to the new
    312 activity instance using a {@link android.app.Fragment} which is preserved by calling {@link
    313 android.app.Fragment#setRetainInstance setRetainInstance(true)}). After the activity has been
    314 recreated, this retained {@link android.app.Fragment} is reattached and you gain access to the
    315 existing cache object, allowing images to be quickly fetched and re-populated into the {@link
    316 android.widget.ImageView} objects.</p>
    317 
    318 <p>Heres an example of retaining a {@link android.util.LruCache} object across configuration
    319 changes using a {@link android.app.Fragment}:</p>
    320 
    321 <pre>
    322 private LruCache&lt;String, Bitmap&gt; mMemoryCache;
    323 
    324 &#64;Override
    325 protected void onCreate(Bundle savedInstanceState) {
    326     ...
    327     RetainFragment retainFragment =
    328             RetainFragment.findOrCreateRetainFragment(getFragmentManager());
    329     mMemoryCache = retainFragment.mRetainedCache;
    330     if (mMemoryCache == null) {
    331         mMemoryCache = new LruCache&lt;String, Bitmap&gt;(cacheSize) {
    332             ... // Initialize cache here as usual
    333         }
    334         retainFragment.mRetainedCache = mMemoryCache;
    335     }
    336     ...
    337 }
    338 
    339 class RetainFragment extends Fragment {
    340     private static final String TAG = "RetainFragment";
    341     public LruCache&lt;String, Bitmap&gt; 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         }
    350         return fragment;
    351     }
    352 
    353     &#64;Override
    354     public void onCreate(Bundle savedInstanceState) {
    355         super.onCreate(savedInstanceState);
    356         <strong>setRetainInstance(true);</strong>
    357     }
    358 }
    359 </pre>
    360 
    361 <p>To test this out, try rotating a device both with and without retaining the {@link
    362 android.app.Fragment}. You should notice little to no lag as the images populate the activity almost
    363 instantly from memory when you retain the cache. Any images not found in the memory cache are
    364 hopefully available in the disk cache, if not, they are processed as usual.</p>
    365