管理Bitmap的内存使用

编写:kesenhoo - 原文:http://developer.android.com/training/displaying-bitmaps/manage-memory.html

这节课将作为缓存Bitmaps课程的进一步延伸。为了优化垃圾回收机制与Bitmap的重用,我们还有一些特定的事情可以做。 同时根据Android的不同版本,推荐的策略会有所差异。DisplayingBitmaps的示例程序会演示如何设计我们的程序,使得它能够在不同的Android平台上高效地运行.

为了给这节课奠定基础,我们首先要知道Android管理Bitmap内存使用的演变进程:

  • 在Android 2.2 (API level 8)以及之前,当垃圾回收发生时,应用的线程是会被暂停的,这会导致一个延迟滞后,并降低系统效率。 从Android 2.3开始,添加了并发垃圾回收的机制, 这意味着在一个Bitmap不再被引用之后,它所占用的内存会被立即回收。
  • 在Android 2.3.3 (API level 10)以及之前, 一个Bitmap的像素级数据(pixel data)是存放在Native内存空间中的。 这些数据与Bitmap本身是隔离的,Bitmap本身被存放在Dalvik堆中。我们无法预测在Native内存中的像素级数据何时会被释放,这意味着程序容易超过它的内存限制并且崩溃。 自Android 3.0 (API Level 11)开始, 像素级数据则是与Bitmap本身一起存放在Dalvik堆中。

下面会介绍如何在不同的Android版本上优化Bitmap内存使用。

管理Android 2.3.3及以下版本的内存使用

在Android 2.3.3 (API level 10) 以及更低版本上,推荐使用recycle()方法。 如果在应用中显示了大量的Bitmap数据,我们很可能会遇到OutOfMemoryError的错误。 recycle()方法可以使得程序更快的释放内存。

Caution:只有当我们确定这个Bitmap不再需要用到的时候才应该使用recycle()。在执行recycle()方法之后,如果尝试绘制这个Bitmap, 我们将得到"Canvas: trying to use a recycled bitmap"的错误提示。

下面的代码片段演示了使用recycle()的例子。它使用了引用计数的方法(mDisplayRefCountmCacheRefCount)来追踪一个Bitmap目前是否有被显示或者是在缓存中。并且在下面列举的条件满足时,回收Bitmap:

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

管理Android 3.0及其以上版本的内存

从Android 3.0 (API Level 11)开始,引进了BitmapFactory.Options.inBitmap字段。 如果使用了这个设置字段,decode方法会在加载Bitmap数据的时候去重用已经存在的Bitmap。这意味着Bitmap的内存是被重新利用的,这样可以提升性能,并且减少了内存的分配与回收。然而,使用inBitmap有一些限制,特别是在Android 4.4 (API level 19)之前,只有同等大小的位图才可以被重用。详情请查看inBitmap文档

保存Bitmap供以后使用

下面演示了如何将一个已经存在的Bitmap存放起来以便后续使用。当一个应用运行在Android 3.0或者更高的平台上并且Bitmap从LruCache中移除时,Bitmap的一个软引用会被存放在Hashset中,这样便于之后可能被inBitmap重用:

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

使用已经存在的Bitmap

在运行的程序中,decode方法会检查看是否存在可重用的Bitmap。 例如:

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

下面的代码是上述代码片段中,addInBitmapOptions()方法的具体实现。 它会为inBitmap查找一个已经存在的Bitmap,并将它设置为inBitmap的值。 注意这个方法只有在找到合适且可重用的Bitmap时才会赋值给inBitmap(我们需要在赋值之前进行检查):

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

最后,下面这个方法判断候选Bitmap是否满足inBitmap的大小条件:

  1. static boolean canUseForInBitmap(
  2. Bitmap candidate, BitmapFactory.Options targetOptions) {
  3. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
  4. // From Android 4.4 (KitKat) onward we can re-use if the byte size of
  5. // the new bitmap is smaller than the reusable bitmap candidate
  6. // allocation byte count.
  7. int width = targetOptions.outWidth / targetOptions.inSampleSize;
  8. int height = targetOptions.outHeight / targetOptions.inSampleSize;
  9. int byteCount = width * height * getBytesPerPixel(candidate.getConfig());
  10. return byteCount <= candidate.getAllocationByteCount();
  11. }
  12. // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1
  13. return candidate.getWidth() == targetOptions.outWidth
  14. && candidate.getHeight() == targetOptions.outHeight
  15. && targetOptions.inSampleSize == 1;
  16. }
  17. /**
  18. * A helper function to return the byte usage per pixel of a bitmap based on its configuration.
  19. */
  20. static int getBytesPerPixel(Config config) {
  21. if (config == Config.ARGB_8888) {
  22. return 4;
  23. } else if (config == Config.RGB_565) {
  24. return 2;
  25. } else if (config == Config.ARGB_4444) {
  26. return 2;
  27. } else if (config == Config.ALPHA_8) {
  28. return 1;
  29. }
  30. return 1;
  31. }