缓存 Bitmaps
加载单独的一张图片用于显示是很简单的,但是如果一次性加载大量的图片时,事情就变得比较复杂了,在许多的情况下(像
ListView
,
GridView
or
ViewPager
之类的组件 ),在屏幕上显示的图片加上所有待显示的图片有可能马上就会在屏幕上无限制的进行滚动、切换。
像
ListView
,
GridView
这类组件,它们的子项当不可见时,所占用的内存会被回收以供正在前台显示子项使用。垃圾回收器也会释放你已经加载了的图片占用的内存,假设它们不是一个生命周期很长的对象。这些对内存的有效利用都很好,但如果你想让你的UI运行流畅的话,就不应该每次显示时都去重新加载图片。这个时候保持一些内存和文件缓存就很有必要了,这样可以让你快速的重新加载处理图片。
使用内存缓存
内存缓存是预先消耗应用的一点内存来存储数据,以便可以快速的为应用中的组件提供数据,是一种典型的以空间换时间的策略。
LruCache
类(Android v4 Support Library 类库中开始提供)非常适合来做图片缓存任务 ,它可以使用一个
LinkedHashMap
的强引用来保存最近使用的对象,并且当它保存的对象占用的内存总和超出了为它设计的最大内存时会把不经常使用的对象成员踢出以供垃圾回收器回收
Note : 在以前,一个非常流行的内存缓存的实现是使用SoftReference
or WeakReference
,但是这种办法现在并不推荐。从Android 2.3开始,垃圾回收器会更加积极的去回收软引用和弱引用引用的对象,这样导致这种做法相当的无效。另外,在Android 3.0之前,图片数据保存在本地内存中,它们不是以一种可预见的方式来释放的,这样可能会导致应用内存的消耗量出现短暂的超限,应用程序崩溃
为了为
LruCache
设置一个合适的内存大小,有很多因素要进行考虑,如下
一、还剩余多少内存给你的activity和/或应用使用
二、屏幕上一次性需要显示多少张图片,有多少图片在等待在屏幕上显示
三、手机的大小和密度是多少,一个超高密度屏幕的设备( Galaxy Nexus
)往往需要一个更大的缓存
四、图片的尺寸和配置是多少,决定了每张图片占用内存的大小
五、图片的访问频率是多少,是否一些比另外一些访问的更加频繁。如果是这样的话,你可能需要在内存中一直保存那几项,
甚至你需要为不同用途的图片组配置不同的
LruCache
对象
六、有些时候你还要去平衡图片的质量和数量,保存大量低质量的图片,而另外临时的去加载一个高质量的图片版本是很有用的
这里没有一个固定的大小或公式来适用所有的应用,你需要详细的去分析你的应用图片使用的情况从而来找到一个合适的解决办法
一个设置
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);
}
Note:在这个小例子程序中,为LruCache 设置了一个缓存大小,为应用程序最大能使用内存的1/8,在一个中密度或高密度的设备上,缓存的大小最少为 4M(32/8),以GridView 为例,如果GridView 满屏显示图片的话,大概会消耗1.5M(800*480*4bytes)内存,所以我们能够至少缓存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);
}
}
在图片加载的Task中,需要把加载好的图片加入到内存缓存中
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
之类的组件会有一个很大的数据集,很简单的就能够把内存缓存填满。你的应用也有可能被其它的任务(像来电)打断,你的应用会被切入到后台,这样就有可能会被杀死,内存缓存对象也会被销毁。 当你的应用重新回到前台显示时,你的应用又需要一张一张的去加载图片了。
磁盘文件缓存能够用来处理这些情况,保存处理好的图片,当内存缓存不可用的时候,直接读取在硬盘中保存好的图片,这样可以有效的减少图片加载的次数。读取磁盘文件要比直接从内存缓存中读取要慢一些,而且需要在一个UI主线程外的线程中进行,因为磁盘的读取速度是不能够保证的。磁盘文件缓存显然也是一种以空间换时间的策略。
Note: 如果图片使用非常频繁的话,一个 ContentProvider
可能更适合代替去存储缓存图片,比如图片gallery 应用
下面是一个使用磁盘文件缓存的程序片段
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);
}
Note:即使是初始化磁盘缓存都需要进行磁盘操作,因此你不能在UI主线程中进行这项操作,这就意味着可以在磁盘缓存初始化之前进行访问。为了避免这种情况的发生,上面的程序片段中,一个锁对象确保了磁盘缓存没有初始化完成之前不能够对磁盘缓存进行访问
内存缓存在UI线程中进行检测,磁盘缓存在UI主线程外的线程中进行检测,当图片处理完成之后,分别存储到内存缓存和磁盘缓存中
处理设备配置改变
应用在运行的时候设备的配置参数有可能改变,例如设备朝向改变,会导致Android销毁你的Activity然后按照新的配置重启,这种情况下,你可能会想避免重新去加载处理所有的图片,让用户能有一个流畅的体验
幸运的是,在上一节你已经为图片提供了一个非常好的内存缓存。使用 Fragment 能够把内存缓存对象传递到新的activity实例中,调用
setRetainInstance(true)
) 方法来保留Fragment实例。当activity重新创建好后, 被保留的Fragment依附于activity而存在,通过Fragment你可以获取到已经存在的内存缓存对象了,这样就可以快速的获取到图片,并设置到ImageView上,给用户一个流畅的体验。
下面是一个示例程序片段
private LruCache<String, Bitmap> mMemoryCache;
@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;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 使得Fragment在Activity销毁后还能够保留下来
setRetainInstance(true);
}
}
你可以通过改变设备的朝向来测试上面的程序,你会发现基本上没有延迟,图片很快的就显示在屏幕上。