【Android Developers Training】 58. 缓存位图
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好。
原文链接:http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
向你的应用中加载一个单一的位图是很直接的行为,然而当你需要一次性加载一组图像的大集合时,事情会变得更加复杂。在很多情况下(比如对于ListView,GridView或者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. 缓存位图的更多相关文章
- 【Android Developers Training】 55. 序言:高效显示位图
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
- 【Android Developers Training】 60. 在你的UI中显示位图
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
- 【Android Developers Training】 59. 管理图片存储
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
- 【Android Developers Training】 7. 添加Action Buttons
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
- 【Android Developers Training】 3. 构建一个简单UI
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
- 【Android Developers Training】 2. 运行你的应用
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
- 【Android Developers Training】 95. 创建一个同步适配器
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
- 【Android Developers Training】 91. 解决云储存冲突
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
- 【Android Developers Training】 86. 基于连接类型修改您的下载模式
注:本文翻译自Google官方的Android Developers Training文档,译者技术一般,由于喜爱安卓而产生了翻译的念头,纯属个人兴趣爱好. 原文链接:http://developer ...
随机推荐
- nginx 日志分割(简单、全面)
Nginx 日志分割 因业务需要做了简单的Nginx 日志分割, 第1章 详细配置如下. #建议在mkdir /home/shell -p 专门写shell 脚本位置 root@localhost ...
- Hive 桶的分区
(一).桶的概念: 对于每一个表(table)或者分区, Hive可以进一步组织成桶(没有分区能分桶吗?),也就是说桶是更为细粒度的数据范围划分.Hive也是 针对某一列进行桶的组织.Hive采用对列 ...
- Java Garbage Collectors
Generational Collectors (分代收集器) GC algos optimised based on two hypotheses / observations: Most obje ...
- 微信小程序对医疗创业的启示,“餐饮+微信小程序”的猜想
一:微信小程序对医疗创业的启示:如何用完即走 仔细看了张小龙在28日微信公开课上发布小程序时的演讲全文,我觉得对解决当下医疗创业的困惑有着巨大的启发.没准还能开辟新的未来. 张小龙对小程序精髓的阐释是 ...
- Java基础知识二次学习--第八章 流
第八章 流 时间:2017年4月28日11:03:07~2017年4月28日11:41:54 章节:08章_01节 视频长度:21:15 内容:IO初步 心得: 所有的流在java.io包里面 定 ...
- 基于react的简单TODOList
前段时间看了下react,写个栗子记录 页面效果如下 效果:展示代办事件,正文加了删除线的是已经完成的,未加横杠的是未完成的, 交互:1.在input里面输入新添加的内容,点击Add按钮添加代办事件 ...
- THREE笛卡尔右手坐标系详解
1,正常的笛卡尔右手坐标系,以屏幕右方为+X轴,屏幕上方为+Y轴,垂直屏幕向外为+Z轴,如下图,xy轴组成的平面为屏幕面 但由于THREE里的相机并不总是从屏幕正前方视角,还可以设置坐标系任意一个轴为 ...
- Github+Hexo,搭建专有博客
前言 记得从大二开始,就一直想搭个专属网站,当时使劲抠页面[前端页面是从QQ空间抠的,现在想抠估计没这么容易了],写代码,忙活半天才把程序弄好. 可惜最终项目还是没上线,因为当时有两问题绕不开 需要购 ...
- 全景智慧城市常诚——没接触过VR全景的你就是目前VR最大的新闻
据调查,自2015年开始,VR(虚拟现实)技术在传媒行业中的应用呈现井喷式增长,各大国际主流媒体纷纷在新闻报道中使用VR技术.国内运用VR报道新闻最早在2015年12月,财新网利用VR技术对深圳山体垮 ...
- 一天搞定CSS:文本text--05
1.文本体系 2.文本各属性取值 说明: 每一个属性后面的分支是属性值,以及对属性值的说明. 比如text-align- - - -有3个取值:left,center,right 3.空格大小 4.代 ...