注意:对于大多数情况下,我们建议您使用Glide库来获取,解码和显示应用程序中的位图。 Glide将处理与Android上的位图和其他图像相关的这些和其他任务的大部分复杂性抽象化。 有关使用和下载Glide的信息,请访问GitHub上的Glide存储库。
将单个位图加载到用户界面(UI)中非常简单,但是如果您需要一次加载更大的一组图像,情况会变得更加复杂。 在许多情况下(比如使用ListView,GridView或ViewPager等组件),屏幕上的图像总数和可能很快滚动到屏幕上的图像几乎是无限的。
通过回收子视图在屏幕外移动时,内存使用率可以通过像这样的组件来保持。 垃圾收集器也释放你的加载位图,假设你不保留任何长期的引用。 这一切都很好,但是为了保持流畅且快速的用户界面,您要避免每次回到屏幕上时连续处理这些图像。 内存和磁盘缓存通常可以在这里帮助,允许组件快速重新加载处理后的图像。
本教程将引导您使用内存和磁盘位图缓存来提高加载多个位图时UI的响应性和流畅性。
使用内存缓存
内存缓存提供对位图的快速访问,代价是占用宝贵的应用程序内存。 LruCache类(也可在支持库中用于API级别4)特别适合于缓存位图的任务,将最近引用的对象保留在强引用的LinkedHashMap中,并在缓存超过其之前排除最近最少使用的成员 指定的大小。
注意:过去,流行的内存缓存实现是SoftReference或WeakReference位图缓存,但不建议这样做。 从Android 2.3(API Level 9)开始,垃圾收集器更加积极地收集软/弱引用,这使得它们相当无效。 另外,在Android 3.0(API Level 11)之前,位图的备份数据被存储在本地存储器中,而不是以可预见的方式释放,这可能导致应用程序暂时超出其内存限制和崩溃。
为了选择适合LruCache的尺寸,应该考虑许多因素,例如:
- 你的活动和/或应用程序的其余部分的内存密集程度如何?
- 一次会在屏幕上显示多少图片?屏幕外又有多少图片准备好滚进屏幕进行显示?
- 什么是设备的屏幕大小和密度? Galaxy Nexus等额外高密度屏幕(xhdpi)设备需要更大的缓存才能保存与Nexus S(hdpi)等设备相同数量的图像。
- 什么尺寸和配置的位图,因此多少内存将分别占用?
- 图像被多久访问一次?有些人会比其他人更频繁地访问吗?如果是这样,也许你可能想要保持某些项目总是在内存中,甚至有不同的位图组的LruCache对象。
- 你能平衡质量和数量吗?有时,存储大量较低质量的位图会更有用,可能会在另一个后台任务中加载更高质量的版本。
没有适合所有应用程序的具体大小或公式,您需要分析您的使用情况并提出合适的解决方案。 缓存太小会导致额外的开销而没有任何好处,缓存太大可能会再次导致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);
}
注意:在这个例子中,八分之一的应用程序内存被分配给我们的缓存。 在普通/ hdpi设备上,这是最低约4MB(32/8)。 在800x480分辨率的设备上填充图像的全屏GridView将使用大约1.5MB(800 * 480 * 4字节),所以这会在内存中缓存大约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线程上。 图像处理完成后,最终的位图将被添加到内存和磁盘缓存中以供将来使用。
处理配置更改
运行时配置更改(如屏幕方向更改)会导致Android使用新配置销毁和重新启动运行活动(有关此行为的更多信息,请参阅处理运行时更改)。 您希望避免再次处理所有图像,以便用户在发生配置更改时拥有平稳快速的体验。
幸运的是,您在“使用内存缓存”部分中创建了一个漂亮的位图缓存。 可以使用通过调用setRetainInstance(true)保存的Fragment将此缓存传递到新的活动实例。 在活动重新创建之后,这个保留的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);
}
}
为了测试这个,试着旋转一个设备,保留和不保留碎片。 您应该注意到几乎没有滞后,因为当您保留缓存时,图像几乎立即从内存填充活动。 任何在内存缓存中找不到的图像都可以在磁盘缓存中找到,如果没有的话,就像往常一样处理。
翻译自:Caching Bitmaps