性能优化:Bitmap内存大小优化的几种常见方式
Android中的bitmap是比较占用内存的,bitmap的大小直接影响到了应用占用内存的大小。bitmap占用内存大小的计算方式为:
bitmap内存大小 = bitmap宽度(px) * bitmap长度(px) * 一个像素点占用的字节数
BitmapFactory给我们提供了多个decode方法,我们可以从不同的数据源中加载bitmap,如下图所示:
一个像素点占用的字节数
对应的参数是Bitmap.Config,它是一个枚举类,具体取值如下:
而每个新建的Bitmap的默认Config的值就Bitmap.Config.ARGB_8888,它表示一个像素点占用4个字节((8 + 8 + 8 + 8) / 8 = 4byte)。
改变Bitmap内存大小
根据上面的原理,我们可以从两个方面减少Bitmap的内存占用,一个是改变Bitmap的宽高,另一个是改变Bitmap.Config的值,将Bitmap.Config.ARGB_8888改为占用字节更少的Bitmap.Config.ARGB_4444或者Bitmap.Config.RGB_565。
具体有以下几种方法:
1、采样率压缩(改变Bitmap大小)。
2、通过martix进行压缩(改变Bitmap大小)。Bitmap.createBitmap或者Bitmap.createScaledBitmap方法。
3、更改Bitmap.Config格式。
具体示例如下:
我们先来看下原图大小:
原图大小为640px * 360px,且是放置在assets目录下的,表示系统不会有缩放操作;如果是放在对应的drawable目录下,则会由系统进行相应的缩放操作。
1、采样率压缩(改变Bitmap大小)。
val assetFileDescriptor = assets.openFd("maomi.jpg")
val fileInputStream = assetFileDescriptor.createInputStream()
val bmpInSampleSizeJpg = BitmapOptionsUtil.decodeSampledBitmapFromFileStream(
fileInputStream,
ScreenUtils.dip2px(this, 80f),
ScreenUtils.dip2px(this, 45f)
)
val descBmpConfigJpg =
"height:${bmpInSampleSizeJpg?.height},\nwidth:${bmpInSampleSizeJpg?.width},\nallocationByteCount:${bmpInSampleSizeJpg?.allocationByteCount}byte,\n" +
"byteCount:${bmpInSampleSizeJpg?.byteCount}byte,\nrowBytes:${bmpInSampleSizeJpg?.rowBytes}byte,\ndensity:${bmpInSampleSizeJpg?.density}"
tvInSampleSizeJpgInfo.text = "Jpg通过inSampleSize压缩后的信息:$descBmpConfigJpg"
ivInSampleSizeJpg.setImageBitmap(bmpInSampleSizeJpg)
可以明显的看到,图片的宽高各自缩小为原来的1/4,bitmap占用内存大小为原来的1/16。
采样率压缩Bitmap大小的方式,适用于与原有的图片对象宽高比较大,而目标Bitmap的尺寸比较小,此时就没有必要将Bitmap按照原有的大尺寸加载进来,可以有效的避免内存浪费和OOM。具体采样率(inSampleSize)的计算,请看BitmapOptionsUtil。
2、通过martix进行压缩(改变Bitmap大小)。
val matrix = Matrix()
matrix.setScale(0.1f, 0.1f)
val bmpMatrixJpg = Bitmap.createBitmap(
bmpOriginJpg, 0, 0, bmpOriginJpg.getWidth(),
bmpOriginJpg.getHeight(), matrix, true
)
// Bitmap.createScaledBitmap内部也会使用Matrix进行缩放
// val bmpMatrixJpg = Bitmap.createScaledBitmap(
// bmpOriginJpg,
// ScreenUtils.dip2px(this, 60f),
// ScreenUtils.dip2px(this, 45f),
// true
// )
val descBmpConfigJpg =
"height:${bmpMatrixJpg?.height},\nwidth:${bmpMatrixJpg?.width},\nallocationByteCount:${bmpMatrixJpg?.allocationByteCount}byte,\n" +
"byteCount:${bmpMatrixJpg?.byteCount}byte,\nrowBytes:${bmpMatrixJpg?.rowBytes}byte,\ndensity:${bmpMatrixJpg?.density}"
tvMatrixJpgInfo.text = "Jpg通过Matrix压缩后的信息:$descBmpConfigJpg"
ivMatrixJpg.setImageBitmap(bmpMatrixJpg)
由于我们设置的缩放比是0.1f,也就是宽高均是之前的1/10,所以压缩后的bitmap占用的内存大小变为原来的1/100。
这种情况适用原图大小和目标bitmap大小均已知的情况。
3、更改Bitmap.Config格式。
val option = BitmapFactory.Options()
option.inPreferredConfig = Bitmap.Config.ARGB_4444
// option.inPreferredConfig = Bitmap.Config.RGB_565 // 对透明度没要求的话可以试一下rgb_565
val bmpBmpConfigJpg = BitmapFactory.decodeStream(assets.open("maomi.jpg"), null, option)
val descBmpConfigJpg =
"height:${bmpBmpConfigJpg?.height},\nwidth:${bmpBmpConfigJpg?.width},\nallocationByteCount:${bmpBmpConfigJpg?.allocationByteCount}byte,\n" +
"byteCount:${bmpBmpConfigJpg?.byteCount}byte,\nrowBytes:${bmpBmpConfigJpg?.rowBytes}byte,\ndensity:${bmpBmpConfigJpg?.density}"
tvBmpConfigJpgInfo.text = "Jpg通过Bitmap.Config压缩后的信息:$descBmpConfigJpg"
ivBmpConfigJpg.setImageBitmap(bmpBmpConfigJpg)
我们将Bitmap.Config的值改为了根据输出的byteCount的值改为了Bitmap.Config.ARGB_4444,根据byteCount输出的值可以明显的看到,bitmap的内存大小减少了一半。
如果对透明度没要求的话可以试一下Bitmap.Config.RGB_565。
这种情况适用于对图片分辨率要求不高的情况。
通过Bitmap#compress方法压缩,质量压缩。
还有一种很重要的压缩方式,通过Bitmap#compress方法,修改quality的值,来改变Bitmap生成的字节流的大小。这种方法不会改变Bitmap占用的内存大小。
质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的。图片的长,宽,像素都不变,那么bitmap所占内存大小是不会变的。这里改变的是bitmap对应的字节数组的大小,适合去传递二进制的图片数据,比如微信分享。
val bytearray = getBytesFromCompressBitmap(bmpOriginJpg, 32 * 1024)
val bmpQualityJpg = BitmapFactory.decodeByteArray(bytearray, 0, bytearray.size)
val descQualityJpg =
"height:${bmpQualityJpg.height},\nwidth:${bmpQualityJpg.width},\nallocationByteCount:${bmpQualityJpg.allocationByteCount}byte,\n" +
"byteCount:${bmpQualityJpg.byteCount}byte,\nrowBytes:${bmpQualityJpg.rowBytes}byte,\ndensity:${bmpQualityJpg.density},\n" +
"bytearray:${bytearray.size}"
tvQualityJpgInfo.text = "Jpg进行Quality压缩后的信息:$descQualityJpg"
ivQualityJpg.setImageBitmap(bmpQualityJpg)
/**
* 将Bitmap的字节流压缩为目标大小
* @targetSize 单位为Byte
*/
private fun getBytesFromCompressBitmap(
bitmap: Bitmap,
targetSize: Int
): ByteArray {
val baos = ByteArrayOutputStream()
var quality = 100
bitmap.compress(Bitmap.CompressFormat.PNG, quality, baos)
var bytes = baos.toByteArray()
while (bytes.size > targetSize && quality >= 5) {
quality -= 5
if (quality < 0) {
quality = 0
}
// 重置,不然会累加
baos.reset()
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos)
bytes = baos.toByteArray()
}
try {
baos.close()
} catch (e: Exception) {
e.printStackTrace()
}
return bytes
}
可以看到,质量压缩不会改变原有bitmap的大小,它改变的是通过Bitmap#compress方法的字节流。
具体开发过程中,可以根据需要自行选择合适的方式。
项目地址
具体页面地址:提供了jpg和png两种图片格式的demo。