Bitmap相关tips的笔记

  掌握如何使用通用的技术来加载bitmap对象是保证UI界面流畅性的前提之一。如果在应用中不注意这一点的话,bitmap对象很快就消耗掉应用的内存,最终导致内存溢出异常。

java.lang.OutofMemoryError:bitmap size exceeds VM budget

  下面的内容就是如何在android中高效加载Bitmap的一些tips,参考自官网的training。

  • 高效加载Bitmap
    • BitmapFactory类提供一些方法来构造 bitmap(decodeByteArray(),decodeFile(),decodeResource()..)这些方法为了构建bitmap 会调用内存,因此如果不加注意的话很容易导致OOM异常(毕竟bitmap都不小)。如果设置BitmapFactory.Option类的 inJustDecodeBounds属性为true的话可以让系统只是简单的解析原始图片的宽和高等信息(进而判断 这个bitmap资源的大小是否跟imageView大小一致,如果大于imageView的话就应该做压缩从而减少加载 这个bitmap所消耗的资源),而不是粗暴的去加载整个图片(如果只是读取宽高信息完全没必要去下载整个图 片).如果设置是false(默认也是false)的话,option的outWidth,outHeight,outMimeType返回的 都是null.这个inJustDecodeBounds属性帮助我们不去加载整个图片就可以读取图片的基本信息。
    • 例如有如下情景:一张bitmap原始大小为1024x768,但是imageView的大小却只有128x96大小,bitmap尺寸远远大于imageView。解决方案如下
      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
      public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
      int reqWidth, int reqHeight) {
      final BitmapFactory.Options options = new BitmapFactory.Options();
      options.inJustDecodeBounds = true;
      BitmapFactory.decodeResource(res, resId, options);//解析出bitmap的原始大小信息
      // Calculate inSampleSize
      options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
      // 解析完了后记得把options.inJustDecodeBounds属性关掉
      options.inJustDecodeBounds = false;
      return BitmapFactory.decodeResource(res, resId, options);
      //这个时候options.inJustDecodeBounds为false那么返回的bitmap就是完全解析了的对象
      }
      public static int calculateInSampleSize(
      BitmapFactory.Options options, int reqWidth, int reqHeight) {
      //传递进来的option.injustDecode -->true
      final int height = options.outHeight;
      final int width = options.outWidth;
      int inSampleSize = 1;
      if (height > reqHeight || width > reqWidth) {
      final int halfHeight = height / 2;
      final int halfWidth = width / 2;
      // Calculate the largest inSampleSize value that is a power of 2 and keeps both
      // height and width larger than the requested height and width.
      while ((halfHeight / inSampleSize) > reqHeight
      && (halfWidth / inSampleSize) > reqWidth) {
      inSampleSize *= 2;
      }
      }
      return inSampleSize;
      }
      使用如下:
      mImageView.setImageBitmap(decodeSampledBitmapFromResource(
      getResources(), R.id.myimage, 128 , 96));

总结就是如果Bitmap的尺寸远大于imageView的话可以尝试加载小bitmap从而节省内存空间: 开启Options的InJustDecodeBounds开关读取Bitmap资源的宽高. 接着计算targetSize是原图的几分之几, 把这个值作为options的sample, 最后才通过BitmapFactory.decode*()传入这个options,最终加载到的bitmap就是最节省内存资源的bitmap了。

  • 在非UI线程中加载位图
    • 如果调用BitmapFactory.decodeFile()函数或者是从网络上加载图片资源的话就不应该放在主线程中进行。官网建议的是采用AsyncTask来异步执行解析操作,也就是把decode操作放在了doInBackGround里,也很好理解,不过AsyncTask会持有外部组件imageView的引用,采取WeakReference就可以防止内存泄漏的问题。
    • 不过很多人建议是在android代码中减少使用AsyncTask(为何减少使用见另一篇博文).而是采用Thread+Handler来解决这个问题。把decode操作放在线程的run()方法里面,然后利用handler来实现自线程和UI线程的交互。
1
2
3
4
5
6
7
8
9
10
11
Runnable loadBitmapTask implements Runnable{
@Override
public void run(){
Bitmap bitmap = BitmapFactory.decodeFile(filePath);//IO操作
Message msg = Message.obtain();
msg.what = 1;
msg.obj = bitmap;
mHandler.sendMessage(msg);
}
}
  • 缓存bitmap的tips
    • 历史: 在之前比较流行的缓存持有的是bitmap对象的软引用(内存不足时才会被回收)和弱引用(垃圾回收操作执行了就会被回收)来实现,然而到了现在持有的是bitmap的强引用,new LinkedHashMap(0, 0.75f, true);,从android2.3开始,垃圾回收变的更具侵略性,有时候根本不遵守软/弱引用回收原则,而是直接回收,这样导致利用软/弱引用作为缓存机制很不可靠。此外,在android3.0之前,bitmap的像素数据还会存在于本地内存中,致使无法被释放,最终导致应用短暂的超过内存限制从而导致崩溃。到了3.0后,像素数据开始从native memory转到Dalvik的heap中,使得像素数据不再和bitmap分离开来。(也为inBitmap属性做了铺垫)
    • cache的大小应该根据自己应用的实际情况设计,综合考虑图片尺寸以及质量规格,屏幕展示图片数量,是否有高频次图片等等因素…
  • 磁盘缓存bitmap
    • 应用如果被打电话这样的任务给阻塞的话,应用则进入后台,如果用户此时继续执行启动其他应用程序的操作,导致手机内存不够用了,系统则根据应用优先级回收内存,如果此时你的应用的优先度不及其他应用的话很可能就被回收了,如果再回到应用最初加载的那些图片存储在LruCache也已经被回收了,导致不得不再次重新加载。这种情况下考虑磁盘缓存可以解决这歌问题。
    • 在LruCache如果出现比较频繁出现的bitmap解决方案是把它们放到另一个LruCache对象里单独使用,而磁盘缓存如果出现比较常用的bitmap解决方法是使用contentprovider.
    • LruCache对象的get,put操作在UI线程执行无影响,但是磁盘缓存是IO操作,不应该放在UI线程中执行,否则发生异常。
    • 如果activity的配置发生变化导致重新create,而bitmap不应该被重新构建,此时可以用到上一篇我讲的使用Fragment缓存数据, 在RetainedFragment里面建立一个LruCache缓存好相应的bitmaps.
  • 复用bitmap的像素数据

    • 使用BitmapFactory.Option.inBitmap属性可以告知Bitmap解码器去尝试使用已经存在的像素数据内存区域。
    • 在sdk11-18的版本中,想要复用一个bitmap的像素数据,来者的大小必须跟被复用的bitmap大小完全一致,但是从19开始,来者的大小可以小于或者等于被复用的bitmap的尺寸。另外就是两者的解码格式都必须保持一致。
    • 需要注意的一点是,想要利用inBitmap特性的话必须要把BitmapFactory.Option.inMutable属性设置为true也就是允许bitmap的像素数据是可以变动的,否则的话decode bitmap无法复用之前的图片数据。
    • 用法如下:

      1
      2
      mBitmapOption.inBitmap = mCurrentBitmap;
      mOtherBitmap = BitmapFactory.decodeFile(fileName,mBitmapOption);
    • 跟inBitmap属性对应的是inPurgeable属性,这个属性到了lollipop便给废弃掉了。不过设置inPurgeable为true的话是告诉系统如果内存不够用的话可以回收当前由BitmapFactory.decode()创建的bitmap的像素数据,当这个bitmap再次要被解析来使用的时候,系统会检测另外一个属性inInputShareable,只有这个属性设置为true的时候这个bitmap会持有一个inputStream的弱引用(需要的时候直接把像素数组取出来),如果设置为false,那么将复制这个像素数组(又是消耗内存),那么这个inPurgeable想要达到的效果也不复存在。(虽然这个属性建议使用inBitmap来替代,但是Fesco框架在5.0表现不好原因体现在这个属性上所以需要注意)

    • fresco在提升bitmap复用的角度选择了inPurgeable而没有选择inBitmap(google建议不要继续使用)还是从兼容角度来考虑的,毕竟inBitmap首先是从android3.0才有的,无法兼容2.3,其次在3.0-4.4版本中bitmap尺寸必须一致才能复用,限制过多不利于fresco应用于低版本机型,所以选择了fresco。这也是为什么在tmall apad时在nexus9内存表现不佳的原因。

如何为低版本Bitmap(没有alpha channel)添加alpha channel呢?

rgb->argb :

1
2
3
4
5
6
7
8
9
private Bitmap adjustOpacity(Bitmap bitmap, int opacity) {
Bitmap mutableBitmap = bitmap.isMutable()
? bitmap
: bitmap.copy(Bitmap.Config.ARGB_8888, true);
Canvas canvas = new Canvas(mutableBitmap);
int color = (opacity & 0xFF) << 24; //把alpha通道向左移 3 * 8 位
canvas.drawColor(color, PorterDuff.Mode.DST_IN);// 拿到copy出来的bitmap的canvas对象的基础上添加alpha像素数据
return mutableBitmap;
}