掌握如何使用通用的技术来加载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。解决方案如下12345678910111213141516171819202122232425262728293031323334353637383940public 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 inSampleSizeoptions.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 -->truefinal 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线程的交互。
- 如果调用BitmapFactory.decodeFile()函数或者是从网络上加载图片资源的话就不应该放在主线程中进行。官网建议的是采用AsyncTask来异步执行解析操作,也就是把decode操作放在了doInBackGround里,也很好理解,不过AsyncTask会持有外部组件imageView的引用,采取WeakReference
|
|
- 缓存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对象的软引用(内存不足时才会被回收)和弱引用(垃圾回收操作执行了就会被回收)来实现,然而到了现在持有的是bitmap的强引用,new LinkedHashMap
- 磁盘缓存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无法复用之前的图片数据。
用法如下:
12mBitmapOption.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 :
|
|