[关闭]
@946898963 2020-03-25T10:48:17.000000Z 字数 12575 阅读 1684

高效的加载Bitmap避免内存泄露

Android应用性能优化 Bitmap


我们在使用bitmap时,经常会遇到内存溢出等情况,这是因为图片太大或者android系统对单个应用施加的内存限制等原因造成的,所以,高效的使用bitmap就显得尤为重要,对他效率的优化也是如此。

Bitmap基本加载方式

BitmapFactory提供的解析Bitmap的静态工厂方法有以下五种:

  1. Bitmap decodeFile(...)
  2. Bitmap decodeResource(...)
  3. Bitmap decodeByteArray(...)
  4. Bitmap decodeStream(...)
  5. Bitmap decodeFileDescriptor(...)

其中常用的三个:decodeFile、decodeResource、decodeStream。decodeFile和decodeResource其实最终都是调用decodeStream方法来解析Bitmap,decodeStream的内部则是调用两个native方法解析Bitmap的:

  1. nativeDecodeAsset()
  2. nativeDecodeStream()

这两个native方法只是对应decodeFile、decodeResource和decodeStream来解析的,像decodeByteArray、decodeFileDescriptor也有专门的native方法负责解析Bitmap。

接下来就是看看这两个方法在解析Bitmap时究竟有什么区别decodeFile、decodeResource,查看后发现它们调用路径如下:

  1. decodeFile->decodeStream
  2. decodeResource->decodeResourceStream->decodeStream

decodeResource在解析时多调用了一个decodeResourceStream方法,而这个decodeResourceStream方法代码如下:

#decodeResource

  1. public static Bitmap decodeResource(Resources res, int id, Options opts) {
  2. validate(opts);
  3. Bitmap bm = null;
  4. InputStream is = null;
  5. try {
  6. final TypedValue value = new TypedValue();
  7. is = res.openRawResource(id, value);
  8. bm = decodeResourceStream(res, value, is, null, opts);
  9. } catch (Exception e) {
  10. /* do nothing.
  11. If the exception happened on open, bm will be null.
  12. If it happened on close, bm is still valid.
  13. */
  14. } finally {
  15. try {
  16. if (is != null) is.close();
  17. } catch (IOException e) {
  18. // Ignore
  19. }
  20. }
  21. if (bm == null && opts != null && opts.inBitmap != null) {
  22. throw new IllegalArgumentException("Problem decoding into existing bitmap");
  23. }
  24. return bm;
  25. }

#decodeResourceStream

  1. public static Bitmap decodeResourceStream(Resources res, TypedValue value,
  2. InputStream is, Rect pad, Options opts) {
  3. if (opts == null) {
  4. opts = new Options();
  5. }
  6. if (opts.inDensity == 0 && value != null) {
  7. final int density = value.density;
  8. if (density == TypedValue.DENSITY_DEFAULT) {//图片在drawable目录下
  9. opts.inDensity = DisplayMetrics.DENSITY_DEFAULT;
  10. } else if (density != TypedValue.DENSITY_NONE) {//图片在非drawable目录下
  11. opts.inDensity = density;
  12. }
  13. }
  14. if (opts.inTargetDensity == 0 && res != null) {
  15. opts.inTargetDensity = res.getDisplayMetrics().densityDpi;
  16. }
  17. return decodeStream(is, pad, opts);
  18. }

阅读:BitmapFactory.Options中的inDensity和inTargetDensity

它主要是对Options进行处理了,在得到opts.inDensity属性的前提下,如果我们没有对该属性设定值,那么将opts.inDensity=DisplayMetrics.DENSITY_DEFAULT;赋定这个默认的Density值,这个默认值为160,为标准的dpi比例,即在Density=160的设备上1dp=1px,这个方法中还有这么一行:

  1. opts.inTargetDensity = res.getDisplayMetrics().densityDpi;

对opts.inTargetDensity进行了赋值,该值为当前设备的densityDpi值,所以说在decodeResourceStream方法中主要做了两件事:

  1. 1、对opts.inDensity赋值,没有则赋默认值160
  2. 2、对opts.inTargetDensity赋值,没有则赋当前设备的densityDpi

之后重点来了,之后参数将传入decodeStream方法,该方法中在调用native方法进行解析Bitmap后会调用这个方法setDensityFromOptions(bm, opts);:

Android 4.4之前#decodeStream

  1. public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
  2. // we don't throw in this case, thus allowing the caller to only check
  3. // the cache, and not force the image to be decoded.
  4. if (is == null) {
  5. return null;
  6. }
  7. // we need mark/reset to work properly
  8. if (!is.markSupported()) {
  9. is = new BufferedInputStream(is, DECODE_BUFFER_SIZE);
  10. }
  11. // so we can call reset() if a given codec gives up after reading up to
  12. // this many bytes. FIXME: need to find out from the codecs what this
  13. // value should be.
  14. is.mark(1024);
  15. Bitmap bm;
  16. boolean finish = true;
  17. if (is instanceof AssetManager.AssetInputStream) {
  18. final int asset = ((AssetManager.AssetInputStream) is).getAssetInt();
  19. if (opts == null || (opts.inScaled && opts.inBitmap == null)) {
  20. float scale = 1.0f;
  21. int targetDensity = 0;
  22. if (opts != null) {
  23. final int density = opts.inDensity;
  24. targetDensity = opts.inTargetDensity;
  25. if (density != 0 && targetDensity != 0) {
  26. scale = targetDensity / (float) density;
  27. }
  28. }
  29. bm = nativeDecodeAsset(asset, outPadding, opts, true, scale);
  30. if (bm != null && targetDensity != 0) bm.setDensity(targetDensity);
  31. finish = false;
  32. } else {
  33. bm = nativeDecodeAsset(asset, outPadding, opts);
  34. }
  35. } else {
  36. // pass some temp storage down to the native code. 1024 is made up,
  37. // but should be large enough to avoid too many small calls back
  38. // into is.read(...) This number is not related to the value passed
  39. // to mark(...) above.
  40. byte [] tempStorage = null;
  41. if (opts != null) tempStorage = opts.inTempStorage;
  42. if (tempStorage == null) tempStorage = new byte[16 * 1024];
  43. if (opts == null || (opts.inScaled && opts.inBitmap == null)) {
  44. float scale = 1.0f;
  45. int targetDensity = 0;
  46. if (opts != null) {
  47. final int density = opts.inDensity;
  48. targetDensity = opts.inTargetDensity;
  49. if (density != 0 && targetDensity != 0) {
  50. scale = targetDensity / (float) density;
  51. }
  52. }
  53. bm = nativeDecodeStream(is, tempStorage, outPadding, opts, true, scale);
  54. if (bm != null && targetDensity != 0) bm.setDensity(targetDensity);
  55. finish = false;
  56. } else {
  57. bm = nativeDecodeStream(is, tempStorage, outPadding, opts);
  58. }
  59. }
  60. if (bm == null && opts != null && opts.inBitmap != null) {
  61. throw new IllegalArgumentException("Problem decoding into existing bitmap");
  62. }
  63. return finish ? finishDecode(bm, outPadding, opts) : bm;
  64. }

#finishDecode

  1. private static Bitmap finishDecode(Bitmap bm, Rect outPadding, Options opts) {
  2. if (bm == null || opts == null) {
  3. return bm;
  4. }
  5. final int density = opts.inDensity;
  6. if (density == 0) {
  7. return bm;
  8. }
  9. bm.setDensity(density);
  10. final int targetDensity = opts.inTargetDensity;
  11. if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
  12. return bm;
  13. }
  14. byte[] np = bm.getNinePatchChunk();
  15. int[] lb = bm.getLayoutBounds();
  16. final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
  17. if (opts.inScaled || isNinePatch) {
  18. float scale = targetDensity / (float) density;
  19. if (scale != 1.0f) {
  20. final Bitmap oldBitmap = bm;
  21. bm = Bitmap.createScaledBitmap(oldBitmap, (int) (bm.getWidth() * scale + 0.5f),
  22. (int) (bm.getHeight() * scale + 0.5f), true);
  23. if (bm != oldBitmap) oldBitmap.recycle();
  24. if (isNinePatch) {
  25. np = nativeScaleNinePatch(np, scale, outPadding);
  26. bm.setNinePatchChunk(np);
  27. }
  28. if (lb != null) {
  29. int[] newLb = new int[lb.length];
  30. for (int i=0; i<lb.length; i++) {
  31. newLb[i] = (int)((lb[i]*scale)+.5f);
  32. }
  33. bm.setLayoutBounds(newLb);
  34. }
  35. }
  36. bm.setDensity(targetDensity);
  37. }
  38. return bm;
  39. }

这里我们只需要关注,当decodeStream完成后,在设备密度不为0且不等于资源密度时,会执行finishDecode,在finishDecode中会调用createScaledBitmap重新创建bitmap并回收旧的bitmap,也就是说在java层有一个调整bitmap的逻辑。接下来看Android 4.4之后的decodeStream的相关源码:

Android 4.4之后#decodeStream

  1. public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts) {
  2. // we don't throw in this case, thus allowing the caller to only check
  3. // the cache, and not force the image to be decoded.
  4. if (is == null) {
  5. return null;
  6. }
  7. validate(opts);
  8. Bitmap bm = null;
  9. Trace.traceBegin(Trace.TRACE_TAG_GRAPHICS, "decodeBitmap");
  10. try {
  11. if (is instanceof AssetManager.AssetInputStream) {
  12. final long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
  13. bm = nativeDecodeAsset(asset, outPadding, opts);
  14. } else {
  15. bm = decodeStreamInternal(is, outPadding, opts);
  16. }
  17. if (bm == null && opts != null && opts.inBitmap != null) {
  18. throw new IllegalArgumentException("Problem decoding into existing bitmap");
  19. }
  20. setDensityFromOptions(bm, opts);
  21. } finally {
  22. Trace.traceEnd(Trace.TRACE_TAG_GRAPHICS);
  23. }
  24. return bm;
  25. }

#setDensityFromOptions

  1. private static void setDensityFromOptions(Bitmap outputBitmap, Options opts) {
  2. if (outputBitmap == null || opts == null) return;
  3. final int density = opts.inDensity;
  4. if (density != 0) {
  5. outputBitmap.setDensity(density);
  6. final int targetDensity = opts.inTargetDensity;
  7. if (targetDensity == 0 || density == targetDensity || density == opts.inScreenDensity) {
  8. return;
  9. }
  10. byte[] np = outputBitmap.getNinePatchChunk();
  11. final boolean isNinePatch = np != null && NinePatch.isNinePatchChunk(np);
  12. if (opts.inScaled || isNinePatch) {
  13. outputBitmap.setDensity(targetDensity);
  14. }
  15. } else if (opts.inBitmap != null) {
  16. // bitmap was reused, ensure density is reset
  17. outputBitmap.setDensity(Bitmap.getDefaultDensity());
  18. }
  19. }

可以看到,decodeStream方法在执行完成后,会调用setDensityFromOptions方法,该方法主要就是把前面赋值过的两个属性inDensity和inTargetDensity给Bitmap进行赋值,不过并不是直接赋给Bitmap就完了,中间有个判断,当inDensity的值与inTargetDensity或与设备的屏幕Density不相等时,则将应用inTargetDensity的值,如果相等则应用inDensity的值。

在setDensityFromOptions方法中,我们没有看到根据密度调整Bitmap的操作,这是因为Android 4.4以后,为了节省内存,将这个操作放在了native方法中,也就是C层去做Bitmap的缩放。

  1. 所以总结来说,setDensityFromOptions方法就是把inTargetDensity的值赋给Bitmap,不过前提是opts.inScaled = true

进过上面的分析,可以得出这样一个结论:

在不配置Options的情况下:

1、decodeFile、decodeStream在解析时不会对Bitmap进行一系列的屏幕适配,解析出来的将是原始大小的图。

2、decodeResource在解析时会对Bitmap根据当前设备屏幕像素密度densityDpi的值进行缩放适配操作,使得解析出来的Bitmap与当前设备的分辨率匹配,达到一个最佳的显示效果,并且Bitmap的大小将比原始的大。

BitmapFactory.decodeResource加载的图片可能会经过缩放,在Android 4.4之前,该缩放目前是放在java层做的,效率比较低,而且需要消耗java层的内存。因此,在Android 4.4之前,如果大量使用该接口加载图片,容易导致OOM错误;而在Android 4.4之后,这个操作放在了Native层,也就是C层去做,节省了Java层的内存空间,不会出现上述问题。

BitmapFactory.decodeStream不会对所加载的图片进行缩放,在Android 4.4之前,相比之下占用内存少,效率更高,但是decodeStream直接拿的图片来读取字节码了,不会根据机器的各种分辨率来自动适应,所以使用了decodeStream的时候,需要在hdpi和mdpi,ldpi中配置相应的图片资源,否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。

这两个接口各有用处,关于如何选择这两个方法,在Android 4.4之前,除了使用场景之外,还需要考虑性能方面,如果对性能要求较高,则应该使用decodeStream;如果对性能要求不高,且需要Android自带的图片自适应缩放功能,则可以使用decodeResource;在Android 4.4之后,则只需要根据具体的使用场景去考虑即可。

部分摘自:Android性能优化之Bitmap的内存优化

建议阅读:安卓屏幕的尺寸信息&Android中图片大小、内存占用与drawable文件夹关系的研究与分析

BitmapFactory.cpp

高效加载

在Android 4.4以前,加载Bitmap的时候,尽量不要使用setImageResource,decodeResource来设置一张大图,因为正如前面讲的,这些函数在完成decode后,在某些条件下会对Bitmap进行缩放,而缩放操作最终都是通过java层的createBitmap来完成的,需要消耗更多内存。

首先建议通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source,decodeStream最大的秘密在于其直接调用JNI>>nativeDecodeAsset()来完成decode,无需再使用java层的createBitmap,从而节省了java层的空间。

其次读取时加上BitmapFactory的Option参数,可以更有效减少加载的内存,从而更有效的阻止抛out of Memory异常。

设置Options.inPreferredConfig值来降低内存消耗 //如把默认值ARGB_8888改为RGB_565,节约一半内存
设置Options.inPurgeable和inInputShareable让系统能及时回收内存
设置Options.inSampleSize 对大图片进行压缩

此外需要注意的是:decodeStream直接拿的图片来读取字节码了,不会根据机器的各种分辨率来自动适应,使用了decodeStream之后,需要在hdpi和mdpi,ldpi中配置相应的图片资源,否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了。

此外,我们还可以利用BitmapFactory.Options的参数,来高效的加载Bitmap。

  • 借助inSampleSize参数实现按需加载

很多时候ImageView并没有原始图片那么大,所以没必要加载原始大小的图片。采用BitmapFactory.Options来加载所需尺寸的图片。通过BitmapFactory.Options来缩放图片,主要是用到了它的inSampleSize参数,即采样率。inSampleSize应该为2的倍数,如果不是系统会向下取整并选择一个最接近2的指数来代替;缩放比例为1/(inSampleSize的二次方)。按需加载虽然可以减少内存的使用,但是可能会有图片失真

  • 借助于inBitmap复用原有Bitmap的内存空间

BitmapFactory.Option中的inBitmap

  • 借助于inPreferredConfig合理设置图片像素的表示模式

Bitmap.Config

  • 借助于inPurgeable和inInputShareable设置空间回收(这两个字段已经废弃了)

BitmapFactory.Option

高效地加载图片,代码示例:

注意:int reqWidth, int reqHeight的单位都是像素。

  1. public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
  2. // First decode with inJustDecodeBounds=true to check dimensions
  3. final BitmapFactory.Options options = new BitmapFactory.Options();
  4. options.inJustDecodeBounds = true;
  5. BitmapFactory.decodeFileDescriptor(fd, null, options);
  6. // Calculate inSampleSize
  7. options.inSampleSize = calculateInSampleSize(options, reqWidth,
  8. reqHeight);
  9. // Decode bitmap with inSampleSize set
  10. options.inJustDecodeBounds = false;
  11. //结合了方式一
  12. options.inPurgeable = true;
  13. options.inInputShareable = true;
  14. options.inPreferredConfig = Bitmap.Config.RGB_565;
  15. return BitmapFactory.decodeFileDescriptor(fd, null, options);
  16. }
  17. public static Bitmap decodeBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
  18. BitmapFactory.Options options = new BitmapFactory.Options();
  19. options.inJustDecodeBounds = true;
  20. BitmapFactory.decodeResource(res, resId, options);
  21. options.inSampleSize = calcuateInSampleSize(options, reqWidth, reqHeight);
  22. options.inJustDecodeBounds = false;
  23. //结合了方式一
  24. options.inPurgeable = true;
  25. options.inInputShareable = true;
  26. options.inPreferredConfig = Bitmap.Config.RGB_565;
  27. return BitmapFactory.decodeResource(res, resId, options);
  28. }
  29. public Bitmap decodeBitmapFromFile(String path, int reqWidth, int reqHeight) {
  30. BitmapFactory.Options options = new BitmapFactory.Options();
  31. options.inJustDecodeBounds = true;
  32. BitmapFactory.decodeFile(path, options);
  33. options.inSampleSize = calculateSampleSize(options, reqHeight, reqWidth);
  34. options.inJustDecodeBounds = false;
  35. options.inPurgeable = true;
  36. options.inPreferredConfig = Bitmap.Config.RGB_565;
  37. options.inInputShareable = true;
  38. return BitmapFactory.decodeFile(path, options);
  39. }
  40. //获取采样率
  41. private static int calcuateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
  42. int width = options.outWidth;//注意这里width是以像素为单位的
  43. int height = options.outHeight;//注意这里height是以像素为单位的
  44. int inSampleSize = 1;
  45. if (height > reqHeight || width > reqWidth) {
  46. int halfHeight = height / 2;
  47. int halfWidth = width / 2;
  48. while ((halfHeight / inSampleSize) >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
  49. inSampleSize *= 2;
  50. }
  51. }
  52. return inSampleSize;
  53. }
  54. //参考自Volley的ImageRequest的源码
  55. private static int calculateSampleSizeB(int width, int height, int reqWidth, int reqHeight) {
  56. double wr = (double) width / (double) reqWidth;
  57. double hr = (double) height / (double) reqHeight;
  58. double ratio = Math.min(wr, hr);
  59. float n = 1.0f;
  60. while (n * 2 <= ratio) {
  61. n *= 2;
  62. }
  63. return (int) n;
  64. }
  65. // 显示图片
  66. Bitmap bitmap = DecodeBitmap.decodeBitmapFromResource(getResources(), R.mipmap.haimei2, 400, 400);
  67. imageView.setImageBitmap(bitmap);

当inJustDecodeBounds参数为true时,BitmapFactory只会解析图片的原始宽/高信息,并不会真正的加载图片,所以这个操作是轻量级的。需要注意这时候BitmapFactory获取的图片宽/高信息和图片的位置与程序运行的设备有关。

通过BitmapFactory.Options对象来加载一张缩放后的图片,对FileInputStream的缩放存在问题,因为FileInputStream是一种有序的文件流,而两次decodeStream调用影响了文件流的位置属相,导致第二次decodeStream时得到的是null。所以一般通过文件流来得到对应的文件描述符,通过BitmapFactory.decodeFileDescriptor()来加载一张缩放后的图片。

关于解决decodeStream加载的问题的另一种解决方案,建议阅读:Bitmap的高效加载

在decodeBitmapFromResource和decodeSampledBitmapFromFileDescriptor方法中可以结合高效加载一中的方法,配置Options的其他参数,进行更高效的加载。

BitmapFactory.Options

BitmapFactory.Option

参考链接:

BitmapFactory.Options避免 内存溢出 OutOfMemoryError的优化方法
温故而知新 - 一些解决OOM的方法
获取缩略图
android Options.inPurgeable的意思
android内存优化
Android 之 Bitmap
Bitmap的分析与使用.md
Android图片加载解析之Bitmap

Android性能優化之Bitmap的內存優化
Bitmap详解与Bitmap的内存优化

Bitmap基本概念及在Android4.4系统上使用BitmapFactory的注意事项

深入理解Android Bitmap的各种操作

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注