1.Bitmap
在Android开发中经常会使用到Bitmap,而Bitmap使用不当很容易引发OOM。
Bitmap占用内存大小的计算公式为:图片宽度×图片高度×一个像素点所占字节数 ,因此减小这三个参数的任一值都可减小bitmap所占的内存大小(也可以通过Bitmap.getAllocationByteCount()方法来查看Bitmap所占内存大小)。
因此使用Bitmap时需要优化,防止引发内存溢出问题。优化方法有两种:①减少bitmap对内存的占用;②重用已经占用内存的bitmap空间或使用现有的bitmap,比如图片非常多时使用LruCache缓存机制。
2.减小内存占用
①减小宽高BitmapFactory.Options.inSampleSize
inSampleSize是BitmapFactory.Options的一个属性,改变它即可改变图片的宽高。如果该值设置为大于1的值(小于1的值即为1),就会请求解码器对原始图像进行二次采样,返回较小的图像以节省内存。
比如inSampleSize = 4,则返回的图像宽度为原始宽度的1/4,高度为原始高度的1/4,像素数目为原始像素数目的1/16。
inSampleSize属性通常配合inJustDecodeBounds属性使用,如果inJustDecodeBounds设置为true,则解码器将返回null(无位图),但outWidth/outHeight仍会设置字段,从而允许调用者查询位图而不必为其像素分配内存。
private fun sampleCompress(requestWidth: Int, requestHeight: Int) {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true //不分配内存空间,仅计算图片尺寸
BitmapFactory.decodeResource(resources, R.mipmap.timg, options)
Log.d(TAG, "bitmap outWidth:${options.outWidth}") //原始图片宽
Log.d(TAG, "bitmap outHeight:${options.outHeight}") //原始图片高
// 根据宽高要求,计算缩放倍数
var sampleSize = 1
if (requestWidth < options.outWidth || requestHeight < options.outHeight) {
sampleSize = max(options.outWidth * 1.0/requestWidth, options.outHeight * 1.0/requestHeight).toInt()
}
options.inJustDecodeBounds = false
options.inSampleSize = sampleSize
val bitmap = BitmapFactory.decodeResource( resources, R.mipmap.timg, options)
logBmInfo(bitmap) //缩放后的图片
}
原始Bitmap宽高各为1000,若要求宽高各为500 ,则得出inSampleSize为2。根据log可知,原始Bitmap占用内存大小为4000000B,但是经过使用inSampleSize属性压缩宽高,从而减小为原先1/4的内存占用。
②减小每个像素占用的字节数BitmapFactory.Options.inPreferredConfig
inPreferredConfig是BitmapFactory.Options的一个属性,默认值为Bitmap.Config.ARGB_8888,改变该配置,可改变一个像素点占用的字节数。
该属性中A代表透明度,R代表红色,G代表绿色,B代表蓝色。
位图使用像素的一格一格的小点来描述图像,计算机屏幕其实就是一张包含大量像素点的网格。在位图中,平时看到的图像是由每一个网格中的像素点的位置和色彩值决定的,每一点的色彩是固定的,而每个像素点色彩值的种类,产生了不同的位图Config,常见的有:
1)ALPHA_8:表示8位Alpha位图,A占8位,没有颜色,只有透明度 ,每个像素占用1个字节内存。
2)ARGB_4444(已废弃) :表示16位ARGB位图,即A占4位,R占4位,G占4位,B占4位,共占用2个字节 。
3)ARGB_8888:表示32位ARGB位图,即A占8位,R占8位,G占8位,B占8位,每个像素占用4个字节内存。
4)RGB_565:表示16位RGB位图,即R占5位,G占6位,B占5位,没有透明度,每个像素占用2个字节内存。
private fun argbCompress() {
val bm1 = BitmapFactory.decodeResource( resources, R.mipmap.timg)
logBmInfo(bm1)
val options = BitmapFactory.Options()
// 设配置为RGB_565
options.inPreferredConfig = Bitmap.Config.RGB_565
val bm2 = BitmapFactory.decodeResource( resources, R.mipmap.timg, options)
logBmInfo(bm2)
从执行日志结果可以看到优化后的Bitmap内存占用为未优化Bitmap大小的一半 ,长度和宽度没发生变化。RGB_565对不要求透明度的图来说视觉影像不大。
3.易错:压缩compress不能改变bitamp占用内存的大小
Bitmap的compress(Bitmap.CompressFormat format, int quality, OutputStream stream)方法是将位图的压缩版本写入指定的输出流,该方法可能需要几秒钟才能完成,因此最好在子线程中调用。(注:并非所有格式都直接支持所有位图配置,因此从BitmapFactory返回的位图可能具有不同的位深,并且可能丢失了每个像素的alpha值(例如JPEG仅支持不透明的像素))。
compress方法的参数:format是压缩图像格式,quality是压缩质量(根据format不同,quality压缩效果也不同),stream是写入压缩数据的输出流。
format为Bitmap.CompressFormat.JPEG,根据quality 0-100压缩;
format为Bitmap.CompressFormat.PNG,则quality参数就会失效,因为PNG图片是无损的,无法压缩;
format为Bitmap.CompressFormat.WEBP,它会比JPEG更加省空间,根据quality 0-100压缩。
compress压缩损失的是颜色精度,所需的存储空间变小了,但使用压缩后的流重新生成Bitmap并不会改变bitmap占用内存的大小,因为bitmap的宽高未改变,而且Bitmap.Config未改变,即一个像素所占用的字节数也未改变,所以最终bitmap所占的内存并没有改变。
private fun bitmapCompress(bitmap: Bitmap){
val out = ByteArrayOutputStream()
Log.d(TAG, "———— JPEG ————")
bitmap.compress( Bitmap.CompressFormat.JPEG, 30, out)
Log.d(TAG, "out byte count:${out.size()}")
val jpegArray = out.toByteArray()
val jpeg = BitmapFactory.decodeByteArray( jpegArray, 0, jpegArray.size)
logBmInfo(jpeg)
out.reset()
Log.d(TAG, "———— PNG ————")
bitmap.compress( Bitmap.CompressFormat.PNG, 30, out)
Log.d(TAG, "out byte count:${out.size()}")
val pngArray = out.toByteArray()
val png = BitmapFactory.decodeByteArray( pngArray, 0, pngArray.size)
logBmInfo(png)
out.reset()
Log.d(TAG, "———— WEBP ————")
bitmap.compress( Bitmap.CompressFormat.WEBP, 30, out)
Log.d(TAG, "out byte count:${out.size()}")
val webpArray = out.toByteArray()
val webp = BitmapFactory.decodeByteArray( webpArray, 0, webpArray.size)
logBmInfo(webp)
}
从执行日志结果如下,quality为30进行压缩时,JPEG格式和WEBP所占存储空间变小了(不一定WEBP格式所占存储空间小于JPEG格式),而PNG格式并未压缩。三者的流重新解码成bitmap,可见bitmap所占内存大小并未发生变化。
压缩后的流重新解码生成bitmap,展示出来会发现PNG格式无影响,JPEG格式和WEBP格式图片质量明显变差了。
4.Bitmap复用
除了减少bitmap对内存的占用,还有方案来优化,即重用已经占用内存的bitmap空间或使用现有的bitmap。
①重用BitmapFactory.Options.inBitmap
inBitmap是BitmapFactory.Options的一个属性,可以通过设置该属性来重用已经占用内存的bitmap空间。
但是Bitmap重用有一定限制:
1)在Android4.4之前,只能重用相同大小的Bitmap内存区域;
2)在4.4之后可以重用任何Bitmap的区域,只要这块内存比将要分配内存的Bitmap大就可以;
3)重用的bitmap是要可变的。
以Android4.4之后为例,先通过设置 options.inJustDecodeBounds为true来查询需加载的bitmap宽高,然后判断reuseBitmap是否符合重用,若符合则将其赋值给options.inBitmap属性,最终得到想要的bitmap,即重用了reuseBitmap的内存空间。
private fun getBitmap(): Bitmap {
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.mipmap.timg, options)
// 判断是否满足重用条件,这里就假设Bitmap.Config为ARGB_8888来计算内存大小
if (reuseBitmap.allocationByteCount >= options.outWidth * options.outHeight * 4) {
// reuseBitmap为可变的重用bitmap
options.inBitmap = reuseBitmap
}
options.inJustDecodeBounds = false
return BitmapFactory.decodeResource( resources, R.mipmap.timg, options)
}
②LruCache
在使用RecyclerView时,如果itemView中含有图片,滑动时会导致bitmap不断重新创建,从而浪费内存空间。此时,可使用LruCache来缓存bitmap,再次需要时从缓存取出即可,无需重新创建。
private val memoryCache = object : LruCache
override fun sizeOf(key: String, value: Bitmap): Int {
// 告知lruCache bitmap所占内存大小
return value.allocationByteCount
}
}
fun putBitmap(key: String, bitmap: Bitmap) {
memoryCache.put(key, bitmap)
}
fun getBitmap(key: String): Bitmap? {
return memoryCache.get(key)
}
5.加载巨图
加载图片时,一般为了尽可能避免OOM都会按照如下做法:
1)对于图片显示:根据需要显示图片控件的大小对图片进行压缩显示。
2)如果图片数量非常多:则会使用LruCache等缓存机制,将所有图片占据的内容维持在一个范围内。
其实对于图片加载还有一种情况,就是单个图片非常巨大,并且还不允许压缩。比如显示:世界地图、清明上河图、微博长图等。
对于这种需求,首先不允许压缩,要按照原图尺寸加载,那么屏幕肯定是不够大的,并且考虑到内存的情况,不可能一次性将整图加载到内存中,所以肯定是局部加载,那么就需要用到一个类:BitmapRegionDecoder。其次,既然屏幕显示不完,那么就要添加一个上下左右拖动的手势,让用户可以拖动查看。
①BitmapRegionDecoder
BitmapRegionDecoder主要用于显示图片的某一块矩形区域。
BitmapRegionDecoder提供了一系列的newInstance方法来构造对象,支持传入文件路径,文件描述符,文件的inputstrem等。比如:
BitmapRegionDecoder bitmapRegionDecoder =BitmapRegionDecoder.newInstance(inputStream, false);
这里传入了需要处理的图片,接下来就要指定显示的区域了:
Bitmap Bitmap = bitmapRegionDecoder.decodeRegion(rect, options);
第一个参数很明显是一个rect,第二个参数是BitmapFactory.Options,通过它可以控制图片的inSampleSize,inPreferredConfig等。返回值就是加载的局部图片。
BitmapRegionDecoder使用举例:
InputStream inputStream = getAssets().open( "world.jpg");
//获得图片的宽、高
BitmapFactory.Options tmpOptions = new BitmapFactory.Options();
tmpOptions.inJustDecodeBounds = true;
BitmapFactory.decodeStream(inputStream, null, tmpOptions);
int width = tmpOptions.outWidth;
int height = tmpOptions.outHeight;
//设置显示图片的中心区域
BitmapRegionDecoder bitmapRegionDecoder = BitmapRegionDecoder.newInstance( inputStream, false);
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
Bitmap bitmap = bitmapRegionDecoder.decodeRegion(new Rect(width / 2 - 100, height / 2 - 100, width / 2 + 100, height / 2 + 100), options);
mImageView.setImageBitmap(bitmap);
这样就实现了使用BitmapRegionDecoder去加载assets中的图片,调用bitmapRegionDecoder.decodeRegion解析图片的中间矩形区域,返回bitmap,最终显示在ImageView上。
②自定义显示大图控件
为了滑动查看整个图,可以自定义一个控件去显示巨图,首先Rect的范围就是自定义View的大小,然后根据用户的移动手势,不断去更新Rect的参数即可。
参考鸿洋大神的https://blog.csdn.net/lmj623565791/article/details/49300989/