【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 ...
随机推荐
- ShadowBroker释放的NSA工具中Esteemaudit漏洞复现过程
没有时间测试呢,朋友们都成功复现,放上网盘地址:https://github.com/x0rz/EQGRP 近日臭名昭著的方程式组织工具包再次被公开,TheShadowBrokers在steemit. ...
- Linux-vim学习入门
1.前言 vi/vim是linux中很重要的文本编辑器.我第一次使用这个编辑器时,很不习惯,甚至都不知道如何移动光标和插入字符.慢慢地经过学习,才知道如何使用vi/vim. vi/vi ...
- 基于express+mongodb+pug的博客系统——pug篇
很久之前就想自己搭一个博客了,最开始用hexo+github,但是换电脑后总是有些麻烦.后来使用WordPress,但是用WordPress总觉得没什么技术含量,前后端都是人家写好的,而且买的垃圾虚拟 ...
- 使用Redis实现分布式锁
在天猫.京东.苏宁等等电商网站上有很多秒杀活动,例如在某一个时刻抢购一个原价1999现在秒杀价只要999的手机时,会迎来一个用户请求的高峰期,可能会有几十万几百万的并发量,来抢这个手机,在高并发的情形 ...
- Vue.js 实战总结
最近在某个项目中用到了Vue.js,从上手做开发到项目发布,一步步踩了不少坑.本文试图总结过去一个多月使用Vue.js中的一些经验,也算是一点心得体会吧,拿出来与大家分享,欢迎多多交流. Vue.js ...
- 又是一个MVP+RxJava+Retrofit的干货集中营
欢迎访问我的个人博客 ,未经允许不得转发 前言 今天想要与大家一起分享的是近一个星期内开发的一个新app--干货集中营客户端,其实网上已经有许多类似的项目,代码家也在他的干货集中营中推荐了几款优秀的作 ...
- RegExp(正则表达式)常用知识点小结
原文地址:→看过来 正则表达式用到的地方很多,但是每次很久不用就全忘光了,每次都要重新看一遍文档,为了节省时间,把它的一些基本要点画总结在一张图片中,这样方便以后查看. PS:细节的东西还是需要看详细 ...
- 安卓TextView限定行数最大值,点击按钮显示所有内容
问题展示 如上图所示,在普通的TextView中,要求: 最多显示3行 超过三行显示展开按钮 且点击展开按钮显示完整内容 这个需求看似简单,但解决起来会遇到两个较为棘手的问题:1,如何判断是否填满了前 ...
- 关于关系型数据库(MySQL)的一些概念
主键:关系型数据库中的一条记录中有若干个属性,若其中某一个属性组(注意是组)能唯一标识一条记录, 该属性组就可以成为一个主键,主键不允许为空,主键只能有同一个 外键:如果一个表的某个属性是另一个表的主 ...
- Windows 自动获取IP脚本
@echo off echo 正在自动获取IP地址.... set 连接名称=以太网 netsh interface ip set address name = "%连接名称%" ...