加载一个bitmap到你的UI中是很简单的,然而当你要同时加载一大批图片就会变得很复杂了。在很多情况下(比如ListView,GridView或者ViewPager),屏幕上的图片总数和即将滚动到屏幕的图片会根本没有限制。
可用内存空间在这种将屏幕外的子view回收的组件下会一直下降,这是在假设你没有保持任何有用的长引用的情况下。这样做很好,但是为了保持流畅和快速加载UI,你需要避免在这些图片每次回到屏幕时都去不断地处理。
这个教程会教你如何使用内存和磁盘缓存处理bitmap,在加载很多bitmap的时候,提升你的UI组件的响应速度和流畅性。
使用内存缓存
内存缓存可以在耗费宝贵的内存的情况下快速的读取bitmap。LruCache类(同样适用API级别4之前使用Support类库),特别适合用来缓存bitmap,它会保持对最近使用的对象的强引用,这是在强引用的LinkedHashMap中实现的,并且它会在将耗尽限定大小内存前取消最进最少使用的资源。
注意:在过去,一个流行的实现内存缓存的方法是SoftReference或WeakReference bitmap的缓存,然而,这并不推荐。Android 2.3(API 级别9)起,垃圾收集器会在收集soft/weak 引用 变得更加激进,这样回使得它们变得基本上不起作用。另外,在Android 3.0(API 级别11)之前,备份bitmap的数据是存储在一个native内存中,它不是用一种可以预测的方式释放的,会潜在地造成应用程序很快的耗尽它的内存限制并崩溃。
为了选择一个合适的LruCache大小,有很多因素需要被考虑到,例如:
·你剩下的activity或程序会有宽裕的内存使用吗?
·有多少图片会一下展示在屏幕上?有多少图片需要准备显示在屏幕上?
·设置的屏幕尺寸和密度是多少?一个特变高密度pm(xhdpi)的设备,比如Galaxy Nexus 会比其它设置像Nexus S(hdpi)需要一个大的缓存去在内存中支持同样多的图片.
·bimap的尺寸和配置如何,那么有多少内存会被占用?
·图片的访问频率是怎么样的?是不是有些会被访问的比其他的频繁些呢?如果这样,你可能想保证这些图片一直在内存中,甚至是创建多个不同的LruCache对象来处理不同组的bitmap。
·你可以平衡质量和数量吗?有时候,存储大量的低质量bitmap,在后台任务中静默下载高质量版本的图片会很有效果
没有一个特定的尺寸和公式适合所有的程序,这决定于你分析你的内存情况,提出一个合适的方案。缓存太小会造成额外的开销,没有任何益处,缓存太大,同样会造成java.lang.OutOfMemory异常,让你的程序工作在很少的内存下。
下面是一个设置bitmap的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.
//获取最大可以虚拟机内存,超过这个数量会抛出OutOfMemory异常
//LruCache存储单位是kb,它的构造方法含有一个int参数
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
// 使用1/8的可用内存来做缓存
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.
//这个缓存的大小是用kb来衡量的,而不是按个数
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);
}
注意:在这个例子中,1/8的程序内存被分配到我们的缓存中。在普通/高像素的设备中,这个值至少是4MB(32/8).一个全屏的GridView,用图片填满一个800x480分辨率的设备会使用到大概1.5MB(800*480*4字节),那么,这个内存至少可以缓存2.5页图片。
当加载bitmap到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.imge_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
BitmapWorkerTask 同样需要更新,用来添加键值到内存缓存中。
class BitmapWorkerTask extends AsyncTask<Integer,Void,Bitmap>{
...
//后台解析图片
@override
protected Bitmap doInBackGround(Integer... parms){
final Bitmap bitmap = decodeSampledBitmapFromResource(getResouces(),param[0],100,100));
addBitmapToMemoryCache(String.valueOf(params[0]),bitmap);
return bitmap;
}
...
}
使用磁盘缓存
内存缓存是一个有效提高访问最近看过的bitmap的方法,然而,你不能依赖所有的图片都可以在内存中使用。类似GridView的组件都会附带大量的信息,可以很容易的填满所有的内存缓存。你的程序可能被其它任务中断,比如电话,并且,在后台的时候,它可能会被杀死,内存缓存会被销毁。一旦用户重新使用,你的程序就必须再次处理这些图片。 磁盘缓存可以用在这些例子中,通过持久化处理bitmap,来帮助减少加载内存缓存中没有的图片的时间。当然,从磁盘读取图片会比在内存中加载图片要慢,并且应该在后台线程中处理,这是因为磁盘的读取时间也是无法预计的。 注意:ContentProvider应该是一个很适合存储频繁访问的缓存图片的地方,例如,一个图片浏览程序。 这个示例程序中使用了一个实现了DiskLruCache的类 ,这是从 Android source中提取出来的。这是更新的示例代码,它在已存在的内存缓存中添加了磁盘缓存。
private DiskLruCache mDisLruCache; 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){ ... // 初始化内存缓存 ... //在后台线程中初始化磁盘缓存 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(mdiskCahceLock){ File cacheDir = params[0]; mDiskLruCache = DiskLruCache.open(cacheDir,DISK_CACHE_SIZE); mDiskCacheStarting = false; //初始化完成 mDiskCacheLock.notifyAll(); // 缓存所有等待的线程 } return null; } } class BitmapWorkerTask extends AsyncTask<Integer,Void,Bitmap>{ ... // 后台解码图片 @Override protected Bitmap doInBackground(Integer... params){ final String imageKey = String.valueOf(params[0]); //后台检查磁盘缓存 Bitmap bitmap = getBitmapFromDiskCache(imageKey); if(bitmap==null){//没有在磁盘缓存中找到 //普通方式处理 final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(),params[0],100,100); } //添加bitmap到缓存中 addBitmapToCache(imageKey,bitmap); return bitmap; } public void addBitmapToCache(String key,Bitmap bitmap){ // 像以前一样添加到内存缓存中 if(getBitmapFromMemCache==null){ mMemoryCache.put(key,bitmap); } //同时添加中磁盘缓存中 synchronized(mdiskCacheLock){ if(mDiskLruCache != null&& mDiskLruCache.get(key) == null){ mDiskLruCache.put(key,bitmap); } } } public Bitmap getBitmapFromDiskCache(String key){ synchronized(mDiskCacheLock){ //等待磁盘缓存从后台线程中开始 while(mDiskCacheStarting){ try{ mDiskCacheLock.wait(); }catch(InterruptedException e){} } if(mDiskLruCache != null){ return mDiskLruCache.get(key); } } return null; } //创建一个唯一的子目录在指定的程序缓存目录中。尝试使用外部空间,但是如果没有挂载,那么 //就恢复使用内部空间 public static File getDiskCacheDir(Context context,String uniqueName){ //检查媒体是否挂载或存储空间是否内置,如果是,那么尝试使用外部存储目录 //否则使用内存缓存目录 final Sting cachePath = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())||!isExternalStorageRemovable()?getExternalCacheDir(context).getPath():context.getCacheDir().getPath(); return new File(cachePath+File.separator +uniqueName); }