原文:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
图片缓存
在Android开发中,加载一个图片到界面很容易,但如果一次加载大量图片就复杂多了。在很多情况下(比如:ListView,GridView或ViewPager),能够滚动的组件需要加载的图片几乎是无限多的。
有些组件的child view在不显示时会回收,并循环使用,如果没有任何对bitmap的持久引用的话,垃圾回收器会释放你加载的bitmap。这没什么问题,但当这些图片再次显示的时候,要想避免重复处理这些图片,从而达到加载流畅的效果,就要使用内存缓存和本地缓存了,这些缓存可以让你快速加载处理过的图片。
这些缓存,就是本章要讨论的内容。
使用内存缓存
内存缓存以牺牲内存的代价,带来快速的图片访问。LruCache类(API Level 4之前可以使用Support Library)非常适合图片缓存任务,在一个LinkedHashMap中保存着对Bitmap的强引用,当缓存数量超过容器容量时,删除最近最少使用的成员(LRU)。
注意:在过去,非常流行用SoftReference或WeakReference来实现图片的内存缓存,但现在不再推荐使用这个方法了。因为从Android 2.3 (API Level 9)之后,垃圾回收器会更积极的回收soft/weak的引用,这将导致使用soft/weak引用的缓存几乎没有缓存效果。顺带一提,在Android3.0(API Level 11)以前,bitmap是储存在native 内存中的,所以系统以不可预见的方式来释放bitmap,这可能会导致短时间超过内存限制从而造成崩溃。
为了给LruCache一个合适的容量,需要考虑很多因素,比如:
- 你其它的Activity 和/或 Application是怎样使用内存的?
- 屏幕一次显示多少图片?需要多少图片为显示到屏幕做准备?
- 屏幕的大小(size)和密度(density)是多少?像Galaxy Nexus这样高密度(xhdpi)的屏幕在缓存相同数量的图片时,就需要比低密度屏幕Nexus S(hdpi)更大的内存。
- 每个图片的尺寸多大,相关配置怎样的,占用多大内存?
- 图片的访问频率高不高?不同图片的访问频率是否不一样?如果是,你可能会把某些图片一直缓存在内存中,或需要多种不同缓存策略的LruCache。
- 你能平衡图片的质量和数量吗?有时候,缓存多个质量低的图片是很有用的,而质量高的图片应该(像下载文件一样)在后台任务中加载。
这里没有适应所有应用的特定大小或公式,只能通过分析具体的使用方法,来得出合适的解决方案。缓存太小的话没有实际用处,还会增加额外开销;缓存太大的话,会再一次造成OutOfMemory异常,并给应用的其他部分留下很少的内存。
下面是一个图片的LruCache的配置例子:
private LruCache<String, Bitmap> mMemoryCache; @Override
protected void 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.转换单位
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); // Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8; mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.转换单位
return bitmap.getByteCount() / 1024;
}
};
...
} public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
} public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
注意:在这个例子中,将应用最大可使用内存的八分之一分配给了缓存,在一个普通的或者hdpi设备上,这个值大约至少是4MB(32/8)。在一个800x480分辨率的设备上全屏显示一个填满图片的GridView大约要1.5MB(800*480*4 bytes),因此,这个LruCache至少能够缓存2.5页的图片。
当往ImageView中加载图片时,先检查以下LruCache中是否已经缓存。如果已经缓存,则直接取出并更新ImageView,否则要启动个后台任务来处理图片:
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);
}
}
这个BitmapWorkerTask也需要将新处理完的图片添加进缓存:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(
getResources(), params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
使用磁盘缓存
内存缓存能够加快对最近显示过的图片的访问速度,然而你不能认为缓存中的图片全是有效的。像GridView这样需要大量数据的组件是很容易填满内存缓存的。你的应用可能会被别的任务打断(比如一个来电),它可能会在后台被杀掉,其内存缓存当然也被销毁了。当用户恢复你的应用时,应用将重新处理之前缓存的每一张图片。
在这个情形中,使用磁盘缓存可以持久的储存处理过的图片,并且缩短加载内存缓存中无效的图片的时间。当然从磁盘加载图片比从内存中加载图片要慢的多,并且由于磁盘读取的时间是不确定的,所以要在后台线程进行磁盘加载。
注意:如果以更高的频率访问图片,比如图片墙应用,使用ContentProvider可能更适合储存图片缓存。
下面这个例子除了之前的内存缓存,还添加了一个磁盘缓存,这个磁盘缓存实现自Android源码中的DiskLruCache:
private DiskLruCache mDiskLruCache;
private final Object mDiskCacheLock = new Object();
private boolean mDiskCacheStarting = true;
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
private static final String DISK_CACHE_SUBDIR = "thumbnails"; @Override
protected void onCreate(Bundle savedInstanceState) {
...
// Initialize memory cache
...
// Initialize disk cache on background thread
File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
new InitDiskCacheTask().execute(cacheDir);
...
} class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
@Override
protected Void 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
}
return null;
}
} class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// Decode image in background.
@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(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
synchronized (mDiskCacheLock) {
if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
mDiskLruCache.put(key, bitmap);
}
}
} public Bitmap 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) {
return mDiskLruCache.get(key);
}
}
return null;
} // 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 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
final String cachePath =
Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
!isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
context.getCacheDir().getPath(); return new File(cachePath + File.separator + uniqueName);
}
注意:磁盘缓存的初始化会涉及磁盘操作,因此不应该在UI线程做初始化操作。然而,这样做可能会出现初始化完成前,就访问缓存的情况。为了解决这个问题,上面的例子使用了一个锁对象,确保在磁盘缓存初始化完成前,不会有其他对象使用它。
检查内存缓存可以在UI线程,检查磁盘缓存最好在后台线程。永远不要在UI线程做磁盘操作。当图片处理完成,应该将其添加进内存缓存和磁盘缓存中,以备将来不时之需。