<span style="font-family:SimSun;font-size:14px;background-color: rgb(255, 255, 255);"></span>
加载一张图片并显示在画面上是非常简单的,但是当需要同时加载很多图片时,问题就变得比较复杂了。如ListView、GridView或ViewPager中,就需要在画面上显示很多图片,且图片的数量会随着控件的滚动而一直增加。为了使内存保持在一个合理的范围,这类控件通常会在view滚动出屏幕时对view进行回收。垃圾处理器也会认为你不再持有这些对象的引用,对这些图片进行回收。这些方式控制内存都是非常好的,但是为了界面保持流畅、使数据的加载保持快速,就需要去避免在每次用户滑动回来的时候再去重新处理图片。这个时候,你需要使用内存缓存技术,这个技术使得控件可以快速加载处理过的图片。
使用内存缓存技术
内存缓存技术为那些被图片耗掉很多宝贵内存的应用提供了快速的解决方法。LruCache类非常适合处理图片的缓存、将常用的对象保存在一个强引用的LinkedHashMap中,在内存占用超过期望值之前就对最少使用的对象进行回收。
在过去,一种很流行的缓存图片的方法是使用软引用或弱引用,但是现在已经不推荐使用了。从Android2.3开始,垃圾回收器更倾向于回收弱/软引用,这使得它们变得不再可靠。另外,Android3.0之后,图片的数据被保存在本地的内存中,这使得它的回收就变得难以掌握和预测,造成了潜在的问题,可能导致应用的内存瞬间超出内存限制导致OOM。
为了给LruCache选择一个合适的大小,需要考虑很多因素,例如:
- 设备可为单个应用分配的最大内存量是多少?
- 会一次性在屏幕上显示多少张图片?需要预加载多少张图片?
- 屏幕的大小和分辨率是多少?一个高分辨率的设备如Galaxy Nexus(xhdpi)和一个分辨率较低的设备如Nexus S(hdpi)相比,保存同样数量的图片,则分辨率高的设备需要更多的内存。
- 图片的尺寸和大小即每张图片需要占用多少内存。
- 图片被访问的频率有多高?是否有的图片访问频率要比其他图片高的多?如果是,你也许需要将这些图片常驻在内存中或者使用多个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);
}
在这个例子中,将应用1/8的内存分配给了缓存。在一个普通设备(hdpi)上也就相当于4MB(32/8)左右的大小。如果将屏幕上放置一个gridview并用图片填满它,则分辨率为800*480的设备需要使用大约1.5MB(800*480*4字节),所以4MB的缓存大小至少可以缓存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可能更合适用来保存这些处理过的图片,例如使用gallery的应用。
下面的代码是在内存缓存的基础上配合使用了硬盘缓存:
<span style="font-weight: normal;">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);
}</span>
<span style="font-weight: normal;">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);
}</span>
注:虽然硬盘缓存的初始化需要对硬盘进行操作,从而不应该在主线程中处理,但是,由于必须得保证硬盘缓存在初始化前可用,因此选择在主线程中处理。基于这个,在上面的实现中,一个锁住的对象保证了应用不会在硬盘缓存初始化前读取它。
当内存缓存在主线程中检查时,硬盘缓存也在子线程中检查。所有硬盘操作都不应该放在主线程中。当图片处理完成后,获得的图片会在内存缓存和硬盘缓存中各保存一份。
处理Configuration Changes
运行时配置改变,如屏幕发生了旋转,导致Android当前运行的Activity被关闭并用新的配置进行重启。一旦配置发生变化,为了使用户体验到平滑且快速的过渡就需要避免去重新加载图片。
幸运的是,你有一个很好的图片内存缓存区。只需要调用setRetainInstance(true)方法,它就可以将缓存的图片传递给使用fragment的被新建的activity实例。activity重建完成后,这个保有缓存区的fragment被添加到activity,于是我们就可以对缓存区域进行操作了,这样就能迅速的读取图片并将它显示在ImageView上。
下面是在配置更改时使用Fragment保留LruCache的例子:
<span style="font-weight: normal;">private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void 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;
}
...
}
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();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}</span>
可以测试一下,在设置和不设置fragment的情况下分别试试旋转屏幕。你会发现当设置了之后旋转屏幕时,activity的显示很少或基本上没有延迟。如果在内存缓存中没有找到图片,那基本上可以在硬盘缓存中找到,如果还没有,则它会被重新处理。
<span style="font-weight: normal;">private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void 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;
}
...
}
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();
fm.beginTransaction().add(fragment, TAG).commit();
}
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
}
}</span>