Caching Bitmaps

来源:互联网 发布:编程猫的网址 编辑:程序博客网 时间:2024/06/05 06:14

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 ListViewGridView orViewPager), the total number of images on-screen combined with images that might soon scroll onto the screen are essentially unlimited.

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.

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.

译:

加载单个Bitmap到UI是简单直接的,但是如果你需要一次加载大量的图片,事情则会变得复杂起来。在大多数情况下(例如在使用ListView,GridView或ViewPager时), 屏幕上的图片和因滑动将要显示的图片的数量通常是没有限制的。

通过循环利用子视图可以抑制内存的使用,GC(garbage collector)也会释放那些不再需要使用的bitmap。这些机制都非常好,但是为了保持一个流畅的用户体验,你想要在屏幕滑回来时避免每次重复处理那些图片。内存与磁盘缓存通常可以起到帮助的作用,允许组件快速的重新加载那些处理过的图片。

这一课会介绍在加载多张位图时使用内存Cache与磁盘Cache来提高反应速度与UI的流畅度。

Use a Memory Cache


A memory cache offers fast access to bitmaps at the cost of taking up valuable application memory. The LruCache class (also available in the Support Library 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 LinkedHashMap and evicting the least recently used member before the cache exceeds its designated size.

Note: In the past, a popular memory cache implementation was a SoftReference or WeakReferencebitmap 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.

In order to choose a suitable size for a LruCache, a number of factors should be taken into consideration, for example:

译:

内存缓存以花费宝贵的程序内存为前提来快速访问位图。LruCache 类(在API Level 4的Support Library中也可以找到) 特别合适用来caching bitmaps,用一个强引用(strong referenced)的 LinkedHashMap 来保存最近引用的对象,并且在Cache超出设置大小的时候踢出(evict)最近最少使用到的对象。

Note: 在过去, 一个比较流行的内存缓存的实现方法是使用软引用(SoftReference)或弱引用(WeakReference)bitmap缓存, 然而这是不推荐的。从Android 2.3 (API Level 9) 开始,GC变得更加频繁的去释放soft/weak references,这使得他们就显得效率低下。而且在Android 3.0 (API Level 11)之前,备份的bitmap是存放在native memory 中,它不是以可预知的方式被释放,这样可能导致程序超出它的内存限制而崩溃。

为了给LruCache选择一个合适的大小,有下面一些因素需要考虑到:

  • How memory intensive is the rest of your activity and/or application?
  • How many images will be on-screen at once? How many need to be available ready to come on-screen?
  • What is the screen size and density of the device? An extra high density screen (xhdpi) device like Galaxy Nexus will need a larger cache to hold the same number of images in memory compared to a device like Nexus S (hdpi).
  • What dimensions and configuration are the bitmaps and therefore how much memory will each take up?
  • 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 LruCacheobjects for different groups of bitmaps.
  • 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.

译:

  • 你的程序剩下了多少可用的内存?
  • 多少图片会被一次呈现到屏幕上?有多少图片需要准备好以便马上显示到屏幕?
  • 设备的屏幕大小与密度是多少? 一个具有特别高密度屏幕(xhdpi)的设备,像 Galaxy Nexus 会比 Nexus S (hdpi)需要一个更大的Cache来缓存同样数量的图片.
  • 位图的尺寸与配置是多少,会花费多少内存?
  • 图片被访问的频率如何?是其中一些比另外的访问更加频繁吗?如果是,也许你想要保存那些最常访问的到内存中,或者为不同组别的位图(按访问频率分组)设置多个LruCache 对象。
  • 你可以平衡质量与数量吗? 某些时候保存大量低质量的位图会非常有用,在加载更高质量图片的任务则交给另外一个后台线程。

没有指定的大小与公式能够适用与所有的程序,你需要负责分析你的使用情况后提出一个合适的解决方案。一个太小的Cache会导致额外的花销却没有明显的好处,一个太大的Cache同样会导致java.lang.OutOfMemory的异常(Cache占用太多内存,其他活动则会因为内存不够而异常),并且使得你的程序只留下小部分的内存用来工作。

下面是一个为bitmap建立LruCache 的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
privateLruCache<String, Bitmap> mMemoryCache;
 
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
...
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
finalint maxMemory = (int) (Runtime.getRuntime().maxMemory() /1024);
 
// Use 1/8th of the available memory for this memory cache.
finalint cacheSize = maxMemory / 8;
 
mMemoryCache =new LruCache<String, Bitmap>(cacheSize) {
@Override
protectedint sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
returnbitmap.getByteCount() / 1024;
}
};
...
}
 
publicvoid addBitmapToMemoryCache(String key, Bitmap bitmap) {
if(getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
 
publicBitmap getBitmapFromMemCache(String key) {
returnmMemoryCache.get(key);
}

Note: 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 GridView filled with images on a device with 800×480 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.

When loading a bitmap into an ImageView, the LruCache is checked first. If an entry is found, it is used immediately to update the ImageView, otherwise a background thread is spawned to process the image:

译:

Note:在上面的例子中, 有1/8的程序内存被作为Cache. 在一个常见的设备上(hdpi),最小大概有4MB (32/8). 如果一个填满图片的GridView组件放置在800×480像素的手机屏幕上,大概会花费1.5MB (800x480x4 bytes), 因此缓存的容量大概可以缓存2.5页的图片内容.

当加载位图到 ImageView 时,LruCache 会先被检查是否存在这张图片。如果找到有,它会被用来立即更新 ImageView 组件,否则一个后台线程则被触发去处理这张图片。

1
2
3
4
5
6
7
8
9
10
11
12
publicvoid loadBitmap(int resId, ImageView imageView) {
finalString imageKey = String.valueOf(resId);
 
finalBitmap bitmap = getBitmapFromMemCache(imageKey);
if(bitmap != null) {
mImageView.setImageBitmap(bitmap);
}else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task =new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}

The BitmapWorkerTask also needs to be updated to add entries to the memory cache:

1
2
3
4
5
6
7
8
9
10
11
12
classBitmapWorkerTask extendsAsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protectedBitmap doInBackground(Integer... params) {
finalBitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0],100,100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
returnbitmap;
}
...
}

Use a Disk Cache


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 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 has to process each image again.

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.

译:

内存缓存能够提高访问最近查看过的位图的速度,但是你不能保证这个图片会在Cache中。像类似 GridView 等带有大量数据的组件很容易就填满内存Cache。你的程序可能会被类似Phone call等任务而中断,这样后台程序可能会被杀死,那么内存缓存就会被销毁。一旦用户恢复前面的状态,你的程序就又需要重新处理每个图片。

磁盘缓存可以用来保存那些已经处理好的位图,并且在那些图片在内存缓存中不可用时减少加载的次数。当然从磁盘读取图片会比从内存要慢,而且读取操作需要在后台线程中处理,因为磁盘读取操作是不可预期的。

Note: A ContentProvider might be a more appropriate place to store cached images if they are accessed more frequently, for example in an image gallery application.

The sample code of this class uses a DiskLruCache implementation that is pulled from the Android source. Here’s updated example code that adds a disk cache in addition to the existing memory cache:

译:

Note:如果图片被更频繁的访问到,也许使用 ContentProvider 会更加的合适,比如在Gallery程序中。

这一节的范例代码中使用了一个从Android源码中剥离出来的 DiskLruCache 。升级过的范例代码给已有的内存缓存添加了磁盘缓存.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
privateDiskLruCache mDiskLruCache;
privatefinal Object mDiskCacheLock = new Object();
privateboolean mDiskCacheStarting = true;
privatestatic final int DISK_CACHE_SIZE = 1024 * 1024 * 10;// 10MB
privatestatic final String DISK_CACHE_SUBDIR = "thumbnails";
 
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
newInitDiskCacheTask().execute(cacheDir);
...
}
 
classInitDiskCacheTask extendsAsyncTask<File, Void, Void> {
@Override
protectedVoid doInBackground(File... params) {
synchronized(mDiskCacheLock) {
File cacheDir = params[0];
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
mDiskCacheStarting =false;// Finished initialization
mDiskCacheLock.notifyAll();// Wake any waiting threads
}
returnnull;
}
}
 
classBitmapWorkerTask extendsAsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protectedBitmap doInBackground(Integer... params) {
finalString 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
finalBitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0],100,100));
}
 
// Add final bitmap to caches
addBitmapToCache(imageKey, bitmap);
 
returnbitmap;
}
...
}
 
publicvoid addBitmapToCache(String key, Bitmap bitmap) {
// Add to memory cache as before
if(getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
 
// Also add to disk cache
synchronized(mDiskCacheLock) {
if(mDiskLruCache != null&& mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
}
 
publicBitmap getBitmapFromDiskCache(String key) {
synchronized(mDiskCacheLock) {
// Wait while disk cache is started from background thread
while(mDiskCacheStarting) {
try{
mDiskCacheLock.wait();
}catch (InterruptedException e) {}
}
if(mDiskLruCache != null) {
returnmDiskLruCache.get(key);
}
}
returnnull;
}
 
// Creates a unique subdirectory of the designated app cache directory. Tries to use external
// but if not mounted, falls back on internal storage.
publicstatic File getDiskCacheDir(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
finalString cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath();
 
returnnew File(cachePath + File.separator + uniqueName);
}

备注:
Google给的上面的代码做了很多简化,实际需要参考DiskLruCache的源码,参考Google给的DisplayBitmap的这个demo.
Note: Even initializing the disk cache requires disk operations and therefore should not take place on the main thread. However, this does mean there's a chance the cache is accessed before initialization. To address this, in the above implementation, a lock object ensures that the app does not read from the disk cache until the cache has been initialized.

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.

译:

Note:初始化磁盘缓存涉及到I/O操作,所以不应该在主线程中进行。但是这也意味着在初始化完成之前缓存可以被访问。为了解决这个问题,在上面的实现中,有一个锁对象(lock object)用来确保在磁盘缓存完成初始化之前,app无法对它进行读取。

内存缓存的检查是可以在UI线程中进行的,磁盘缓存的检查需要在后台线程中处理。磁盘操作永远都不应该在UI线程中发生。当图片处理完成后,最后的位图需要添加到内存缓存与磁盘缓存中,方便之后的使用。

Handle Configuration Changes


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 Handling Runtime Changes). 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.

Luckily, you have a nice memory cache of bitmaps that you built in the Use a Memory Cache section. This cache can be passed through to the new activity instance using a Fragment which is preserved by calling setRetainInstance(true)). After the activity has been recreated, this retained Fragment is reattached and you gain access to the existing cache object, allowing images to be quickly fetched and re-populated into the ImageView objects.

Here’s an example of retaining a LruCache object across configuration changes using a Fragment:

译:

运行时配置改变,例如屏幕方向的改变会导致Android去destory并restart当前运行的Activity。(关于这一行为的更多信息,请参考Handling Runtime Changes). 你需要在配置改变时避免重新处理所有的图片,这样才能提供给用户一个良好的平滑过度的体验。

幸运的是,在前面介绍Use a Memory Cache的部分,你已经知道如何建立一个内存缓存。通过调用setRetainInstance(true))保留一个Fragment实例, 这个缓存可以通过被保留的Fragment传递给新的Activity实例。在这个activity被recreate之后, 这个保留的 Fragment 会被重新附着上。这样你就可以访问Cache对象,从中获取到图片信息并快速的重新添加到ImageView对象中。

下面是配置改变时使用Fragment来保留LruCache 的示例:(翻译的变味了。。。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
privateLruCache<String, Bitmap> mMemoryCache;
 
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
mMemoryCache = retainFragment.mRetainedCache;
if(mMemoryCache == null) {
mMemoryCache =new LruCache<String, Bitmap>(cacheSize) {
...// Initialize cache here as usual
}
retainFragment.mRetainedCache = mMemoryCache;
}
...
}
 
classRetainFragment extendsFragment {
privatestatic final String TAG = "RetainFragment";
publicLruCache<String, Bitmap> mRetainedCache;
 
publicRetainFragment() {}
 
publicstatic RetainFragment findOrCreateRetainFragment(FragmentManager fm) {
RetainFragment fragment = (RetainFragment) fm.findFragmentByTag(TAG);
if(fragment == null) {
fragment =new RetainFragment();
fm.beginTransaction().add(fragment, TAG).commit();
}
returnfragment;
}
 
@Override
publicvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
<strong>setRetainInstance(true);</strong>
}
}

To test this out, try rotating a device both with and without retaining the 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.

为了测试上面的效果,尝试在保留Fragment与没有这样做的情况下旋转屏幕。你会发现当你保留缓存时,从内存缓存中重新绘制几乎没有延迟的现象. 内存缓存中没有的图片可能在存在磁盘缓存中.如果两个缓存中都没有,则图像会像平时一样被处理。

0 0