注意:对于大多数情况,我们建议您使用
Glide
库来获取,解码和显示应用中的位图。Glide
在处理与在Android
上使用位图和其他图像相关的这些和其他任务时,大部分复杂性都是抽象的。 有关使用和下载Glide
的信息,请访问GitHub
上的Glide
存储库。
将单个位图加载到用户界面(UI)中非常简单,但是如果需要一次加载更多的图像,事情会变得更加复杂。 在许多情况下(例如使用ListView
,GridView
或ViewPager
等组件),屏幕上的图像总数以及可能很快滚动到屏幕上的图像基本上是无限的。
通过在子屏幕移动时回收子视图,可以使用这样的组件来降低内存使用率。 垃圾收集器还可以释放加载的位图,假设您没有保留任何长期存在的引用。 这一切都很好,但为了保持流畅和快速加载的UI,您希望避免每次它们回到屏幕时不断处理这些图像。 内存和磁盘缓存通常可以在这里提供帮助,允许组件快速重新加载已处理的映像。
本课程将指导您使用内存和磁盘位图缓存来提高加载多个位图时UI的响应性和流动性。
使用内存缓存
内存缓存以占用宝贵的应用程序内存为代价提供对位图的快速访问。 LruCache
类(也可在支持库中使用,可用于API级别4)特别适合缓存位图的任务,将最近引用的对象保存在强引用的LinkedHashMap
中,并在缓存超过其之前驱逐最近最少使用的成员 指定大小。
注意:过去,流行的内存缓存实现是SoftReference或WeakReference位图缓存,但不建议这样做。 从Android 2.3(API级别9)开始,垃圾收集器更积极地收集软/弱引用,这使得它们相当无效。 此外,在Android 3.0(API Level 11)之前,位图的后备数据存储在本机内存中,而该内存未以可预测的方式释放,可能导致应用程序短暂超出其内存限制并崩溃。
为了为LruCache
选择合适的尺寸,应考虑许多因素,例如:
- 您的其他活动和/或应用程序的内存密集程度如何?
- 一次会在屏幕上显示多少张图片? 有多少需要准备就绪?
- 屏幕尺寸和设备密度是多少? 与像Nexus S(hdpi)这样的设备相比,像Galaxy Nexus这样的超高密度屏幕(xhdpi)设备需要更大的缓存才能在内存中保存相同数量的图像。
- 位图的尺寸和配置是多少,因此每个占用多少内存?
- 图像访问的频率如何? 有些人会比其他人更频繁地访问吗? 如果是这样,也许您可能希望将某些项目始终保留在内存中,甚至可以为不同的位图组使用多个LruCache对象。
- 你能平衡质量和数量吗? 有时,存储大量较低质量的位图可能更有用,可能会在另一个后台任务中加载更高质量的版本。
没有适合所有应用的特定尺寸或配方,您可以自行分析使用情况并提出合适的解决方案。 太小的缓存会导致额外的开销而没有任何好处,缓存太大可能会再次导致java.lang.OutOfMemory异常,并使您的应用程序的其余部分可以使用。
以下是为位图设置LruCache的示例:
private LruCache memoryCache;
@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;
memoryCache = new LruCache(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) {
memoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return memoryCache.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 {
...
// 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 diskLruCache;
private final Object diskCacheLock = new Object();
private boolean diskCacheStarting = 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 {
@Override
protected Void doInBackground(File... params) {
synchronized (diskCacheLock) {
File cacheDir = params[0];
diskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
diskCacheStarting = false; // Finished initialization
diskCacheLock.notifyAll(); // Wake any waiting threads
}
return null;
}
}
class BitmapWorkerTask extends AsyncTask {
...
// 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) {
memoryCache.put(key, bitmap);
}
// Also add to disk cache
synchronized (diskCacheLock) {
if (diskLruCache != null && diskLruCache.get(key) == null) {
diskLruCache.put(key, bitmap);
}
}
}
public Bitmap getBitmapFromDiskCache(String key) {
synchronized (diskCacheLock) {
// Wait while disk cache is started from background thread
while (diskCacheStarting) {
try {
diskCacheLock.wait();
} catch (InterruptedException e) {}
}
if (diskLruCache != null) {
return diskLruCache.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使用新配置销毁并重新启动运行活动(有关此行为的详细信息,请参阅处理运行时更改)。 您希望避免再次处理所有图像,以便在发生配置更改时用户获得流畅,快速的体验。
幸运的是,您在“使用内存缓存”部分中构建了一个很好的位图内存缓存。 可以使用Fragment将此缓存传递给新的活动实例,Fragment通过调用setRetainInstance(true)来保留。 重新创建活动后,将重新附加此保留的Fragment,您可以访问现有的缓存对象,从而可以快速获取图像并将其重新填充到ImageView对象中。
以下是使用片段在配置更改中保留LruCache对象的示例:
private LruCache memoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
RetainFragment retainFragment =
RetainFragment.findOrCreateRetainFragment(getFragmentManager());
memoryCache = retainFragment.retainedCache;
if (memoryCache == null) {
memoryCache = new LruCache(cacheSize) {
... // Initialize cache here as usual
}
retainFragment.retainedCache = memoryCache;
}
...
}
class RetainFragment extends Fragment {
private static final String TAG = "RetainFragment";
public LruCache retainedCache;
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);
}
}
要对此进行测试,请尝试在保留片段和不保留片段的情况下旋转设备。 您应该注意到很少甚至没有延迟,因为当您保留缓存时,图像几乎立即从内存中填充活动。 希望在磁盘高速缓存中找到内存高速缓存中未找到的任何映像,否则,它们将照常处理。