转载声明:Ryan的博客文章欢迎您的转载,但在转载的同时,请注明文章的来源出处,不胜感激! :-)
http://blog.csdn.net/floodingfire/article/details/8171556
译者:Ryan Hoo
来源:https://developer.android.com/develop/index.html
译者按: 在Google最新的文档中,提供了一系列含金量相当高的教程。因为种种原因而鲜为人知,真是可惜!Ryan将会细心整理,将之翻译成中文,希望对开发者有所帮助。
本系列是Google关于展示大Bitmap(位图)的官方演示,可以有效的解决内存限制,更加有效的加载并显示图片,同时避免让人头疼的OOM(Out Of Memory)。
-------------------------------------------------------------------------------------
译文:
加载一个Bitmap(位图)到你的UI界面是非常简单的,但是如果你要一次加载一大批,事情就变得复杂多了。在大多数的情况下(如ListView、GridView或者ViewPager这样的组件),屏幕上的图片以及马上要在滚动到屏幕上显示的图片的总量,在本质上是不受限制的。
像这样的组件在子视图移出屏幕后会进行视图回收,内存使用仍被保留。但假设你不保留任何长期存活的引用,垃圾回收器也会释放你所加载的Bitmap。这自然再好不过了,但是为了保持流畅且快速加载的UI,你要避免继续在图片回到屏幕上的时候重新处理。使用内存和硬盘缓存通常能解决这个问题,使用缓存允许组件快速加载并处理图片。
这节课将带你使用内存和硬盘缓存Bitmap,以在加载多个Bitmap的时候提升UI的响应性和流畅性。
使用内存缓存
以牺牲宝贵的应用内存为代价,内存缓存提供了快速的Bitmap访问方式。LruCache类(可以在Support Library中获取并支持到API Level 4以上,即1.6版本以上)是非常适合用作缓存Bitmap任务的,它将最近被引用到的对象存储在一个强引用的LinkedHashMap中,并且在缓存超过了指定大小之后将最近不常使用的对象释放掉。
注意:以前有一个非常流行的内存缓存实现是SoftReference(软引用)或者WeakReference(弱引用)的Bitmap缓存方案,然而现在已经不推荐使用了。自Android2.3版本(API Level 9)开始,垃圾回收器更着重于对软/弱引用的回收,这使得上述的方案相当无效。此外,Android 3.0(API Level 11)之前的版本中,Bitmap的备份数据直接存储在本地内存中并以一种不可预测的方式从内存中释放,很可能短暂性的引起程序超出内存限制而崩溃。
为了给LruCache选择一个合适的大小,要考虑到很多原因,例如:
- 其他的Activity(活动)和(或)程序都是很耗费内存的吗?
- 屏幕上一次会显示多少图片?有多少图片将在屏幕上显示?
- 设备的屏幕大小和密度是多少?一个超高清屏幕(xhdpi)的设备如Galaxy Nexus,相比Nexus S(hdpi)来说,缓存同样数量的图片需要更大的缓存空间。
- Bitmap的尺寸、配置以及每张图片需要占用多少内存?
- 图片的访问是否频繁?有些会比其他的更加被频繁的访问到吗?如果是这样,也许你需要将某些图片一直保留在内存中,甚至需要多个LruCache对象分配给不同组的Bitmap。
- 你能平衡图片的质量和数量么?有的时候存储大量低质量的图片更加有用,然后可以在后台任务中加载另一个高质量版本的图片。
对于设置缓存大小,并没有适用于所有应用的规范,它取决于你在内存使用分析后给出的合适的解决方案。缓存空间太小并无益处,反而会引起额外的开销,而太大了又可能再次引起java.lang.OutOfMemory异常或只留下很小的空间给应用的其他程序运行。
这里有一个设置Bitmap的LruCache示例:
01 |
private LruCache<String, Bitmap> mMemoryCache; |
04 |
protected void onCreate(Bundle savedInstanceState) { |
08 |
final int memClass = ((ActivityManager) context.getSystemService( |
09 |
Context.ACTIVITY_SERVICE)).getMemoryClass(); |
12 |
final int cacheSize = 1024 * 1024 * memClass / 8 ; |
14 |
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) { |
16 |
protected int sizeOf(String key, Bitmap bitmap) { |
18 |
return bitmap.getByteCount(); |
24 |
public void addBitmapToMemoryCache(String key, Bitmap bitmap) { |
25 |
if (getBitmapFromMemCache(key) == null ) { |
26 |
mMemoryCache.put(key, bitmap); |
30 |
public Bitmap getBitmapFromMemCache(String key) { |
31 |
return mMemoryCache.get(key); |
注意:在这个例子中,1/8的应用内存被分配给缓存。在一个普通的/hdpi设备上最低也在4M左右(32/8)。一个分辨率为800*480的设备上,全屏的填满图片的GridView占用的内存约1.5M(800*480*4字节),因此这个大小的内存可以缓存2.5页左右的图片。
当加载一个Bitmap到ImageView中,先要检查LruCache。如果有相应的数据,则立即用来更新ImageView,否则将启动后台线程来处理这个图片。
01 |
public void loadBitmap( int resId, ImageView imageView) { |
02 |
final String imageKey = String.valueOf(resId); |
04 |
final Bitmap bitmap = getBitmapFromMemCache(imageKey); |
06 |
mImageView.setImageBitmap(bitmap); |
08 |
mImageView.setImageResource(R.drawable.image_placeholder); |
09 |
BitmapWorkerTask task = new BitmapWorkerTask(mImageView); |
BitmapWorkerTask也需要更新内存中的数据:
01 |
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { |
05 |
protected Bitmap doInBackground(Integer... params) { |
06 |
final Bitmap bitmap = decodeSampledBitmapFromResource( |
07 |
getResources(), params[ 0 ], 100 , 100 )); |
08 |
addBitmapToMemoryCache(String.valueOf(params[ 0 ]), bitmap); |
使用硬盘缓存
一个内存缓存对加速访问最近浏览过的Bitmap非常有帮助,但是你不能局限于内存中的可用图片。GridView这样有着更大的数据集的组件可以很轻易消耗掉内存缓存。你的应用有可能在执行其他任务(如打电话)的时候被打断,并且在后台的任务有可能被杀死或者缓存被释放。一旦用户重新聚焦(resume)到你的应用,你得再次处理每一张图片。
在这种情况下,硬盘缓存可以用来存储Bitmap并在图片被内存缓存释放后减小图片加载的时间(次数)。当然,从硬盘加载图片比内存要慢,并且应该在后台线程进行,因为硬盘读取的时间是不可预知的。
注意:如果访问图片的次数非常频繁,那么ContentProvider可能更适合用来存储缓存图片,例如Image Gallery这样的应用程序。
这个类中的示例代码使用DiskLruCache(来自Android源码)实现。在示例代码中,除了已有的内存缓存,还添加了硬盘缓存。
01 |
private DiskLruCache mDiskLruCache; |
02 |
private final Object mDiskCacheLock = new Object(); |
03 |
private boolean mDiskCacheStarting = true ; |
04 |
private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10 ; |
05 |
private static final String DISK_CACHE_SUBDIR = "thumbnails" ; |
08 |
protected void onCreate(Bundle savedInstanceState) { |
13 |
File cacheDir = getDiskCacheDir( this , DISK_CACHE_SUBDIR); |
14 |
new InitDiskCacheTask().execute(cacheDir); |
18 |
class InitDiskCacheTask extends AsyncTask<File, Void, Void> { |
20 |
protected Void doInBackground(File... params) { |
21 |
synchronized (mDiskCacheLock) { |
22 |
File cacheDir = params[ 0 ]; |
23 |
mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE); |
24 |
mDiskCacheStarting = false ; |
25 |
mDiskCacheLock.notifyAll(); |
31 |
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> { |
35 |
protected Bitmap doInBackground(Integer... params) { |
36 |
final String imageKey = String.valueOf(params[ 0 ]); |
39 |
Bitmap bitmap = getBitmapFromDiskCache(imageKey); |
43 |
final Bitmap bitmap = decodeSampledBitmapFromResource( |
44 |
getResources(), params[ 0 ], 100 , 100 )); |