译文出自谷歌安卓官网图片缓存篇
缓存位图
加载一个简单的位图到UI中比较快,但是如果加载一些大的位图的话,则会变得很复杂。在许多情况下(ListView,GridView,ViewPager),屏幕上的图片与即将滚动到屏幕上的图片个数一般是无限制的。通过回收移除屏幕外的子视图可以让内存使用率降下来。假设你不做任何长引用的话,垃圾收集器也会释放你已加载的位图。一切都很好,但是如果你想要保持一个流动的并快速加载的UI的话,你必须避免重复加载这些即将滚动回屏幕的图片。内存缓存与磁盘缓存可以做到这些,允许组件快速加载已处理过的图片。
使用内存缓存(Use a Memory Cache)
内存缓存以占用应用程序一些内存为条件提供了快速获取位图的方式。LruCache类被用来做缓存图片任务的,让最近引用的对象保存到强引用LinkedHashMap中,并在超出缓存大小之前删除最近最少使用的成员。
注意:在过去,流行的内存缓存实现是SoftReference 或 WeakReference位图缓存,但是现在不推荐使用了。从Android2.3开始,垃机回收器会更喜欢回收soft/weak references,这导致了它们变得无效了。除此之外,在Android3.0之前,备份的位图数据被保存在本地内存中,释放方式是不可预测的,可能导致应用程序快速的超过内存限制并导致崩溃。
为了选择合适的LruCache大小,一些因素我们需要考虑:
- Activity或者Application剩余内存
- 多少张图片会立即显示到屏幕上,还有多少会滚动到屏幕中。
- 设备的屏幕大小与密度,像Galaxy Nexus这种高分辨率xhdpi比起Nexus S来说,需要更大的缓存。
- Bitmap的大小与类型决定单张图片所需内存大小
- 图片获取的频率,哪些图片获取的更频繁,如果这样,你可能需要将固定的某些图片放入内存中,或者为多组Bitmap分配多个LruCache
- 质量与数量之间衡量,有时候后台加载高质量的图片的时候,我们存储低质量的图片更好一些。
Android没有指定固定的大小与格式用于适合所有应用程序,它完全由你自己去分析使用并提出合适的解决方案。缓存太小毫无用处并造成负担,缓存过大可能会引起java.lang.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);
}
注意:在这个例子中,八分之一应用程序内存分配给了我们的缓存。在一般或高分辨率设备上,至少为4M内存(32/8)。800*480分辨率的设备上的全屏GridView大概会使用1.5MB (800*480*4 bytes),因此会内存中会至少缓存2.5页图片。
当将Bitmap加入ImageView的时候,LruCache会先被检查。如果内存中存在此Bitmap,则会用它更新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用于获取Bitmap并将获取得Bitmap存入LruCache中:
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;
}
...
}
使用磁盘缓存(Use a Disk Cache)
内存缓存可以加速获取最近浏览过的Bitmap,但是你不能完全依赖内存缓存。像GridView这样携带大数据的组件很容易将内存缓存填满。而且,你的应用程序可能被外来程序打断如电话,此时背景线程可能被杀死,内存可能被销毁。一旦用户回过头的时候,你的应用又得重新处理每张图片了。这些情况发生的时候,我们可以使用磁盘缓存来保存已经加载过的图片,在内存缓存不在的情况下,降低图片加载时间。当然,从磁盘中提取图片比从内存中加载图片要慢一些并且需要为其开启额外线程用于处理。因为磁盘的读写是不可预测的。
注意:如果频繁的访问图片,ContentProvider可能是最好的地方来存储缓存图片。例如相册程序
例子:
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线程中被检查,但是磁盘缓存在背景线程中被检查。磁盘操作永远不能在主线程中发生。当图像被处理完全之后,Bitmap被添加到内存缓存与磁盘缓存中。
处理配置发生改变的情况(Handle Configuration Changes)
运行配置发生改变,例如屏幕旋转改变,会造成Android销毁Activity并重启带有新配置的Activity。如果当配置发生改变的时候,你想要避免再次处理你的图片以便用户能够有一个平滑和快速的体验效果。你可以使用上述提到的内存缓存机制,借助于中间组件Fragment的方法setRetainInstance(true)来保存内存换,然后实现将其传递到新的Activity中。当Activity被重建之后,通过Fragment获取内存缓存对象,并且从其中提取对象到ImageView中。
如下当应用程序配置发生改变的时候,通过Fragment重新获得LruCache对象
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);
}
}