Skip to content

Commit 153f8fe

Browse files
committed
docs: Android U: Displaying Bitmaps Efficiently
Change-Id: I749f6dd82438fc0902b892e9b918243fc0a826d3
1 parent e07b585 commit 153f8fe

File tree

7 files changed

+1244
-0
lines changed

7 files changed

+1244
-0
lines changed

docs/html/resources/resources_toc.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,31 @@
299299
</li>
300300
</ul>
301301
</li>
302+
303+
<li class="toggle-list">
304+
<div><a href="<?cs var:toroot ?>training/displaying-bitmaps/index.html">
305+
<span class="en">Displaying Bitmaps Efficiently<span class="new">&nbsp;new!</span></span>
306+
</a>
307+
</div>
308+
<ul>
309+
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/load-bitmap.html">
310+
<span class="en">Loading Large Bitmaps Efficiently</span>
311+
</a>
312+
</li>
313+
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/process-bitmap.html">
314+
<span class="en">Processing Bitmaps Off the UI Thread</span>
315+
</a>
316+
</li>
317+
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/cache-bitmap.html">
318+
<span class="en">Caching Bitmaps</span>
319+
</a>
320+
</li>
321+
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/display-bitmap.html">
322+
<span class="en">Displaying Bitmaps in Your UI</span>
323+
</a>
324+
</li>
325+
</ul>
326+
</li>
302327

303328
<li class="toggle-list">
304329
<div><a href="<?cs var:toroot ?>training/accessibility/index.html">
308 KB
Binary file not shown.
Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
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+
&#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>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+
&#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>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+
&#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>

0 commit comments

Comments
 (0)