Android Bitmap防止内存溢出

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) //缩放后的图片

}

d1028b67a97940309e3c133866d5a0ed.png

原始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对不要求透明度的图来说视觉影像不大。

e51a0d65ca2b41b0b2628c05a65c824d.png

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所占内存大小并未发生变化。

af79ffe172e546329ee4b1a257c25303.png

压缩后的流重新解码生成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<String, Bitmap>(4*1024*1024) { // 缓存4M图片

    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/

猜你喜欢

转载自blog.csdn.net/zenmela2011/article/details/129923600