【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 ...
随机推荐
- JAVA 基础之Integer
jdk1.5后增加了自动拆箱和自动装箱特性.java的八种 byte,short,int,long,float,double,char,boolean基本类型和各自对应的包装类型的相互转化. 装箱指的 ...
- jQuery手风琴制作
jQuery手风琴制作 说起手风琴,想必大家应该都知道吧,简单的来说就是可以来回收缩的这么一个东西,接下来,我就给大家演示一下用jQuery制作一个手风琴菜单! 写jQuery前,我们需要引用一个jQ ...
- 如何在.Net中使用MongoDB
最近在研究mongodb,针对.net 中使用mongodb的文章要么是早期的驱动版本,要么资料很少,所以写个随笔记录一下 本文主要记录 1.什么是MongoDB 2.MongoDB windows ...
- 【转】典型的JavaScript面试题
问题1: 作用域(Scope) (function() { "use strict"; var a = b = 5; })(); console.log(b); 控制台(conso ...
- OpenStack(企业私有云)万里长征第二步——使用Fuel部署
一.前言 最近一直在使用DevStack来安装OpenStack,注意一直二字,部署了一遍又一遍,操作系统怕是安装了不下上百次,有时是为了验证新的方案,有时是安装出错,还有时是运行过程中出错.总之是碰 ...
- vue组件之间的通信以及如何在父组件中调用子组件的方法和属性
在Vue中组件实例之间的作用域是孤立的,以为不能直接在子组件上引用父组件的数据,同时父组件也不能直接使用子组件的数据 一.父组件利用props往子组件传输数据 父组件: <div> < ...
- python基础 - 01
python 变量名 在python中的变量命名,与其他语言大体相似,变量的命名规则如下: 变量名是数字.字母.下划线的任意组合 变量名的第一个字符不能是数字 系统的关键字不能设置为变量名 Ti ...
- VR全景:实体店与互联网的完美结合
VR元年已过,VR项目.VR创业潮转为理性,VR行业分为两个方向:硬件和内容.硬件又分为VR头显和辅助设备,内容又分为VR全景和VR虚拟内容,如游戏.娱乐.根据行业划分为VR+购物,VR+教育,VR+ ...
- weex入门
近期要做一个安卓端的原生应用程序.情况是这样的:需求方原先已经实现了网页,是一个工具类应用,大致作用是连接到他们公司生产的硬件,然后通手机与智能硬件通信来对硬件进行一系列控制.不过呢,这个网页先前是由 ...
- h5 + nginx + php 视频上传之突破文件大小受限的解决办法
一.环境: CentOS 6.8 nginx 1.8.0 php 7.0.10 二.背景 基于 nginx + php 的 h5 项目,上传视频的时候,如果视频太大,会上传失败. 三.正文 一份视频传 ...