原文: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线程做磁盘操作。当图片处理完成,应该将其添加进内存缓存和磁盘缓存中,以备将来不时之需。

Android Training Caching Bitmaps 翻译的更多相关文章

  1. Android Training

    Building Apps with Content Sharing Simple Data --> Intent && ActionProvider 介绍如何让应用程序共享简单 ...

  2. Android训练课程(Android Training) - 高效的显示图片

    高效的显示图片(Displaying BitmapsEfficiently) 了解如何使用通用的技术来处理和读取位图对象,让您的用户界面(UI)组件是可响应的,并避免超过你的应用程序内存限制的方式.如 ...

  3. Android Training精要(六)如何防止Bitmap对象出现OOM

    1.使用AsyncTask異步加載bitmap圖片避免OOM: class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> ...

  4. android性能小贴士 翻译

    转自http://developer.android.com/training/articles/perf-tips.html 性能小贴士: 这篇文档主要一些微优化可以提升应用程序性能,但是这些改变不 ...

  5. Android训练课程(Android Training) - 添加活动栏(使用action bar)

    2014-10-28 张云飞VIR 翻译自:https://developer.android.com/training/basics/actionbar/index.html 添加活动栏(Addin ...

  6. Android训练课程(Android Training) - 构建你的第一个应用

    本文翻译自:https://developer.android.com/training/basics/firstapp/index.html 构建你的第一个应用(app) 欢迎来到安卓开发! 这个课 ...

  7. Android Training - 管理应用的内存

    http://hukai.me/android-training-managing_your_app_memory/ Random Access Memory(RAM)在任何软件开发环境中都是一个很宝 ...

  8. android 6.0特性翻译 --渣渣

    所有关于Android 6.0 棉花糖的知识 上下文帮助 1.现在按压:不需要离开你正在运行的app或者访问的网站就可 获取帮助,仅仅触摸和按下Home按钮.(长按Home键,可以在 android ...

  9. Android Training精要(七)内存管理

    在2.3.3及以下版本: 通過定義兩個整形變量來檢測bitmap是否display過或者已經在緩存中 下面的代碼當bitmap滿足兩個條件就被回收掉: 1. 兩個整形變量都變為0 2. bitmap不 ...

随机推荐

  1. IEEE 754二进制浮点数算术标准

    可能很多人都遇到过浮点数精度丢失的问题,下面以JavaScript为例. 1 - 0.9 = 0.09999999999999998 纳尼,不应该是0.1么,怎么变成0.099999999999999 ...

  2. springmvc 动态加载配置文件

    <import resource="classpath:config/spring-profile-properties.xml" /> <context:pro ...

  3. 深入理解 Java try-with-resource 语法糖

    背景 众所周知,所有被打开的系统资源,比如流.文件或者Socket连接等,都需要被开发者手动关闭,否则随着程序的不断运行,资源泄露将会累积成重大的生产事故. 在Java的江湖中,存在着一种名为fina ...

  4. 浅谈压缩感知(十七):测量矩阵之有限等距常数RIC的计算

    有限等距常数(RestrictedIsometry Constant, RIC)是与有限等距性质(Restricted IsometryProperty, RIP)紧密结合在一起的一个参数. 一.RI ...

  5. SSE图像算法优化系列三:超高速导向滤波实现过程纪要(欢迎挑战)

    自从何凯明提出导向滤波后,因为其算法的简单性和有效性,该算法得到了广泛的应用,以至于新版的matlab都将其作为标准自带的函数之一了,利用他可以解决的所有的保边滤波器的能解决的问题,比如细节增强.HD ...

  6. SoapUI Pro Project Solution Collection –Easy develop Groovy Script to improve SoapUI ability

    As you know the groovy script and java script language is the soapui supported .but unfortunately So ...

  7. Java 8系列之Stream的基本语法详解

    本文转至:https://blog.csdn.net/io_field/article/details/54971761 Stream系列: Java 8系列之Stream的基本语法详解 Java 8 ...

  8. Nginx 介绍

    Nginx 是什么 Nginx ("engine x") 是一个开源的,支持高性能.高并发的 Web 服务和代理服务软件.它是由俄罗斯人 Igor Sysoev 开发的,最初被应用 ...

  9. fft ocean注解

    针对这两篇教程: http://www.keithlantz.net/2011/10/ocean-simulation-part-one-using-the-discrete-fourier-tran ...

  10. 如何用javac 和java 编译运行整个Java工程

    转自:http://blog.csdn.net/huagong_adu/article/details/6929817      前言:本文教你怎么用javac和Java命令,以及如何利用脚本(she ...