内容概述

[翻译]开发文档:android Bitmap的高效使用

本文内容来自开发文档“Traning > Displaying Bitmaps Efficiently”,包括大尺寸Bitmap的高效加载,图片的异步加载和数据缓存。

Bitmap的处理和加载非常重要,这关系到app的流畅运行和内存占用,如果方法不当,很容易导致界面卡顿和OOM。其中的原因大致有:

  • android系统对进程的内存分配限制,移动设备的配置较低。
  • Bitmap会消耗很大内存。比如相机拍下的 2592x1936 像素的照片,以ARGB_8888 格式一次加载到内存,将占据19M(259219364 bytes)的内存!
  • 通常像ListView,GridView,ViewPager这样的UI元素会同时显示或预加载多个View,这导致内存中同时需要多个Bitmaps。

下面从几个方面来分析如何高效的使用图片。

高效地加载大图

原始图片和最终显示它的View对应,一般要比显示它的View的大小要大,一些拍摄的照片甚至要比手机的屏幕分辨率还要大得多。

原则上“显示多少加载多少”,没有必要加载一个分辨率比将要显示的分辨率还大的图片,除了浪费内存没有任何好处。

下面就来看如何加载一个图片的较小的二次采样后的版本。

读取Bitmap的尺寸和类型

BitmapFactory类提供了几个方法用来从不同的源来加载位图(decodeByteArray(), decodeFile(), decodeResource(), etc.) ,它们都接收一个BitmapFactory.Options类型的参数,为了获取目标图片的尺寸类型,可以将此参数的 inJustDecodeBounds设置为true来只加载图片属性信息,而不去实际加载其内容到内存中。

  1. BitmapFactory.Options options = new BitmapFactory.Options();
  2. options.inJustDecodeBounds = true;
  3. BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
  4. int imageHeight = options.outHeight;
  5. int imageWidth = options.outWidth;
  6. String imageType = options.outMimeType;

上面的代码展示了如何不加载图片,而预先读取它的Width、Height、MiMeType。有了这些信息,就可以根据可用内存来“选择性”地加载图片,避免OOM。

加载缩小后的图片

知道目标图片的尺寸后,可以根据当前内存状态或者显示需求来决定是加载原始图片,或者是采样后的版本。下面是一些参考:

  • 估算加载完整图片需要的内存。
  • 加载这些图片允许的内存大小,要知道总得给程序其它操作留够内存。
  • 使用此图片资源的目标ImageView或其它UI组件的尺寸。
  • 当前设备的屏幕大小和分辨率。

比如,在一个作为缩略图的大小为128x96的ImageView中加载1024x768的图片是完全没有必要的。

为了让图片解码器(decoder)在加载图片时使用二次采样(subsample),可以设置参数BitmapFactory.Options 的inSampleSize属性。比如,一张2048x1536 分辨率的图片,使用inSampleSize为4的参数加载后,以ARGB_8888格式计算,最终是 512x384的图片,占0.75M的内存,而原始分辨率则占12M。

下面的方法用来计算采样率,它保证缩放的比例是2的次方,参见inSampleSize的说明:

If set to a value > 1, requests the decoder to subsample the original image, returning a smaller image to save memory. The sample size is the number of pixels in either dimension that correspond to a single pixel in the decoded bitmap. For example, inSampleSize == 4 returns an image that is 1/4 the width/height of the original, and 1/16 the number of pixels. Any value <= 1 is treated the same as 1. Note: the decoder uses a final value based on powers of 2, any other value will be rounded down to the nearest power of 2.

  1. public static int calculateInSampleSize(
  2. BitmapFactory.Options options, int reqWidth, int reqHeight) {
  3. // Raw height and width of image
  4. final int height = options.outHeight;
  5. final int width = options.outWidth;
  6. int inSampleSize = 1;
  7. if (height > reqHeight || width > reqWidth) {
  8. final int halfHeight = height / 2;
  9. final int halfWidth = width / 2;
  10. // Calculate the largest inSampleSize value that is a power of 2 and keeps both
  11. // height and width larger than the requested height and width.
  12. while ((halfHeight / inSampleSize) > reqHeight
  13. && (halfWidth / inSampleSize) > reqWidth) {
  14. inSampleSize *= 2;
  15. }
  16. }
  17. return inSampleSize;
  18. }

先设置 inJustDecodeBounds为true获得图片信息,计算出采样率,之后设置 inJustDecodeBounds为false,传递得到的inSampleSize来实际加载缩略图:

  1. public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
  2. int reqWidth, int reqHeight) {
  3. // First decode with inJustDecodeBounds=true to check dimensions
  4. final BitmapFactory.Options options = new BitmapFactory.Options();
  5. options.inJustDecodeBounds = true;
  6. BitmapFactory.decodeResource(res, resId, options);
  7. // Calculate inSampleSize
  8. options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
  9. // Decode bitmap with inSampleSize set
  10. options.inJustDecodeBounds = false;
  11. return BitmapFactory.decodeResource(res, resId, options);
  12. }

通过上述的方法,就可以把任意大小的图片加载为一个100x100的缩略图。类似这样:

  1. mImageView.setImageBitmap(
  2. decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

其它几个BitmapFactory.decode**的方法都接收相同的参数,可以采用同样的方法来安全地加载大图。

在非UI线程中处理Bitmap

从网络和磁盘加载图片可能很耗时,这样如果在UI线程中执行加载就会很容易引起ANR,下面使用AsyncTask来在后台线程中异步加载图片,并演示一些同步技巧。

使用AsyncTask

AsyncTask提供了一个简单的方式异步执行操作,然后回到UI线程中处理结果。下面就实现一个AsyncTask子类来加载图片到ImageView。

  1. class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
  2. private final WeakReference<ImageView> imageViewReference;
  3. private int data = 0;
  4. public BitmapWorkerTask(ImageView imageView) {
  5. // Use a WeakReference to ensure the ImageView can be garbage collected
  6. imageViewReference = new WeakReference<ImageView>(imageView);
  7. }
  8. // Decode image in background.
  9. @Override
  10. protected Bitmap doInBackground(Integer... params) {
  11. data = params[0];
  12. return decodeSampledBitmapFromResource(getResources(), data, 100, 100));
  13. }
  14. // Once complete, see if ImageView is still around and set bitmap.
  15. @Override
  16. protected void onPostExecute(Bitmap bitmap) {
  17. if (imageViewReference != null && bitmap != null) {
  18. final ImageView imageView = imageViewReference.get();
  19. if (imageView != null) {
  20. imageView.setImageBitmap(bitmap);
  21. }
  22. }
  23. }
  24. }

上面方法decodeSampledBitmapFromResource()是前一节“加载大图”的代码示例。

WeakReference保证了其它对ImageView的强引用消失后,它可以被正常回收。也许异步加载操作执行结束后ImageView已经不存在了(界面销毁?横竖屏切换引起Activity重建?),这时就没必要去继续显示图片了。所以在onPostExecute()中需要额外的null检查。

有了上面的BitmapWorkerTask后,就可以异步加载图片:

  1. public void loadBitmap(int resId, ImageView imageView) {
  2. BitmapWorkerTask task = new BitmapWorkerTask(imageView);
  3. task.execute(resId);
  4. }

并发处理

对于在像ListView和GridView中显示图片这样的场景,ImageView很可能会被“复用”,这样在快速滑动时,一个ImageView很可能在图片尚未加载显示时就被用来显示另一个图片,此时,上面的BitmapWorkerTask就无法保证onPostExecute中收到的Bitmap就是此ImageView当前需要显示的图片。简单地说就是图片在这些列表控件中发生错位了,本质来看,这是一个异步操作引发的并发问题。

下面采取“绑定/关联”的方式来处理上面的并发问题,这里创建一个Drawable的子类AsyncDrawable,它设置给ImageView,同时它持有对应BitmapWorkerTask 的引用,所以在对ImageView加载图片时,可以根据此AsyncDrawable来获取之前执行中的BitmapWorkerTask,之后取消它,或者在发现重复加载后放弃操作。

  1. static class AsyncDrawable extends BitmapDrawable {
  2. private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
  3. public AsyncDrawable(Resources res, Bitmap bitmap,
  4. BitmapWorkerTask bitmapWorkerTask) {
  5. super(res, bitmap);
  6. bitmapWorkerTaskReference =
  7. new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
  8. }
  9. public BitmapWorkerTask getBitmapWorkerTask() {
  10. return bitmapWorkerTaskReference.get();
  11. }
  12. }

上面,AsyncDrawable的作用类似View.setTag和ViewHolder。

在执行BitmapWorkerTask前,创建一个AsyncDrawable,然后把它绑定到目标ImageView。

注意:列表异步加载图片的场景下,ImageView是容器,是复用的。也就是并发的共享资源。

  1. public void loadBitmap(int resId, ImageView imageView) {
  2. if (cancelPotentialWork(resId, imageView)) {
  3. final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
  4. final AsyncDrawable asyncDrawable =
  5. new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
  6. imageView.setImageDrawable(asyncDrawable);
  7. task.execute(resId);
  8. }
  9. }

上面setImageDrawable()方法把ImageView和最新加载图片给它的异步任务关联起来了。

cancelPotentialWork方法()用来判断是否已经有一个任务正在加载图片到此ImageView中。如果没有,或者有但加载的是其它图片,则取消此“过期”的异步任务。如果有任务正在加载同样的图片到此ImageView那么就没必要重复开启任务了。

  1. public static boolean cancelPotentialWork(int data, ImageView imageView) {
  2. final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
  3. if (bitmapWorkerTask != null) {
  4. final int bitmapData = bitmapWorkerTask.data;
  5. // If bitmapData is not yet set or it differs from the new data
  6. if (bitmapData == 0 || bitmapData != data) {
  7. // Cancel previous task
  8. bitmapWorkerTask.cancel(true);
  9. } else {
  10. // The same work is already in progress
  11. return false;
  12. }
  13. }
  14. // No task associated with the ImageView, or an existing task was cancelled
  15. return true;
  16. }

辅助方法getBitmapWorkerTask()用来获取ImageView关联的BitmapWorkerTask。

  1. private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
  2. if (imageView != null) {
  3. final Drawable drawable = imageView.getDrawable();
  4. if (drawable instanceof AsyncDrawable) {
  5. final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
  6. return asyncDrawable.getBitmapWorkerTask();
  7. }
  8. }
  9. return null;
  10. }

最后,修改BitmapWorkerTask的 onPostExecute()方法,只有在任务未被取消,而且目标ImageView关联的BitmapWorkerTask对象为当前BitmapWorkerTask时,才设置Bitmap给此ImageView:

  1. class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
  2. ...
  3. @Override
  4. protected void onPostExecute(Bitmap bitmap) {
  5. if (isCancelled()) {
  6. bitmap = null;
  7. }
  8. if (imageViewReference != null && bitmap != null) {
  9. final ImageView imageView = imageViewReference.get();
  10. final BitmapWorkerTask bitmapWorkerTask =
  11. getBitmapWorkerTask(imageView);
  12. if (this == bitmapWorkerTask && imageView != null) {
  13. imageView.setImageBitmap(bitmap);
  14. }
  15. }
  16. }
  17. }

通过上述过程,就可以使用BitmapWorkerTask 正确安全地异步加载图片了。

Bitmap的缓存

上面分别从节约内存和避免耗时加载卡顿界面两个方面讨论了有关图片处理的技巧。

在列表显示大量图片,或者其它任意的图片显示操作下,默认地系统会对内存中无强引用的图片数据进行回收,而很多时候,如列表来回滑动多次显示同样的图片,引起图片的内存释放和反复加载,图片加载是耗时操作,最终,使得图片展示交互体验无法流畅进行。

下面从“缓存”的方式讲起,介绍下如何使用内存缓存和磁盘缓存来提高图片显示的流畅度。

内存缓存

从Android 2.3 (API Level 9)开始,GC对Soft/WeakReference的回收更加频繁,所以基于这些引用的缓存策略效果大打折扣。而且在Android 3.0 (API Level 11)以前,Bitmap的数据是以native的方式存储的,对它们的“默认回收”的行为可能引发潜在的内存泄露。

所以,现在推荐的方式是使用强引用,结合LruCache类提供的算法(它在API 12引入,Support库也提供了相同的实现使得支持API 4以上版本)来实现缓存。LruCache算法内部使用 LinkedHashMap 保持缓存对象的强引用,它维持缓存在一个限制的范围内,在内存要超越限制时优先释放最近最少使用的key。

在选择LruCache要维护的缓存总大小时,下面时一些参考建议:

  • 其余Activity或进程对内存的大小要求?
  • 屏幕同时需要显示多少图片,多少会很快进入显示状态?
  • 设备的大小和分辨率?高分辨率设备在显示相同“大小”和数量图片时需要的内存更多。
  • 图片被访问的频率,如果一些图片的访问比其它一些更加频繁,那么最好使用多个LruCache来实现不同需求的缓存。
  • 数量和质量的平衡:有时可以先加载低质量的图片,然后异步加载高质量的版本。

缓存的大小没有标准的最佳数值,根据app的需求场景而定,如果太小则带来的速度收益不大,如果太大则容易引起OOM。

下面是一个使用LruCache来缓存Bitmap的简单示例:

  1. private LruCache<String, Bitmap> mMemoryCache;
  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. ...
  5. // Get max available VM memory, exceeding this amount will throw an
  6. // OutOfMemory exception. Stored in kilobytes as LruCache takes an
  7. // int in its constructor.
  8. final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
  9. // Use 1/8th of the available memory for this memory cache.
  10. final int cacheSize = maxMemory / 8;
  11. mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
  12. @Override
  13. protected int sizeOf(String key, Bitmap bitmap) {
  14. // The cache size will be measured in kilobytes rather than
  15. // number of items.
  16. return bitmap.getByteCount() / 1024;
  17. }
  18. };
  19. ...
  20. }
  21. public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
  22. if (getBitmapFromMemCache(key) == null) {
  23. mMemoryCache.put(key, bitmap);
  24. }
  25. }
  26. public Bitmap getBitmapFromMemCache(String key) {
  27. return mMemoryCache.get(key);
  28. }

上面的代码使用了进程分配内存的1/8来作为缓存的最大值。在一个标准/hdpi分辨率的设备上,最小值大约为4MB(32/8)。一个800x480的设备上,全屏的GridView填满图片后大约使用1.5MB(8004804 bytes)的内存,这样,缓存可以保证约2.5页的图片数据。

在使用ImageView加载图片时,先去内存缓存中查看,如果存在就直接使用内中的图片,否则就异步加载它:

  1. public void loadBitmap(int resId, ImageView imageView) {
  2. final String imageKey = String.valueOf(resId);
  3. final Bitmap bitmap = getBitmapFromMemCache(imageKey);
  4. if (bitmap != null) {
  5. mImageView.setImageBitmap(bitmap);
  6. } else {
  7. mImageView.setImageResource(R.drawable.image_placeholder);
  8. BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
  9. task.execute(resId);
  10. }
  11. }

异步加载图片的BitmapWorkerTask 在获取到图片后将数据添加到缓存:

  1. class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
  2. ...
  3. // Decode image in background.
  4. @Override
  5. protected Bitmap doInBackground(Integer... params) {
  6. final Bitmap bitmap = decodeSampledBitmapFromResource(
  7. getResources(), params[0], 100, 100));
  8. addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
  9. return bitmap;
  10. }
  11. ...
  12. }

磁盘缓存

内存缓存的确是最快速的,但是一方面内存容易受限,另一方面进程重建后缓存就失效了。可以增加一个磁盘缓存的策略,这样可以缓存更多的内容,而且依然提供比网络获取数据更好的速度。如果图片被访问非常频繁,也可以考虑使用ContentProvider实现图片数据的缓存。

下面的代码使用DiskLruCache(它从android源码中可获得,在sdk提供的sample中也有)来实现磁盘缓存:

  1. private DiskLruCache mDiskLruCache;
  2. private final Object mDiskCacheLock = new Object();
  3. private boolean mDiskCacheStarting = true;
  4. private static final int DISK_CACHE_SIZE = 1024 * 1024 * 10; // 10MB
  5. private static final String DISK_CACHE_SUBDIR = "thumbnails";
  6. @Override
  7. protected void onCreate(Bundle savedInstanceState) {
  8. ...
  9. // Initialize memory cache
  10. ...
  11. // Initialize disk cache on background thread
  12. File cacheDir = getDiskCacheDir(this, DISK_CACHE_SUBDIR);
  13. new InitDiskCacheTask().execute(cacheDir);
  14. ...
  15. }
  16. class InitDiskCacheTask extends AsyncTask<File, Void, Void> {
  17. @Override
  18. protected Void doInBackground(File... params) {
  19. synchronized (mDiskCacheLock) {
  20. File cacheDir = params[0];
  21. mDiskLruCache = DiskLruCache.open(cacheDir, DISK_CACHE_SIZE);
  22. mDiskCacheStarting = false; // Finished initialization
  23. mDiskCacheLock.notifyAll(); // Wake any waiting threads
  24. }
  25. return null;
  26. }
  27. }
  28. class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
  29. ...
  30. // Decode image in background.
  31. @Override
  32. protected Bitmap doInBackground(Integer... params) {
  33. final String imageKey = String.valueOf(params[0]);
  34. // Check disk cache in background thread
  35. Bitmap bitmap = getBitmapFromDiskCache(imageKey);
  36. if (bitmap == null) { // Not found in disk cache
  37. // Process as normal
  38. final Bitmap bitmap = decodeSampledBitmapFromResource(
  39. getResources(), params[0], 100, 100));
  40. }
  41. // Add final bitmap to caches
  42. addBitmapToCache(imageKey, bitmap);
  43. return bitmap;
  44. }
  45. ...
  46. }
  47. public void addBitmapToCache(String key, Bitmap bitmap) {
  48. // Add to memory cache as before
  49. if (getBitmapFromMemCache(key) == null) {
  50. mMemoryCache.put(key, bitmap);
  51. }
  52. // Also add to disk cache
  53. synchronized (mDiskCacheLock) {
  54. if (mDiskLruCache != null && mDiskLruCache.get(key) == null) {
  55. mDiskLruCache.put(key, bitmap);
  56. }
  57. }
  58. }
  59. public Bitmap getBitmapFromDiskCache(String key) {
  60. synchronized (mDiskCacheLock) {
  61. // Wait while disk cache is started from background thread
  62. while (mDiskCacheStarting) {
  63. try {
  64. mDiskCacheLock.wait();
  65. } catch (InterruptedException e) {}
  66. }
  67. if (mDiskLruCache != null) {
  68. return mDiskLruCache.get(key);
  69. }
  70. }
  71. return null;
  72. }
  73. // Creates a unique subdirectory of the designated app cache directory. Tries to use external
  74. // but if not mounted, falls back on internal storage.
  75. public static File getDiskCacheDir(Context context, String uniqueName) {
  76. // Check if media is mounted or storage is built-in, if so, try and use external cache dir
  77. // otherwise use internal cache dir
  78. final String cachePath =
  79. Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) ||
  80. !isExternalStorageRemovable() ? getExternalCacheDir(context).getPath() :
  81. context.getCacheDir().getPath();
  82. return new File(cachePath + File.separator + uniqueName);
  83. }

因为磁盘读取依然属于耗时操作,需要在后台线程中从磁盘加载图片。另一方面,磁盘缓存需要一个初始化过程,也是异步完成,所以上面提供一个mDiskCacheLock 来保证DiskLruCache的访问同步。显然,磁盘缓存结合内存缓存是最佳的选择,上面数据从网络和从磁盘读取后都会同步到内存缓存中。

Bitmap内存管理

上面介绍了对Bitmap的缓存的实现,更进一步,下面来看看如何高效地释放Bitmap的内存,以及促进对它的复用。

首先,Bitmap的内存管理在不同的android版本中默认策略不同:

  • 在android 2.2(API 8)及更低的版本中,GC回收内存时主线程等待,而之后3.0 (API level 11)引入了并发的垃圾回收线程,这样,如果Bitmap不再被引用时,它对应的内存很快就会被回收。

  • 在2.3.3 (API level 10)版本及以前,Bitmap对应图片的像素数据是native内存中存储的,和Bitmap对象(在Dalvik堆内存中)是分开的。当Bitmap对象回收后对应的内存的回收行为不可预期,这样就会导致程序很容易达到内存边界。3.0版本就将像素数据和Bitmap对象存储在一起(Dalvik heap中 ),对象回收后对应像素数据也被释放。

android 2.3.3及更低版本的Bitmap内存管理

在2.3.3及以前版本中,android.graphics.Bitmap#recycle方法被推荐使用,调用后对应图片数据回尽快被回收掉。但确保对应图片的确不再使用了,因为方法执行后就不能再对对应的Bitmap做任何使用了,否则收到“"Canvas: trying to use a recycled bitmap"”这样的错误。

下面的代码演示了使用“引用计数”的方式来管理recycle()方法的执行,当一个Bitmap对象不再被显示或缓存时,就调用其recycle()方法主动释放其像素数据。

  1. /**
  2. * A BitmapDrawable that keeps track of whether it is being displayed or cached.
  3. * When the drawable is no longer being displayed or cached,
  4. * {@link android.graphics.Bitmap#recycle() recycle()} will be called on this drawable's bitmap.
  5. */
  6. public class RecyclingBitmapDrawable extends BitmapDrawable {
  7. static final String TAG = "CountingBitmapDrawable";
  8. private int mCacheRefCount = 0;
  9. private int mDisplayRefCount = 0;
  10. private boolean mHasBeenDisplayed;
  11. public RecyclingBitmapDrawable(Resources res, Bitmap bitmap) {
  12. super(res, bitmap);
  13. }
  14. /**
  15. * Notify the drawable that the displayed state has changed. Internally a
  16. * count is kept so that the drawable knows when it is no longer being
  17. * displayed.
  18. *
  19. * @param isDisplayed - Whether the drawable is being displayed or not
  20. */
  21. public void setIsDisplayed(boolean isDisplayed) {
  22. synchronized (this) {
  23. if (isDisplayed) {
  24. mDisplayRefCount++;
  25. mHasBeenDisplayed = true;
  26. } else {
  27. mDisplayRefCount--;
  28. }
  29. }
  30. // Check to see if recycle() can be called
  31. checkState();
  32. }
  33. /**
  34. * Notify the drawable that the cache state has changed. Internally a count
  35. * is kept so that the drawable knows when it is no longer being cached.
  36. *
  37. * @param isCached - Whether the drawable is being cached or not
  38. */
  39. public void setIsCached(boolean isCached) {
  40. synchronized (this) {
  41. if (isCached) {
  42. mCacheRefCount++;
  43. } else {
  44. mCacheRefCount--;
  45. }
  46. }
  47. // Check to see if recycle() can be called
  48. checkState();
  49. }
  50. private synchronized void checkState() {
  51. // If the drawable cache and display ref counts = 0, and this drawable
  52. // has been displayed, then recycle
  53. if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
  54. && hasValidBitmap()) {
  55. if (BuildConfig.DEBUG) {
  56. Log.d(TAG, "No longer being used or cached so recycling. "
  57. + toString());
  58. }
  59. getBitmap().recycle();
  60. }
  61. }
  62. private synchronized boolean hasValidBitmap() {
  63. Bitmap bitmap = getBitmap();
  64. return bitmap != null && !bitmap.isRecycled();
  65. }
  66. }

android 3.0 及以上版本bitmap内存的管理

在3.0(API 11)版本后 增加了BitmapFactory.Options.inBitmap 字段,使用为此字段设置了Bitmap对象的参数的decode方法会尝试复用现有的bitmap内存,这样避免了内存的分配和回收。

不过实际的实现很受限,比如在4.4(API 19)版本以前,只有大小相同的bitmap可以被复用。

下面的代码中,使用一个HashSet来维护一个WeakReference的集合,它们引用了使用LruCache缓存被丢弃的那些Bitmap,这样后续的decode就可以复用它们。

  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方法可以从前面的集合中先查看是否有可用的对象。

  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() 完成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来复用。方法canUseForInBitmap()通过对大小的检查来判定是否可以被复用:

  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. }

在界面显示图片

下面分别示范ViewPager和GridView的形式来展示图片,综合了上面的异步加载,缓存等知识。

使用ViewPager

可以用ViewPager实现“swipe view pattern”,比如在图片浏览功能中左右滑动来查看不同的图片(上一个,下一个)。

既然使用ViewPager,就需要为它提供PagerAdapter子类。假设图片的预计内存使用不用太过担心,那么PagerAdapter或者FragmentPagerAdapter就够用了,更复杂的内存管理需求下,可以采用FragmentStatePagerAdapter,它在ViewPager显示的不同Fragment离开屏幕后自动销毁它们并保持其状态。

下面代码中,ImageDetailActivity中定义了显示用的ViewPager和它对应的ImagePagerAdapter:

  1. public class ImageDetailActivity extends FragmentActivity {
  2. public static final String EXTRA_IMAGE = "extra_image";
  3. private ImagePagerAdapter mAdapter;
  4. private ViewPager mPager;
  5. // A static dataset to back the ViewPager adapter
  6. public final static Integer[] imageResIds = new Integer[] {
  7. R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
  8. R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
  9. R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
  10. @Override
  11. public void onCreate(Bundle savedInstanceState) {
  12. super.onCreate(savedInstanceState);
  13. setContentView(R.layout.image_detail_pager); // Contains just a ViewPager
  14. mAdapter = new ImagePagerAdapter(getSupportFragmentManager(), imageResIds.length);
  15. mPager = (ViewPager) findViewById(R.id.pager);
  16. mPager.setAdapter(mAdapter);
  17. }
  18. public static class ImagePagerAdapter extends FragmentStatePagerAdapter {
  19. private final int mSize;
  20. public ImagePagerAdapter(FragmentManager fm, int size) {
  21. super(fm);
  22. mSize = size;
  23. }
  24. @Override
  25. public int getCount() {
  26. return mSize;
  27. }
  28. @Override
  29. public Fragment getItem(int position) {
  30. return ImageDetailFragment.newInstance(position);
  31. }
  32. }
  33. }

类ImageDetailFragment作为ViewPager的item,用来展示一个图片的详细内容:

  1. public class ImageDetailFragment extends Fragment {
  2. private static final String IMAGE_DATA_EXTRA = "resId";
  3. private int mImageNum;
  4. private ImageView mImageView;
  5. static ImageDetailFragment newInstance(int imageNum) {
  6. final ImageDetailFragment f = new ImageDetailFragment();
  7. final Bundle args = new Bundle();
  8. args.putInt(IMAGE_DATA_EXTRA, imageNum);
  9. f.setArguments(args);
  10. return f;
  11. }
  12. // Empty constructor, required as per Fragment docs
  13. public ImageDetailFragment() {}
  14. @Override
  15. public void onCreate(Bundle savedInstanceState) {
  16. super.onCreate(savedInstanceState);
  17. mImageNum = getArguments() != null ? getArguments().getInt(IMAGE_DATA_EXTRA) : -1;
  18. }
  19. @Override
  20. public View onCreateView(LayoutInflater inflater, ViewGroup container,
  21. Bundle savedInstanceState) {
  22. // image_detail_fragment.xml contains just an ImageView
  23. final View v = inflater.inflate(R.layout.image_detail_fragment, container, false);
  24. mImageView = (ImageView) v.findViewById(R.id.imageView);
  25. return v;
  26. }
  27. @Override
  28. public void onActivityCreated(Bundle savedInstanceState) {
  29. super.onActivityCreated(savedInstanceState);
  30. final int resId = ImageDetailActivity.imageResIds[mImageNum];
  31. mImageView.setImageResource(resId); // Load image into ImageView
  32. }
  33. }

上面的图片加载是在UI线程中执行的,利用之前的AsyncTask实现的异步加载功能,将操作放在后台线程中去:

  1. public class ImageDetailActivity extends FragmentActivity {
  2. ...
  3. public void loadBitmap(int resId, ImageView imageView) {
  4. mImageView.setImageResource(R.drawable.image_placeholder);
  5. BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
  6. task.execute(resId);
  7. }
  8. ... // include BitmapWorkerTask class
  9. }
  10. public class ImageDetailFragment extends Fragment {
  11. ...
  12. @Override
  13. public void onActivityCreated(Bundle savedInstanceState) {
  14. super.onActivityCreated(savedInstanceState);
  15. if (ImageDetailActivity.class.isInstance(getActivity())) {
  16. final int resId = ImageDetailActivity.imageResIds[mImageNum];
  17. // Call out to ImageDetailActivity to load the bitmap in a background thread
  18. ((ImageDetailActivity) getActivity()).loadBitmap(resId, mImageView);
  19. }
  20. }
  21. }

正如上面展示的那样,可以将需要的耗时处理放在BitmapWorkerTask中去执行。下面为整个代码加入缓存功能:

  1. public class ImageDetailActivity extends FragmentActivity {
  2. ...
  3. private LruCache<String, Bitmap> mMemoryCache;
  4. @Override
  5. public void onCreate(Bundle savedInstanceState) {
  6. ...
  7. // initialize LruCache as per Use a Memory Cache section
  8. }
  9. public void loadBitmap(int resId, ImageView imageView) {
  10. final String imageKey = String.valueOf(resId);
  11. final Bitmap bitmap = mMemoryCache.get(imageKey);
  12. if (bitmap != null) {
  13. mImageView.setImageBitmap(bitmap);
  14. } else {
  15. mImageView.setImageResource(R.drawable.image_placeholder);
  16. BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
  17. task.execute(resId);
  18. }
  19. }
  20. ... // include updated BitmapWorkerTask from Use a Memory Cache section
  21. }

上面的代码实现使得图片的加载显示灰常流畅,如果还需要对图片施加额外的处理,都可以继续去扩展异步任务来实现。

使用GridView展示图片

网格视图的显示风格非常适合每个Item都是缩略图这样的情形。这时,同时在屏幕上会展示大量图片,随着滑动ImageView也会被回收利用。相比ViewPager每次展示一个图片的较大的情况,此时除了可以使用上面提到的缓存,异步加载技术外,一个需要处理的问题就是“并发”——异步加载时保证ImageView显示图片不会错乱。同样的问题在ListView中也是存在的,因为它们的re-use原则。

下面的ImageGridFragment 用来显示整个GridView,它里面同时定义了用到的BaseAdapter:

  1. public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
  2. private ImageAdapter mAdapter;
  3. // A static dataset to back the GridView adapter
  4. public final static Integer[] imageResIds = new Integer[] {
  5. R.drawable.sample_image_1, R.drawable.sample_image_2, R.drawable.sample_image_3,
  6. R.drawable.sample_image_4, R.drawable.sample_image_5, R.drawable.sample_image_6,
  7. R.drawable.sample_image_7, R.drawable.sample_image_8, R.drawable.sample_image_9};
  8. // Empty constructor as per Fragment docs
  9. public ImageGridFragment() {}
  10. @Override
  11. public void onCreate(Bundle savedInstanceState) {
  12. super.onCreate(savedInstanceState);
  13. mAdapter = new ImageAdapter(getActivity());
  14. }
  15. @Override
  16. public View onCreateView(
  17. LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
  18. final View v = inflater.inflate(R.layout.image_grid_fragment, container, false);
  19. final GridView mGridView = (GridView) v.findViewById(R.id.gridView);
  20. mGridView.setAdapter(mAdapter);
  21. mGridView.setOnItemClickListener(this);
  22. return v;
  23. }
  24. @Override
  25. public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
  26. final Intent i = new Intent(getActivity(), ImageDetailActivity.class);
  27. i.putExtra(ImageDetailActivity.EXTRA_IMAGE, position);
  28. startActivity(i);
  29. }
  30. private class ImageAdapter extends BaseAdapter {
  31. private final Context mContext;
  32. public ImageAdapter(Context context) {
  33. super();
  34. mContext = context;
  35. }
  36. @Override
  37. public int getCount() {
  38. return imageResIds.length;
  39. }
  40. @Override
  41. public Object getItem(int position) {
  42. return imageResIds[position];
  43. }
  44. @Override
  45. public long getItemId(int position) {
  46. return position;
  47. }
  48. @Override
  49. public View getView(int position, View convertView, ViewGroup container) {
  50. ImageView imageView;
  51. if (convertView == null) { // if it's not recycled, initialize some attributes
  52. imageView = new ImageView(mContext);
  53. imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
  54. imageView.setLayoutParams(new GridView.LayoutParams(
  55. LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
  56. } else {
  57. imageView = (ImageView) convertView;
  58. }
  59. imageView.setImageResource(imageResIds[position]); // Load image into ImageView
  60. return imageView;
  61. }
  62. }
  63. }

上面的代码暴露的问题就是异步加载和ImageView复用会产生错乱,下面使用之前异步加载图片中讨论过的“关联”技术来解决它:

  1. public class ImageGridFragment extends Fragment implements AdapterView.OnItemClickListener {
  2. ...
  3. private class ImageAdapter extends BaseAdapter {
  4. ...
  5. @Override
  6. public View getView(int position, View convertView, ViewGroup container) {
  7. ...
  8. loadBitmap(imageResIds[position], imageView)
  9. return imageView;
  10. }
  11. }
  12. public void loadBitmap(int resId, ImageView imageView) {
  13. if (cancelPotentialWork(resId, imageView)) {
  14. final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
  15. final AsyncDrawable asyncDrawable =
  16. new AsyncDrawable(getResources(), mPlaceHolderBitmap, task);
  17. imageView.setImageDrawable(asyncDrawable);
  18. task.execute(resId);
  19. }
  20. }
  21. static class AsyncDrawable extends BitmapDrawable {
  22. private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
  23. public AsyncDrawable(Resources res, Bitmap bitmap,
  24. BitmapWorkerTask bitmapWorkerTask) {
  25. super(res, bitmap);
  26. bitmapWorkerTaskReference =
  27. new WeakReference<BitmapWorkerTask>(bitmapWorkerTask);
  28. }
  29. public BitmapWorkerTask getBitmapWorkerTask() {
  30. return bitmapWorkerTaskReference.get();
  31. }
  32. }
  33. public static boolean cancelPotentialWork(int data, ImageView imageView) {
  34. final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
  35. if (bitmapWorkerTask != null) {
  36. final int bitmapData = bitmapWorkerTask.data;
  37. if (bitmapData != data) {
  38. // Cancel previous task
  39. bitmapWorkerTask.cancel(true);
  40. } else {
  41. // The same work is already in progress
  42. return false;
  43. }
  44. }
  45. // No task associated with the ImageView, or an existing task was cancelled
  46. return true;
  47. }
  48. private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
  49. if (imageView != null) {
  50. final Drawable drawable = imageView.getDrawable();
  51. if (drawable instanceof AsyncDrawable) {
  52. final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
  53. return asyncDrawable.getBitmapWorkerTask();
  54. }
  55. }
  56. return null;
  57. }
  58. ... // include updated BitmapWorkerTask class

以上的代码保证了对GridView展示的图片的异步加载不会导致错乱,必须牢记耗时操作不要阻塞UI,保证交互流畅。对应ListView上面的代码依然适用。

资料

  • sdk开发文档

    Training > Displaying Bitmaps Efficiently,

    目录:/docs/training/displaying-bitmaps/index.html

(本文使用Atom编写 2016/3/1)

[翻译]开发文档:android Bitmap的高效使用的更多相关文章

  1. Android官方开发文档Training系列课程中文版:目录

    Android官方开发文档Training系列课程中文版:目录   引言 在翻译了一篇安卓的官方文档之后,我觉得应该做一件事情,就是把安卓的整篇训练课程全部翻译成英文,供国内的开发者使用,尤其是入门开 ...

  2. 在线API,桌面版,jquery,css,Android中文开发文档,JScript,SQL掌用实例

    学习帮助文档大全 jquery,css,Android中文开发文档,JScript,SQL掌用实例 http://api.jq-school.com/

  3. Android 界面滑动实现---Scroller类 从源码和开发文档中学习(让你的布局动起来)

    在android学习中,动作交互是软件中重要的一部分,其中的Scroller就是提供了拖动效果的类,在网上,比如说一些Launcher实现滑屏都可以通过这个类去实现..   例子相关博文:Androi ...

  4. Android 滑动界面实现---Scroller类别 从源代码和开发文档了解(让你的移动布局)

    在android学习,行动互动是软件的重要组成部分,其中Scroller是提供了拖动效果的类,在网上.比方说一些Launcher实现滑屏都能够通过这个类去实现.. 样例相关博文:Android 仿 窗 ...

  5. Android官方开发文档下载

    Android官方开发文档 docs-24_r02.rar(链接:https://pan.baidu.com/s/12xC998JeUHj3ndfDXPM2ww 密码:bxyk) ADT下载.Andr ...

  6. Android App签名打包 与 SDK开发文档

    Android App签名打包签名的意义1.为了保证每个程序开发者的合法权益2.放置部分人通过使用相同的Package Name来混淆替换已经安装的程序,从而出现一些恶意篡改3.保证我们每次发布的版本 ...

  7. 翻译-ExcelDNA开发文档

    转载自个人主页 前言 翻译开源项目ExcelDNA开发文档 异步处理 ExcelDNA支持两种异步函数: RTD,该函数适用与Excel2003及以上版本,(当你使用ExcelAsyncUtil.*时 ...

  8. 【原创】Odoo开发文档学习之:ORM API接口(ORM API)(边Google翻译边学习)

    官方ORM API开发文档:https://www.odoo.com/documentation/10.0/reference/orm.html Recordsets(记录集) New in vers ...

  9. 【原创】Odoo开发文档学习之:构建接口扩展(Building Interface Extensions)(边Google翻译边学习)

    构建接口扩展(Building Interface Extensions) 本指南是关于为Odoo的web客户创建模块. 要创建有Odoo的网站,请参见建立网站;要添加业务功能或扩展Odoo的现有业务 ...

随机推荐

  1. XSS

    XSS的含义 XSS(Cross Site Scripting)即跨站脚本.跨站的主要内容是在脚本上. 跨站脚本 跨站脚本的跨,体现了浏览器的特性,可以跨域.所以也就给远程代码或者第三方域上的代码提供 ...

  2. ABP文档 - Javascript Api - Message

    本节内容: 显示信息 确认 Message API给用户显示一个信息,或从用户那里获取一个确认信息. Message API默认使用sweetalert实现,为使sweetalert正常工作,你应该包 ...

  3. CSS垂直居中的11种实现方式

    今天是邓呆呆球衣退役的日子,在这个颇具纪念意义的日子里我写下自己的第一篇博客,还望前辈们多多提携,多多指教! 接下来,就进入正文,来说说关于垂直居中的事.(以下这11种垂直居中的实现方式均为笔者在日常 ...

  4. AFNetworking 3.0 源码解读 总结(干货)(下)

    承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...

  5. 免费高效实用的.NET操作Excel组件NPOI(.NET组件介绍之六)

    很多的软件项目几乎都包含着对文档的操作,前面已经介绍过两款操作文档的组件,现在介绍一款文档操作的组件NPOI. NPOI可以生成没有安装在您的服务器上的Microsoft Office套件的Excel ...

  6. javascript动画系列第一篇——模拟拖拽

    × 目录 [1]原理介绍 [2]代码实现 [3]代码优化[4]拖拽冲突[5]IE兼容 前面的话 从本文开始,介绍javascript动画系列.javascript本身是具有原生拖放功能的,但是由于兼容 ...

  7. Tableau未必最佳,国内BI也能突破重围!

    如今,百度一下商业智能或BI工具,总能看到Tableau的身影.并不是Tableau的营销做得好,而是国内对于商业智能工具的认知和选择似乎都落在了Tableau身上.导致不管业内业外都对商业智能的概念 ...

  8. 【干货分享】流程DEMO-资产请购单

    流程名: 资产请购  业务描述: 流程发起时,会检查预算,如果预算不够,流程必须经过总裁审批,如果预算够用,将发起流程,同时占用相应金额的预算,但撤销流程会释放相应金额的预算.  流程相关文件: 流程 ...

  9. 【SAP业务模式】之ICS(七):IDOC配置

    这是ICS业务模式系列的最后一篇了,主要讲解IDOC的配置. 一.指定EDI传输的供应商逻辑地址 事务代码:WEL1 注意:上面逻辑地址是生产公司+内部客户.有以下两种情形: 1.如果内部客户都是纯数 ...

  10. Linux系统中用DNW向ARM开发板下载程序

    在Linux下通过dnw来给开发板发送程序.包括驱动程序代码:secbulk.c,应用程序代码:dnw.c.只能运行在32位系统上,在64位系统上提示错误:DNW download Data size ...