docs: Android U: Displaying Bitmaps Efficiently

Change-Id: I749f6dd82438fc0902b892e9b918243fc0a826d3
This commit is contained in:
Scott Main
2012-04-04 17:45:24 -07:00
parent e07b585065
commit 153f8fe420
7 changed files with 1244 additions and 0 deletions

View File

@@ -299,6 +299,31 @@ class="new">&nbsp;new!</span></span>
</li>
</ul>
</li>
<li class="toggle-list">
<div><a href="<?cs var:toroot ?>training/displaying-bitmaps/index.html">
<span class="en">Displaying Bitmaps Efficiently<span class="new">&nbsp;new!</span></span>
</a>
</div>
<ul>
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/load-bitmap.html">
<span class="en">Loading Large Bitmaps Efficiently</span>
</a>
</li>
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/process-bitmap.html">
<span class="en">Processing Bitmaps Off the UI Thread</span>
</a>
</li>
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/cache-bitmap.html">
<span class="en">Caching Bitmaps</span>
</a>
</li>
<li><a href="<?cs var:toroot ?>training/displaying-bitmaps/display-bitmap.html">
<span class="en">Displaying Bitmaps in Your UI</span>
</a>
</li>
</ul>
</li>
<li class="toggle-list">
<div><a href="<?cs var:toroot ?>training/accessibility/index.html">

Binary file not shown.

View File

@@ -0,0 +1,337 @@
page.title=Caching Bitmaps
parent.title=Displaying Bitmaps Efficiently
parent.link=index.html
trainingnavtop=true
next.title=Displaying Bitmaps in Your UI
next.link=display-bitmap.html
previous.title=Processing Bitmaps Off the UI Thread
previous.link=process-bitmap.html
@jd:body
<div id="tb-wrapper">
<div id="tb">
<h2>This lesson teaches you to</h2>
<ol>
<li><a href="#memory-cache">Use a Memory Cache</a></li>
<li><a href="#disk-cache">Use a Disk Cache</a></li>
<li><a href="#config-changes">Handle Configuration Changes</a></li>
</ol>
<h2>You should also read</h2>
<ul>
<li><a href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a></li>
</ul>
<h2>Try it out</h2>
<div class="download-box">
<a href="{@docRoot}shareables/training/BitmapFun.zip" class="button">Download the sample</a>
<p class="filename">BitmapFun.zip</p>
</div>
</div>
</div>
<p>Loading a single bitmap into your user interface (UI) is straightforward, however things get more
complicated if you need to load a larger set of images at once. In many cases (such as with
components like {@link android.widget.ListView}, {@link android.widget.GridView} or {@link
android.support.v4.view.ViewPager }), the total number of images on-screen combined with images that
might soon scroll onto the screen are essentially unlimited.</p>
<p>Memory usage is kept down with components like this by recycling the child views as they move
off-screen. The garbage collector also frees up your loaded bitmaps, assuming you don't keep any
long lived references. This is all good and well, but in order to keep a fluid and fast-loading UI
you want to avoid continually processing these images each time they come back on-screen. A memory
and disk cache can often help here, allowing components to quickly reload processed images.</p>
<p>This lesson walks you through using a memory and disk bitmap cache to improve the responsiveness
and fluidity of your UI when loading multiple bitmaps.</p>
<h2 id="memory-cache">Use a Memory Cache</h2>
<p>A memory cache offers fast access to bitmaps at the cost of taking up valuable application
memory. The {@link android.util.LruCache} class (also available in the <a
href="{@docRoot}reference/android/support/v4/util/LruCache.html">Support Library</a> for use back
to API Level 4) is particularly well suited to the task of caching bitmaps, keeping recently
referenced objects in a strong referenced {@link java.util.LinkedHashMap} and evicting the least
recently used member before the cache exceeds its designated size.</p>
<p class="note"><strong>Note:</strong> In the past, a popular memory cache implementation was a
{@link java.lang.ref.SoftReference} or {@link java.lang.ref.WeakReference} bitmap cache, however
this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more
aggressive with collecting soft/weak references which makes them fairly ineffective. In addition,
prior to Android 3.0 (API Level 11), the backing data of a bitmap was stored in native memory which
is not released in a predictable manner, potentially causing an application to briefly exceed its
memory limits and crash.</p>
<p>In order to choose a suitable size for a {@link android.util.LruCache}, a number of factors
should be taken into consideration, for example:</p>
<ul>
<li>How memory intensive is the rest of your activity and/or application?</li>
<li>How many images will be on-screen at once? How many need to be available ready to come
on-screen?</li>
<li>What is the screen size and density of the device? An extra high density screen (xhdpi) device
like <a href="http://www.android.com/devices/detail/galaxy-nexus">Galaxy Nexus</a> will need a
larger cache to hold the same number of images in memory compared to a device like <a
href="http://www.android.com/devices/detail/nexus-s">Nexus S</a> (hdpi).</li>
<li>What dimensions and configuration are the bitmaps and therefore how much memory will each take
up?</li>
<li>How frequently will the images be accessed? Will some be accessed more frequently than others?
If so, perhaps you may want to keep certain items always in memory or even have multiple {@link
android.util.LruCache} objects for different groups of bitmaps.</li>
<li>Can you balance quality against quantity? Sometimes it can be more useful to store a larger
number of lower quality bitmaps, potentially loading a higher quality version in another
background task.</li>
</ul>
<p>There is no specific size or formula that suits all applications, it's up to you to analyze your
usage and come up with a suitable solution. A cache that is too small causes additional overhead with
no benefit, a cache that is too large can once again cause {@code java.lang.OutOfMemory} exceptions
and leave the rest of your app little memory to work with.</p>
<p>Heres an example of setting up a {@link android.util.LruCache} for bitmaps:</p>
<pre>
private LruCache<String, Bitmap> mMemoryCache;
&#64;Override
protected void onCreate(Bundle savedInstanceState) {
...
// Get memory class of this device, exceeding this amount will throw an
// OutOfMemory exception.
final int memClass = ((ActivityManager) context.getSystemService(
Context.ACTIVITY_SERVICE)).getMemoryClass();
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = 1024 * 1024 * memClass / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
&#64;Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in bytes rather than number of items.
return bitmap.getByteCount();
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
</pre>
<p class="note"><strong>Note:</strong> In this example, one eighth of the application memory is
allocated for our cache. On a normal/hdpi device this is a minimum of around 4MB (32/8). A full
screen {@link android.widget.GridView} filled with images on a device with 800x480 resolution would
use around 1.5MB (800*480*4 bytes), so this would cache a minimum of around 2.5 pages of images in
memory.</p>
<p>When loading a bitmap into an {@link android.widget.ImageView}, the {@link android.util.LruCache}
is checked first. If an entry is found, it is used immediately to update the {@link
android.widget.ImageView}, otherwise a background thread is spawned to process the image:</p>
<pre>
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
</pre>
<p>The <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> also needs to be
updated to add entries to the memory cache:</p>
<pre>
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
&#64;Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
</pre>
<h2 id="disk-cache">Use a Disk Cache</h2>
<p>A memory cache is useful in speeding up access to recently viewed bitmaps, however you cannot
rely on images being available in this cache. Components like {@link android.widget.GridView} with
larger datasets can easily fill up a memory cache. Your application could be interrupted by another
task like a phone call, and while in the background it might be killed and the memory cache
destroyed. Once the user resumes, your application it has to process each image again.</p>
<p>A disk cache can be used in these cases to persist processed bitmaps and help decrease loading
times where images are no longer available in a memory cache. Of course, fetching images from disk
is slower than loading from memory and should be done in a background thread, as disk read times can
be unpredictable.</p>
<p class="note"><strong>Note:</strong> A {@link android.content.ContentProvider} might be a more
appropriate place to store cached images if they are accessed more frequently, for example in an
image gallery application.</p>
<p>Included in the sample code of this class is a basic {@code DiskLruCache} implementation.
However, a more robust and recommended {@code DiskLruCache} solution is included in the Android 4.0
source code ({@code libcore/luni/src/main/java/libcore/io/DiskLruCache.java}). Back-porting this
class for use on previous Android releases should be fairly straightforward (a <a
href="http://www.google.com/search?q=disklrucache">quick search</a> shows others who have already
implemented this solution).</p>
<p>Heres updated example code that uses the simple {@code DiskLruCache} included in the sample
application of this class:</p>
<pre>
private DiskLruCache mDiskCache;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails";
&#64;Override
protected void onCreate(Bundle savedInstanceState) {
...
// Initialize memory cache
...
File cacheDir = getCacheDir(this, DISK_CACHE_SUBDIR);
mDiskCache = DiskLruCache.openCache(this, cacheDir, DISK_CACHE_SIZE);
...
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
&#64;Override
protected Bitmap doInBackground(Integer... params) {
final String imageKey = String.valueOf(params[0]);
// Check disk cache in background thread
Bitmap bitmap = getBitmapFromDiskCache(imageKey);
if (bitmap == null) { // Not found in disk cache
// Process as normal
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
}
// Add final bitmap to caches
addBitmapToCache(String.valueOf(imageKey, bitmap);
return bitmap;
}
...
}
public void addBitmapToCache(String key, Bitmap bitmap) {
// Add to memory cache as before
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
// Also add to disk cache
if (!mDiskCache.containsKey(key)) {
mDiskCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromDiskCache(String key) {
return mDiskCache.get(key);
}
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
public static File getCacheDir(Context context, String uniqueName) {
// Check if media is mounted or storage is built-in, if so, try and use external cache dir
// otherwise use internal cache dir
final String cachePath = Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
|| !Environment.isExternalStorageRemovable() ?
context.getExternalCacheDir().getPath() : context.getCacheDir().getPath();
return new File(cachePath + File.separator + uniqueName);
}
</pre>
<p>While the memory cache is checked in the UI thread, the disk cache is checked in the background
thread. Disk operations should never take place on the UI thread. When image processing is
complete, the final bitmap is added to both the memory and disk cache for future use.</p>
<h2 id="config-changes">Handle Configuration Changes</h2>
<p>Runtime configuration changes, such as a screen orientation change, cause Android to destroy and
restart the running activity with the new configuration (For more information about this behavior,
see <a href="{@docRoot}guide/topics/resources/runtime-changes.html">Handling Runtime Changes</a>).
You want to avoid having to process all your images again so the user has a smooth and fast
experience when a configuration change occurs.</p>
<p>Luckily, you have a nice memory cache of bitmaps that you built in the <a
href="#memory-cache">Use a Memory Cache</a> section. This cache can be passed through to the new
activity instance using a {@link android.app.Fragment} which is preserved by calling {@link
android.app.Fragment#setRetainInstance setRetainInstance(true)}). After the activity has been
recreated, this retained {@link android.app.Fragment} is reattached and you gain access to the
existing cache object, allowing images to be quickly fetched and re-populated into the {@link
android.widget.ImageView} objects.</p>
<p>Heres an example of retaining a {@link android.util.LruCache} object across configuration
changes using a {@link android.app.Fragment}:</p>
<pre>
private LruCache<String, Bitmap> mMemoryCache;
&#64;Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment mRetainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = RetainFragment.mRetainedCache;
if (mMemoryCache == null) {
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
... // Initialize cache here as usual
}
mRetainFragment.mRetainedCache = mMemoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache<String, Bitmap> mRetainedCache;
public RetainFragment() {}
public static RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if (fragment == null) {
fragment = new RetainFragment();
}
return fragment;
}
&#64;Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
<strong>setRetainInstance(true);</strong>
}
}
</pre>
<p>To test this out, try rotating a device both with and without retaining the {@link
android.app.Fragment}. You should notice little to no lag as the images populate the activity almost
instantly from memory when you retain the cache. Any images not found in the memory cache are
hopefully available in the disk cache, if not, they are processed as usual.</p>

View File

@@ -0,0 +1,400 @@
page.title=Displaying Bitmaps in Your UI
parent.title=Displaying Bitmaps Efficiently
parent.link=index.html
trainingnavtop=true
previous.title=Caching Bitmaps
previous.link=cache-bitmap.html
@jd:body
<div id="tb-wrapper">
<div id="tb">
<h2>This lesson teaches you to</h2>
<ol>
<li><a href="#viewpager">Load Bitmaps into a ViewPager Implementation</a></li>
<li><a href="#gridview">Load Bitmaps into a GridView Implementation</a></li>
</ol>
<h2>You should also read</h2>
<ul>
<li><a href="{@docRoot}design/patterns/swipe-views.html">Android Design: Swipe Views</a></li>
<li><a href="{@docRoot}design/building-blocks/grid-lists.html">Android Design: Grid Lists</a></li>
</ul>
<h2>Try it out</h2>
<div class="download-box">
<a href="{@docRoot}shareables/training/BitmapFun.zip" class="button">Download the sample</a>
<p class="filename">BitmapFun.zip</p>
</div>
</div>
</div>
<p></p>
<p>This lesson brings together everything from previous lessons, showing you how to load multiple
bitmaps into {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView}
components using a background thread and bitmap cache, while dealing with concurrency and
configuration changes.</p>
<h2 id="viewpager">Load Bitmaps into a ViewPager Implementation</h2>
<p>The <a href="{@docRoot}design/patterns/swipe-views.html">swipe view pattern</a> is an excellent
way to navigate the detail view of an image gallery. You can implement this pattern using a {@link
android.support.v4.view.ViewPager} component backed by a {@link
android.support.v4.view.PagerAdapter}. However, a more suitable backing adapter is the subclass
{@link android.support.v4.app.FragmentStatePagerAdapter} which automatically destroys and saves
state of the {@link android.app.Fragment Fragments} in the {@link android.support.v4.view.ViewPager}
as they disappear off-screen, keeping memory usage down.</p>
<p class="note"><strong>Note:</strong> If you have a smaller number of images and are confident they
all fit within the application memory limit, then using a regular {@link
android.support.v4.view.PagerAdapter} or {@link android.support.v4.app.FragmentPagerAdapter} might
be more appropriate.</p>
<p>Heres an implementation of a {@link android.support.v4.view.ViewPager} with {@link
android.widget.ImageView} children. The main activity holds the {@link
android.support.v4.view.ViewPager} and the adapter:</p>
<pre>
public class ImageDetailActivity extends FragmentActivity {
public static final String EXTRA_IMAGE = "extra_image";
private ImagePagerAdapter mAdapter;
private ViewPager mPager;
// A static dataset to back the ViewPager adapter
public final static Integer[] imageResIds = new Integer[] {
R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
&#64;Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.image_detail_pager); // Contains just a ViewPager
mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
mPager = (ViewPager) findViewById(R.id.pager);
mPager.setAdapter(mAdapter);
}
public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
private final int mSize;
public ImagePagerAdapter(FragmentManager fm, int size) {
super(fm);
mSize = size;
}
&#64;Override
public int getCount() {
return mSize;
}
&#64;Override
public Fragment getItem(int position) {
return ImageDetailFragment.newInstance(position);
}
}
}
</pre>
<p>The details {@link android.app.Fragment} holds the {@link android.widget.ImageView} children:</p>
<pre>
public class ImageDetailFragment extends Fragment {
private static final String IMAGE_DATA_EXTRA = "resId";
private int mImageNum;
private ImageView mImageView;
static ImageDetailFragment newInstance(int imageNum) {
final ImageDetailFragment f = new ImageDetailFragment();
final Bundle args = new Bundle();
args.putInt(IMAGE_DATA_EXTRA, imageNum);
f.setArguments(args);
return f;
}
// Empty constructor, required as per Fragment docs
public ImageDetailFragment() {}
&#64;Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
}
&#64;Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// image_detail_fragment.xml contains just an ImageView
final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
mImageView = (ImageView) v.findViewById(R.id.imageView);
return v;
}
&#64;Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
final int resId = ImageDetailActivity.imageResIds[mImageNum];
<strong>mImageView.setImageResource(resId);</strong> // Load image into ImageView
}
}
</pre>
<p>Hopefully you noticed the issue with this implementation; The images are being read from
resources on the UI thread which can lead to an application hanging and being force closed. Using an
{@link android.os.AsyncTask} as described in the <a href="process-bitmap.html">Processing Bitmaps Off
the UI Thread</a> lesson, its straightforward to move image loading and processing to a background
thread:</p>
<pre>
public class ImageDetailActivity extends FragmentActivity {
...
public void loadBitmap(int resId, ImageView imageView) {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
... // include <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> class
}
public class ImageDetailFragment extends Fragment {
...
&#64;Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
if (ImageDetailActivity.class.isInstance(getActivity())) {
final int resId = ImageDetailActivity.imageResIds[mImageNum];
// Call out to ImageDetailActivity to load the bitmap in a background thread
((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
}
}
}
</pre>
<p>Any additional processing (such as resizing or fetching images from the network) can take place
in the <a href="process-bitmap.html#BitmapWorkerTask">{@code BitmapWorkerTask}</a> without affecting
responsiveness of the main UI. If the background thread is doing more than just loading an image
directly from disk, it can also be beneficial to add a memory and/or disk cache as described in the
lesson <a href="cache-bitmap.html#memory-cache">Caching Bitmaps</a>. Here's the additional
modifications for a memory cache:</p>
<pre>
public class ImageDetailActivity extends FragmentActivity {
...
private LruCache<String, Bitmap> mMemoryCache;
&#64;Override
public void onCreate(Bundle savedInstanceState) {
...
// initialize LruCache as per <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> section
}
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = mMemoryCache.get(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
... // include updated BitmapWorkerTask from <a href="cache-bitmap.html#memory-cache">Use a Memory Cache</a> section
}
</pre>
<p>Putting all these pieces together gives you a responsive {@link
android.support.v4.view.ViewPager} implementation with minimal image loading latency and the ability
to do as much or as little background processing on your images as needed.</p>
<h2 id="gridview">Load Bitmaps into a GridView Implementation</h2>
<p>The <a href="{@docRoot}design/building-blocks/grid-lists.html">grid list building block</a> is
useful for showing image data sets and can be implemented using a {@link android.widget.GridView}
component in which many images can be on-screen at any one time and many more need to be ready to
appear if the user scrolls up or down. When implementing this type of control, you must ensure the
UI remains fluid, memory usage remains under control and concurrency is handled correctly (due to
the way {@link android.widget.GridView} recycles its children views).</p>
<p>To start with, here is a standard {@link android.widget.GridView} implementation with {@link
android.widget.ImageView} children placed inside a {@link android.app.Fragment}:</p>
<pre>
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
private ImageAdapter mAdapter;
// A static dataset to back the GridView adapter
public final static Integer[] imageResIds = new Integer[] {
R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
// Empty constructor as per Fragment docs
public ImageGridFragment() {}
&#64;Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mAdapter = new ImageAdapter(getActivity());
}
&#64;Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
mGridView.setAdapter(mAdapter);
mGridView.setOnItemClickListener(this);
return v;
}
&#64;Override
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
startActivity(i);
}
private class ImageAdapter extends BaseAdapter {
private final Context mContext;
public ImageAdapter(Context context) {
super();
mContext = context;
}
&#64;Override
public int getCount() {
return imageResIds.length;
}
&#64;Override
public Object getItem(int position) {
return imageResIds[position];
}
&#64;Override
public long getItemId(int position) {
return position;
}
&#64;Override
public View getView(int position, View convertView, ViewGroup container) {
ImageView imageView;
if (convertView == null) { // if it's not recycled, initialize some attributes
imageView = new ImageView(mContext);
imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
imageView.setLayoutParams(new GridView.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
} else {
imageView = (ImageView) convertView;
}
<strong>imageView.setImageResource(imageResIds[position]);</strong> // Load image into ImageView
return imageView;
}
}
}
</pre>
<p>Once again, the problem with this implementation is that the image is being set in the UI thread.
While this may work for small, simple images (due to system resource loading and caching), if any
additional processing needs to be done, your UI grinds to a halt.</p>
<p>The same asynchronous processing and caching methods from the previous section can be implemented
here. However, you also need to wary of concurrency issues as the {@link android.widget.GridView}
recycles its children views. To handle this, use the techniques discussed in the <a
href="process-bitmap#concurrency">Processing Bitmaps Off the UI Thread</a> lesson. Here is the updated
solution:</p>
<pre>
public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
...
private class ImageAdapter extends BaseAdapter {
...
&#64;Override
public View getView(int position, View convertView, ViewGroup container) {
...
<strong>loadBitmap(imageResIds[position], imageView)</strong>
return imageView;
}
}
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
... // include updated <a href="process-bitmap.html#BitmapWorkerTaskUpdated">{@code BitmapWorkerTask}</a> class
</pre>
<p class="note"><strong>Note:</strong> The same code can easily be adapted to work with {@link
android.widget.ListView} as well.</p>
<p>This implementation allows for flexibility in how the images are processed and loaded without
impeding the smoothness of the UI. In the background task you can load images from the network or
resize large digital camera photos and the images appear as the tasks finish processing.</p>
<p>For a full example of this and other concepts discussed in this lesson, please see the included
sample application.</p>

View File

@@ -0,0 +1,78 @@
page.title=Displaying Bitmaps Efficiently
trainingnavtop=true
startpage=true
next.title=Loading Large Bitmaps Efficiently
next.link=load-bitmap.html
@jd:body
<div id="tb-wrapper">
<div id="tb">
<h2>Dependencies and prerequisites</h2>
<ul>
<li>Android 2.1 (API Level 7) or higher</li>
<li><a href="{@docRoot}sdk/compatibility-library.html">Support Library</a></li>
</ul>
<h2>Try it out</h2>
<div class="download-box">
<a href="{@docRoot}shareables/training/BitmapFun.zip" class="button">Download the sample</a>
<p class="filename">BitmapFun.zip</p>
</div>
</div>
</div>
<p>This class covers some common techniques for processing and loading {@link
android.graphics.Bitmap} objects in a way that keeps your user interface (UI) components responsive
and avoids exceeding your application memory limit. If you're not careful, bitmaps can quickly
consume your available memory budget leading to an application crash due to the dreaded
exception:<br />{@code java.lang.OutofMemoryError: bitmap size exceeds VM budget}.</p>
<p>There are a number of reasons why loading bitmaps in your Android application is tricky:</p>
<ul>
<li>Mobile devices typically have constrained system resources. Android devices can have as little
as 16MB of memory available to a single application. The <a
href="http://source.android.com/compatibility/downloads.html">Android Compatibility Definition
Document</a> (CDD), <i>Section 3.7. Virtual Machine Compatibility</i> gives the required minimum
application memory for various screen sizes and densities. Applications should be optimized to
perform under this minimum memory limit. However, keep in mind many devices are configured with
higher limits.</li>
<li>Bitmaps take up a lot of memory, especially for rich images like photographs. For example, the
camera on the <a href="http://www.google.com/nexus/">Galaxy Nexus</a> takes photos up to 2592x1936
pixels (5 megapixels). If the bitmap configuration used is {@link
android.graphics.Bitmap.Config ARGB_8888} (the default from the Android 2.3 onward) then loading
this image into memory takes about 19MB of memory (2592*1936*4 bytes), immediately exhausting the
per-app limit on some devices.</li>
<li>Android app UIs frequently require several bitmaps to be loaded at once. Components such as
{@link android.widget.ListView}, {@link android.widget.GridView} and {@link
android.support.v4.view.ViewPager} commonly include multiple bitmaps on-screen at once with many
more potentially off-screen ready to show at the flick of a finger.</li>
</ul>
<h2>Lessons</h2>
<dl>
<dt><b><a href="load-bitmap.html">Loading Large Bitmaps Efficiently</a></b></dt>
<dd>This lesson walks you through decoding large bitmaps without exceeding the per application
memory limit.</dd>
<dt><b><a href="process-bitmap.html">Processing Bitmaps Off the UI Thread</a></b></dt>
<dd>Bitmap processing (resizing, downloading from a remote source, etc.) should never take place
on the main UI thread. This lesson walks you through processing bitmaps in a background thread
using {@link android.os.AsyncTask} and explains how to handle concurrency issues.</dd>
<dt><b><a href="cache-bitmap.html">Caching Bitmaps</a></b></dt>
<dd>This lesson walks you through using a memory and disk bitmap cache to improve the
responsiveness and fluidity of your UI when loading multiple bitmaps.</dd>
<dt><b><a href="display-bitmap.html">Displaying Bitmaps in Your UI</a></b></dt>
<dd>This lesson brings everything together, showing you how to load multiple bitmaps into
components like {@link android.support.v4.view.ViewPager} and {@link android.widget.GridView}
using a background thread and bitmap cache.</dd>
</dl>

View File

@@ -0,0 +1,165 @@
page.title=Loading Large Bitmaps Efficiently
parent.title=Displaying Bitmaps Efficiently
parent.link=index.html
trainingnavtop=true
next.title=Processing Bitmaps Off the UI Thread
next.link=process-bitmap.html
@jd:body
<div id="tb-wrapper">
<div id="tb">
<h2>This lesson teaches you to</h2>
<ol>
<li><a href="#read-bitmap">Read Bitmap Dimensions and Type</a></li>
<li><a href="#load-bitmap">Load a Scaled Down Version into Memory</a></li>
</ol>
<h2>Try it out</h2>
<div class="download-box">
<a href="{@docRoot}shareables/training/BitmapFun.zip" class="button">Download the sample</a>
<p class="filename">BitmapFun.zip</p>
</div>
</div>
</div>
<p>Images come in all shapes and sizes. In many cases they are larger than required for a typical
application user interface (UI). For example, the system Gallery application displays photos taken
using your Android devices's camera which are typically much higher resolution than the screen
density of your device.</p>
<p>Given that you are working with limited memory, ideally you only want to load a lower resolution
version in memory. The lower resolution version should match the size of the UI component that
displays it. An image with a higher resolution does not provide any visible benefit, but still takes
up precious memory and incurs additional performance overhead due to additional on the fly
scaling.</p>
<p>This lesson walks you through decoding large bitmaps without exceeding the per application
memory limit by loading a smaller subsampled version in memory.</p>
<h2 id="read-bitmap">Read Bitmap Dimensions and Type</h2>
<p>The {@link android.graphics.BitmapFactory} class provides several decoding methods ({@link
android.graphics.BitmapFactory#decodeByteArray(byte[],int,int,android.graphics.BitmapFactory.Options)
decodeByteArray()}, {@link
android.graphics.BitmapFactory#decodeFile(java.lang.String,android.graphics.BitmapFactory.Options)
decodeFile()}, {@link
android.graphics.BitmapFactory#decodeResource(android.content.res.Resources,int,android.graphics.BitmapFactory.Options)
decodeResource()}, etc.) for creating a {@link android.graphics.Bitmap} from various sources. Choose
the most appropriate decode method based on your image data source. These methods attempt to
allocate memory for the constructed bitmap and therefore can easily result in an {@code OutOfMemory}
exception. Each type of decode method has additional signatures that let you specify decoding
options via the {@link android.graphics.BitmapFactory.Options} class. Setting the {@link
android.graphics.BitmapFactory.Options#inJustDecodeBounds} property to {@code true} while decoding
avoids memory allocation, returning {@code null} for the bitmap object but setting {@link
android.graphics.BitmapFactory.Options#outWidth}, {@link
android.graphics.BitmapFactory.Options#outHeight} and {@link
android.graphics.BitmapFactory.Options#outMimeType}. This technique allows you to read the
dimensions and type of the image data prior to construction (and memory allocation) of the
bitmap.</p>
<pre>
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType;
</pre>
<p>To avoid {@code java.lang.OutOfMemory} exceptions, check the dimensions of a bitmap before
decoding it, unless you absolutely trust the source to provide you with predictably sized image data
that comfortably fits within the available memory.</p>
<h2 id="load-bitmap">Load a Scaled Down Version into Memory</h2>
<p>Now that the image dimensions are known, they can be used to decide if the full image should be
loaded into memory or if a subsampled version should be loaded instead. Here are some factors to
consider:</p>
<ul>
<li>Estimated memory usage of loading the full image in memory.</li>
<li>Amount of memory you are willing to commit to loading this image given any other memory
requirements of your application.</li>
<li>Dimensions of the target {@link android.widget.ImageView} or UI component that the image
is to be loaded into.</li>
<li>Screen size and density of the current device.</li>
</ul>
<p>For example, its not worth loading a 1024x768 pixel image into memory if it will eventually be
displayed in a 128x96 pixel thumbnail in an {@link android.widget.ImageView}.</p>
<p>To tell the decoder to subsample the image, loading a smaller version into memory, set {@link
android.graphics.BitmapFactory.Options#inSampleSize} to {@code true} in your {@link
android.graphics.BitmapFactory.Options} object. For example, an image with resolution 2048x1536 that
is decoded with an {@link android.graphics.BitmapFactory.Options#inSampleSize} of 4 produces a
bitmap of approximately 512x384. Loading this into memory uses 0.75MB rather than 12MB for the full
image (assuming a bitmap configuration of {@link android.graphics.Bitmap.Config ARGB_8888}). Heres
a method to calculate a the sample size value based on a target width and height:</p>
<pre>
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
if (width > height) {
inSampleSize = Math.round((float)height / (float)reqHeight);
} else {
inSampleSize = Math.round((float)width / (float)reqWidth);
}
}
return inSampleSize;
}
</pre>
<p class="note"><strong>Note:</strong> Using powers of 2 for {@link
android.graphics.BitmapFactory.Options#inSampleSize} values is faster and more efficient for the
decoder. However, if you plan to cache the resized versions in memory or on disk, its usually still
worth decoding to the most appropriate image dimensions to save space.</p>
<p>To use this method, first decode with {@link
android.graphics.BitmapFactory.Options#inJustDecodeBounds} set to {@code true}, pass the options
through and then decode again using the new {@link
android.graphics.BitmapFactory.Options#inSampleSize} value and {@link
android.graphics.BitmapFactory.Options#inJustDecodeBounds} set to {@code false}:</p>
<a name="decodeSampledBitmapFromResource"></a>
<pre>
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// First decode with inJustDecodeBounds=true to check dimensions
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
</pre>
<p>This method makes it easy to load a bitmap of arbitrarily large size into an {@link
android.widget.ImageView} that displays a 100x100 pixel thumbnail, as shown in the following example
code:</p>
<pre>
mImageView.setImageBitmap(
decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));
</pre>
<p>You can follow a similar process to decode bitmaps from other sources, by substituting the
appropriate {@link
android.graphics.BitmapFactory#decodeByteArray(byte[],int,int,android.graphics.BitmapFactory.Options)
BitmapFactory.decode*} method as needed.</p>

View File

@@ -0,0 +1,239 @@
page.title=Processing Bitmaps Off the UI Thread
parent.title=Displaying Bitmaps Efficiently
parent.link=index.html
trainingnavtop=true
next.title=Caching Bitmaps
next.link=cache-bitmap.html
previous.title=Loading Large Bitmaps Efficiently
previous.link=load-bitmap.html
@jd:body
<div id="tb-wrapper">
<div id="tb">
<h2>This lesson teaches you to</h2>
<ol>
<li><a href="#async-task">Use an AsyncTask</a></li>
<li><a href="#concurrency">Handle Concurrency</a></li>
</ol>
<h2>You should also read</h2>
<ul>
<li><a href="{@docRoot}guide/practices/design/responsiveness.html">Designing for Responsiveness</a></li>
<li><a
href="http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html">Multithreading
for Performance</a></li>
</ul>
<h2>Try it out</h2>
<div class="download-box">
<a href="{@docRoot}shareables/training/BitmapFun.zip" class="button">Download the sample</a>
<p class="filename">BitmapFun.zip</p>
</div>
</div>
</div>
<p>The {@link
android.graphics.BitmapFactory#decodeByteArray(byte[],int,int,android.graphics.BitmapFactory.Options)
BitmapFactory.decode*} methods, discussed in the <a href="load-bitmap.html">Load Large Bitmaps
Efficiently</a> lesson, should not be executed on the main UI thread if the source data is read from
disk or a network location (or really any source other than memory). The time this data takes to
load is unpredictable and depends on a variety of factors (speed of reading from disk or network,
size of image, power of CPU, etc.). If one of these tasks blocks the UI thread, the system flags
your application as non-responsive and the user has the option of closing it (see <a
href="{@docRoot}guide/practices/design/responsiveness.html">Designing for Responsiveness</a> for
more information).</p>
<p>This lesson walks you through processing bitmaps in a background thread using
{@link android.os.AsyncTask} and shows you how to handle concurrency issues.</p>
<h2 id="async-task">Use an AsyncTask</h2>
<p>The {@link android.os.AsyncTask} class provides an easy way to execute some work in a background
thread and publish the results back on the UI thread. To use it, create a subclass and override the
provided methods. Heres an example of loading a large image into an {@link
android.widget.ImageView} using {@link android.os.AsyncTask} and <a
href="load-bitmap.html#decodeSampledBitmapFromResource">{@code
decodeSampledBitmapFromResource()}</a>: </p>
<a name="BitmapWorkerTask"></a>
<pre>
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
private final WeakReference<ImageView> imageViewReference;
private int data = 0;
public BitmapWorkerTask(ImageView imageView) {
// Use a WeakReference to ensure the ImageView can be garbage collected
imageViewReference = new WeakReference<ImageView>(imageView);
}
// Decode image in background.
&#64;Override
protected Bitmap doInBackground(Integer... params) {
data = params[0];
return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
}
// Once complete, see if ImageView is still around and set bitmap.
&#64;Override
protected void onPostExecute(Bitmap bitmap) {
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
if (imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
</pre>
<p>The {@link java.lang.ref.WeakReference} to the {@link android.widget.ImageView} ensures that the
{@link android.os.AsyncTask} does not prevent the {@link android.widget.ImageView} and anything it
references from being garbage collected. Theres no guarantee the {@link android.widget.ImageView}
is still around when the task finishes, so you must also check the reference in {@link
android.os.AsyncTask#onPostExecute(Result) onPostExecute()}. The {@link android.widget.ImageView}
may no longer exist, if for example, the user navigates away from the activity or if a
configuration change happens before the task finishes.</p>
<p>To start loading the bitmap asynchronously, simply create a new task and execute it:</p>
<pre>
public void loadBitmap(int resId, ImageView imageView) {
BitmapWorkerTask task = new BitmapWorkerTask(imageView);
task.execute(resId);
}
</pre>
<h2 id="concurrency">Handle Concurrency</h2>
<p>Common view components such as {@link android.widget.ListView} and {@link
android.widget.GridView} introduce another issue when used in conjunction with the {@link
android.os.AsyncTask} as demonstrated in the previous section. In order to be efficient with memory,
these components recycle child views as the user scrolls. If each child view triggers an {@link
android.os.AsyncTask}, there is no guarantee that when it completes, the associated view has not
already been recycled for use in another child view. Furthermore, there is no guarantee that the
order in which asynchronous tasks are started is the order that they complete.</p>
<p>The blog post <a
href="http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html">Multithreading
for Performance</a> further discusses dealing with concurrency, and offers a solution where the
{@link android.widget.ImageView} stores a reference to the most recent {@link android.os.AsyncTask}
which can later be checked when the task completes. Using a similar method, the {@link
android.os.AsyncTask} from the previous section can be extended to follow a similar pattern.</p>
<p>Create a dedicated {@link android.graphics.drawable.Drawable} subclass to store a reference
back to the worker task. In this case, a {@link android.graphics.drawable.BitmapDrawable} is used so
that a placeholder image can be displayed in the {@link android.widget.ImageView} while the task
completes:</p>
<a name="AsyncDrawable"></a>
<pre>
static class AsyncDrawable extends BitmapDrawable {
private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
public AsyncDrawable(Resources res, Bitmap bitmap,
BitmapWorkerTask bitmapWorkerTask) {
super(res, bitmap);
bitmapWorkerTaskReference =
new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
}
public BitmapWorkerTask getBitmapWorkerTask() {
return bitmapWorkerTaskReference.get();
}
}
</pre>
<p>Before executing the <a href="#BitmapWorkerTask">{@code BitmapWorkerTask}</a>, you create an <a
href="#AsyncDrawable">{@code AsyncDrawable}</a> and bind it to the target {@link
android.widget.ImageView}:</p>
<pre>
public void loadBitmap(int resId, ImageView imageView) {
if (cancelPotentialWork(resId, imageView)) {
final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
final AsyncDrawable asyncDrawable =
new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
imageView.setImageDrawable(asyncDrawable);
task.execute(resId);
}
}
</pre>
<p>The {@code cancelPotentialWork} method referenced in the code sample above checks if another
running task is already associated with the {@link android.widget.ImageView}. If so, it attempts to
cancel the previous task by calling {@link android.os.AsyncTask#cancel cancel()}. In a small number
of cases, the new task data matches the existing task and nothing further needs to happen. Here is
the implementation of {@code cancelPotentialWork}:</p>
<pre>
public static boolean cancelPotentialWork(int data, ImageView imageView) {
final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
if (bitmapWorkerTask != null) {
final int bitmapData = bitmapWorkerTask.data;
if (bitmapData != data) {
// Cancel previous task
bitmapWorkerTask.cancel(true);
} else {
// The same work is already in progress
return false;
}
}
// No task associated with the ImageView, or an existing task was cancelled
return true;
}
</pre>
<p>A helper method, {@code getBitmapWorkerTask()}, is used above to retrieve the task associated
with a particular {@link android.widget.ImageView}:</p>
<pre>
private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
if (imageView != null) {
final Drawable drawable = imageView.getDrawable();
if (drawable instanceof AsyncDrawable) {
final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
return asyncDrawable.getBitmapWorkerTask();
}
}
return null;
}
</pre>
<p>The last step is updating {@code onPostExecute()} in <a href="#BitmapWorkerTask">{@code
BitmapWorkerTask}</a> so that it checks if the task is cancelled and if the current task matches the
one associated with the {@link android.widget.ImageView}:</p>
<a name="BitmapWorkerTaskUpdated"></a>
<pre>
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
&#64;Override
protected void onPostExecute(Bitmap bitmap) {
<strong>if (isCancelled()) {
bitmap = null;
}</strong>
if (imageViewReference != null && bitmap != null) {
final ImageView imageView = imageViewReference.get();
<strong>final BitmapWorkerTask bitmapWorkerTask =
getBitmapWorkerTask(imageView);</strong>
if (<strong>this == bitmapWorkerTask &&</strong> imageView != null) {
imageView.setImageBitmap(bitmap);
}
}
}
}
</pre>
<p>This implementation is now suitable for use in {@link android.widget.ListView} and {@link
android.widget.GridView} components as well as any other components that recycle their child
views. Simply call {@code loadBitmap} where you normally set an image to your {@link
android.widget.ImageView}. For example, in a {@link android.widget.GridView} implementation this
would be in the {@link android.widget.Adapter#getView getView()} method of the backing adapter.</p>