注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。

原文链接:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html


向你的应用中加载一个单一的位图是很直接的行为,然而当你需要一次性加载一组图像的大集合时,事情会变得更加复杂。在很多情况下(比如对于ListViewGridView或者ViewPager),屏幕上显示的图片以及会因加载动作而进入屏幕的图片,这两者的总数加起来是无法限制的。

通过对移除屏幕区域的子View进行回收,可以让这类组件内存使用降低下来。垃圾回收器也会对那些假定你将不再需要的引用对象进行回收和释放。这些措施都很好,但是为了保持流畅地和快速地加载UI,你会希望避免多次连续地处理这些图片,当它们回到屏幕区域中来时。一个存储或磁盘缓存可以在这方面提供帮助,它可以让组件迅速的重新加载处理过的图片。

这节课将会教你使用一个存储和磁盘缓存,来提升你的UI加载多个图片时的响应和流畅性。


一). 使用一个内存缓存

一个内存缓存提供了快速访问位图的方法,但它的代价是需要消耗掉珍贵的应用内存。LruCache类(在Support Library也有,可以支持到API Level 4及以上的平台)对于缓存图片来说尤其适合,它能将最近引用的对象存储在一个基于强引用的LinkedHashMap中,并且在缓存超出它的特定大小后,将最近最迟被引用的对象去除。

Note:

在过去,一个流行的内存缓存实现是SoftReference或者WeakReference的位图缓存,然而,这并不是推荐的实现方法。从Android 2.3(API Level 9)开始,垃圾回收器对于软引用和弱引用的回收变得更加地激进,从而使得它们的效用正在下降。从Android 3.0(API Level 11)开始,存储于本机内存的位图数据并不是以一个可预测的形式释放的,这就有潜在的可能性导致一个应用超出它的内存限制进而崩溃。

为了为一个LruCache选择合适的大小,一些因素需要考量,例如:

  • 你的activity或应用剩余的存储压力是如何的?
  • 同一时间有多少应用显示在屏幕上?有多少需要准备就绪显示到屏幕上?
  • 设备的屏幕的尺寸和密度的大小是多少?一个极高密度的屏幕(xhdpi)的设备(比如Galaxy Nexus)可能相对于其他比如hdpi的设备(比如Nexus S)需要更大的缓存来容纳同样数量的照片。
  • 位图文件的尺寸和属性是怎样的,需要消耗多少大的内存空间?
  • 图片被访问的频率高不高?有没有一些图片被访问你的频率比其它的要高?如果有,也许你会期望让这些项目一直保留在内存或者为不同被访问频率的图片设置多组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);
}

Note:

在这个例子中,八分之一的应用内存被分配给了我们的缓存。在一个标准或hdpi的设备上,这大约为4MB左右(32/8)。一个全屏的GridView,在一个分辨率为800x480的设备上,充满图片之后,会使用掉大约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这样的组件其较大的数据集可以迅速填充内存缓存。同时,你的应用可能会被另一个事务打断,如一个来电,此时在后台中,它可能会被杀掉,这样的话内存缓存就被销毁了。一旦这个用户恢复了,你的应用不得不重新处理这些图片。

一个磁盘缓存可以在这种情况下发挥效用,它能保持处理过的位图文件,并减少在内存缓存中不再可以获得的加载时间。当然,从磁盘获取图片比从内存获取图片要慢,由于磁盘读写的速度有很多不确定性,故应该在后台线程中执行。

Note:

一个ContentProvider是一个比较合适的存储缓存图片的地方,对于那些访问频率较高的图片来说,例如在图库的应用中。

下面的代码使用了DiskLruCache的实现,它来自于Android source。并且添加到内存缓存的代码中,更新其功能:

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:

因为初始化磁盘缓存也需要磁盘操作所以它也不能再主线程中执行。然而,这其实意味着缓存有可能在还未初始化的时候就被访问了。为了解决这个问题,在上面的代码实现中,一个信号量(lock)保证了应用会在初始化完成之后才去读取缓存。

虽然内存缓存在UI线程中检查,磁盘缓存是在后台线程中检查。磁盘操作不应该发生在UI线程中执行。当图片处理完成了,最后位图将会同时添加到内存和磁盘缓存中,以备将来使用。


三). 处理配置变更

运行时的配置变更,如屏幕方向变化,会导致Android销毁当前activity,并以新的配置重启activity(可以阅读:Handling Runtime Changes)。你一定希望避免重复处理图像,这样的话用户就能在配置改变时,拥有平滑快速地使用体验。

幸运的是,你在之前的章节中,已经拥有了一个很出色的图片内存缓存了。这个缓存可以通过使用一个Fragment(该Fragment通过调用setRetainInstance(true)将其自身保留),传递给新的activity实例。在activity重新创建之后,这个保留的Fragment就完成了重新依附(reattach),同时你获得了现有缓存对象的访问,允许图片快速提取并填充到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);
}
}

要测试这段代码,尝试分别在保留Fragment和不保留Fragment的情况下旋转设备。你应该能注意到当保留了缓存时,图片填充到activity时几乎没有延迟。那些在内存缓存中找不到的图片一般都会在磁盘缓存中找到,如果找不到,这些图片就会像平常一样处理。

【Android Developers Training】 58. 缓存位图的更多相关文章

  1. 【Android Developers Training】 55. 序言:高效显示位图

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  2. 【Android Developers Training】 60. 在你的UI中显示位图

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  3. 【Android Developers Training】 59. 管理图片存储

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  4. 【Android Developers Training】 7. 添加Action Buttons

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  5. 【Android Developers Training】 3. 构建一个简单UI

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  6. 【Android Developers Training】 2. 运行你的应用

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  7. 【Android Developers Training】 95. 创建一个同步适配器

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  8. 【Android Developers Training】 91. 解决云储存冲突

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

  9. 【Android Developers Training】 86. 基于连接类型修改您的下载模式

    注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...

随机推荐

  1. iOS之copy、strong使用,block特性

    身边一同事,我印象在过去三个月,有两次因为使用“copy”修饰UIKit控件的属性,导致程序崩溃.他还一本正经的说我以前一直使用copy. 好了,到这里我们就不得不说说什么时候使用copy.我的印象中 ...

  2. QT修改UI和源码后,程序页面还是原来页面的解决方法

    发生原因: 移植了一个Qt程序在修改完QT的cpp源码和UI界面布局等内容后,重启点击QT Creator左下角的运行(Ctrl+R)编译程序并启动后,程序界面及修改内容并没有改变. 解决方式: 找到 ...

  3. 016 多对多关联映射 单向(many-to-many)

    一般的设计中,多对多关联映射,需要一个中间表 Hibernate会自动生成中间表 Hibernate使用many-to-many标签来表示多对多的关联 多对多的关联映射,在实体类中,跟一对多一样,也是 ...

  4. List在执行remove方法不能删除指定的对象

    我们根据List中的源码分析, remove方法的原理: public boolean remove(Object o){      if(o ==null) {           for(inti ...

  5. IIS无法启动,应用程序池自动关闭,应用程序池XXXX将被自动禁用 解决方案之一

    最近,新任职的公司有一台测试服务(Windows Server 2008 R2 + IIS6.1)器因突然停电,造成了意外“损伤”.来电后再次开机,发现IIS里大部分的网站均打不开.均为如下(图01) ...

  6. git底层原理(二)

    git对象模型 在git系统中有四种类型的对象,所有的Git操作都是基于这四种类型的对象:"blob":这种对象用来保存文件的内容."tree":可以理解成一个 ...

  7. SQL SERVER大话存储结构(4)_复合索引与包含索引

              索引这块从存储结构来分,有2大类,聚集索引和非聚集索引,而非聚集索引在堆表或者在聚集索引表都会对其 键值有所影响,这块可以详细查看本系列第二篇文章:SQL SERVER大话存储结构 ...

  8. js的event事件

    一 .  焦点:使浏览器能够区分区分用户输入的对象,当一个元素有焦点的时候,那么他就可以接收用户的输入. 我们可以通过一些方式给元素设置焦点 1.点击 2.tab   3.js 不是所有元素都能够接受 ...

  9. 一步一步实现android studio代码上传到github。

    本文只注重代码上传能成功就好,不解释什么是git什么事github,git有什么优势. 1,先创建一个android应用, 第二步:创建github账户 和 安装git.网上的文章多如牛毛.唯一要说的 ...

  10. css如何让div和页面等高?

    我们都知道,只要是block状态的标签,宽度和父级等宽,或者设置宽度100%也可以等宽,但设置高度100%是不管用的,那么如何让标签和页面等高呢,除了用js去动态计算设置高度值,用css也可以 只要将 ...