[原创] 连载 4 - 深入讨论 Android 关于高效显示图片的问题 - 如何管理位图内存

  更加详细的说明,可以参阅如下官网地址:http://developer.android.com/training/building-graphics.html
  快速导航
  1. 如何高效的加载大位图。(如何解码大位图,避免超过每个应用允许使用的最大内存)http://yhz61010.iteye.com/blog/1848337
  2. 如何在非 UI 线程处理位图。(如何使用 AsyncTask 在后台线程处理位图及处理并发问题)http://yhz61010.iteye.com/blog/1848811
  3. 如何对位图进行缓存。(如何通过创建内存缓存和磁盘缓存来流畅的显示多张位图)http://yhz61010.iteye.com/blog/1849645
  4. 如何管理位图内存。(如何针对不同的 Android 版本管理位图内存)http://yhz61010.iteye.com/blog/1850232
  5. 如何在 UI 中显示位图。(如何通过 ViewPager 和 GridView 显示多张图片)http://yhz61010.iteye.com/blog/1852927
  如何管理位图内存?
  在http://yhz61010.iteye.com/blog/1849645一文中,我们已经讨论了如何对位图进行缓存处理。但还有一些具体的事情需要做,比如说如何更好的利用垃圾回收器和重用位图等。对于不同的 Android 版本,我们通常有不同的策略。
  在我们开始今天的学习之前,先来了解下 Android 是如何管理位图内存的:
  ・在 Android 2.2 (API level及之前版本,当进行垃圾回收时,会停止你的应用程序线程,这样就会产生一个延迟,从而会影响性能。在 Android 2.3 中添加了并发的垃圾回收器,这就意味着若位图没有被任何对象引用的话,它所占用的内存就会很快被回收。
  ・在 Android 2.3.3 (API level 10) 及之前版本,返回的位图数据是保存在程序内存中的,和位图本身所占的内存是区分开的,位图本身是保存在 Dalvik 堆中的。返回的位图数据虽然保存在内存中,但是所占用的内存并不会按照可预知的方式将其释放掉,从而导致的潜在问题就是应用程序很容易就超过内存限制并导致程序崩溃。从 Android 3.0 (API Level 11) 开始,返回的位图数据及位图本身都被保存在 Dalvik 堆中。
  下面我们就开始学习如何针对不同的 Android 版本进行位图内存优化。
  Android 2.3.3 及早先版本的内存管理
  在 Android 2.3.3 (API level 10)及早先版本中,建议使用 recycle() 进行内存管理。如果在你的程序中显示大量的位图数据,你很可能就会遇到 OutOfMemoryError 错误。recycle() 方法允许应用程序尽可能的回收内存。
  注意:只有当你确信位图不再被使用时,才能调用 recycle() 方法。若你已经调用了 recycle() 方法,但是之后你却再次访问了那些位图,那么你会看到如下错误提示:”Canvas: trying to use a recycled bitmap”。
  如下的代码片断为你演示了如何调用 recycle()。代码中使用了引用计数(保存在 mDisplayRefCount 和 mCacheRefCount 中)用来跟踪位图是正在被显示还是被保存在内存中。当满足如下条件时,位图将会被回收:
  ・引用计数变量 mDisplayRefCount 和 mCacheRefCount 的值都为 0。
  ・bitmap 对象不为 null,并且它还没被回收。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
synchronized (this) {
if (isDisplayed) {
mDisplayRefCount++;
mHasBeenDisplayed = true;
} else {
mDisplayRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
public void setIsCached(boolean isCached) {
synchronized (this) {
if (isCached) {
mCacheRefCount++;
} else {
mCacheRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
private synchronized void checkState() {
// If the drawable cache and display ref counts = 0, and this drawable
// has been displayed, then recycle.
if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
&& hasValidBitmap()) {
getBitmap().recycle();
}
}
private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}

  Android 3.0 及更高版本的内存管理
  Android 3.0 (API Level 11) 引进了 BitmapFactory.Options.inBitmap 字段,如果设置了该属性,那么当使用了带有该 Options 参数的 decode 方法在加载内容时,decode 方法会尝试重用一个已经存在的位图。这就意味着位图内存已经被重用了,从而性能得到了改善,并且移除了内存的分配和解除分配。下面是一些使用 inBitmap 的注意事项:
  ・重用的位图大小必须和源位图大小相同(这样才能保证它们占用相同的内存),并且位图的格式应该是 JPEG 或 PNG(无论是作为资源形式还是数据流形式)。
  ・若设置了重用的位图的 Bitmap.Config 配置,则需要重写 inPreferredConfig 方法。
  ・你应该总是使用位图的解码方法,因为我们不能认为重用的位图是可用的(例如,若位图大小不匹配,就无法保证位图可重用)。
  保存位图便于以后使用
  如下的代码片断为你演示了位图是如何被保存便于以后使用的。当程序运行在 Android 3.0 或更高版本中时,位图将会从 LruCache 中被移除,指向该位图的 soft reference 将被存放在一个 HashSet 中,以便于之后在 inBitmap 中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
HashSet<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;
// If you're running on Honeycomb or newer, create
// a HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
mReusableBitmaps = new HashSet<SoftReference<Bitmap>>();
}
mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
// Notify the removed entry that is no longer being cached.
@Override
protected void entryRemoved(boolean evicted, String key,
BitmapDrawable oldValue, BitmapDrawable newValue) {
if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
// The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache.
((RecyclingBitmapDrawable) oldValue).setIsCached(false);
} else {
// The removed entry is a standard BitmapDrawable.
if (Utils.hasHoneycomb()) {
// We're running on Honeycomb or later, so add the bitmap
// to a SoftReference set for possible use with inBitmap later.
mReusableBitmaps.add
(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
}
}
....
}

  使用一个已经存在的位图
  我们可以在程序中使用解码方法来判断是否有可重用的位图。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Bitmap decodeSampledBitmapFromFile(String filename,
int reqWidth, int reqHeight, ImageCache cache) {
final BitmapFactory.Options options = new BitmapFactory.Options();
...
BitmapFactory.decodeFile(filename, options);
...
// If we're running on Honeycomb or newer, try to use inBitmap.
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
...
return BitmapFactory.decodeFile(filename, options);
}

  上述代码中的 addInBitmapOptions() 方法实现如下。该方法会查找一个已经存在的位图,将并该位图设置给 inBitmap 属性。注意,只有当找到了一个匹配的位图时(我们永远不能假定这种匹配一定能被找到),该方法才会为 inBitmap 赋值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
private static void addInBitmapOptions(BitmapFactory.Options options,
ImageCache cache) {
// inBitmap only works with mutable bitmaps, so force the decoder to
// return mutable bitmaps.
options.inMutable = true;
if (cache != null) {
// Try to find a bitmap to use for inBitmap.
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
// If a suitable bitmap has been found, set it as the value of
// inBitmap.
options.inBitmap = inBitmap;
}
}
}
// This method iterates through the reusable bitmaps, looking for one
// to use for inBitmap:
protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) {
Bitmap bitmap = null;
if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) {
final Iterator<SoftReference<Bitmap>> iterator
= mReusableBitmaps.iterator();
Bitmap item;
while (iterator.hasNext()) {
item = iterator.next().get();
if (null != item && item.isMutable()) {
// Check to see it the item can be used for inBitmap.
if (canUseForInBitmap(item, options)) {
bitmap = item;
// Remove from reusable set so it can't be used again.
iterator.remove();
break;
}
} else {
// Remove from the set if the reference has been cleared.
iterator.remove();
}
}
}
return bitmap;
}

  最后,为了能给 inBitmap 赋值,我们还需要使用如下方法来判断是否有一个可选的位图满足指定的位图大小:

1
2
3
4
5
6
7
8
private static boolean canUseForInBitmap(
Bitmap candidate, BitmapFactory.Options targetOptions) {
int width = targetOptions.outWidth / targetOptions.inSampleSize;
int height = targetOptions.outHeight / targetOptions.inSampleSize;
// Returns true if "candidate" can be used for inBitmap re-use with
// "targetOptions".
return candidate.getWidth() == width && candidate.getHeight() == height;
}