Bitmap篇之Bitmap优化(二)

191 阅读4分钟

Bitmap优化

Bitmap作为Android中常用的图片处理类,可以加载和显示各种格式的图片。但使用bitmap对象会占用大量内存,尤其是高分辨率图片,极易导致内存溢出,因此需进行优化,提高Android应用的性能。

bitmap内存

一张图片的大小与文件格式(png、jpg),跟其加载至内存所占用的大小无关,而是根据图片分辨率和每个像素点的大小决定的。

  • 图片尺寸:图片尺寸越大,占用内存越多
  • 图片格式:不同的图片格式(ARGB_8888、RGB_565...)具有不同的存储方式,占用内存也会不同

内存 = 图片宽度 * 图片高度 * 每个像素所占字节数

例:一张 1080 * 1920 的RGBA格式的Bitmap内存为:1080 * 1920 * 4 = 8294400字节(约8M)

注意BitmapFactory.decodeResource(resources, R.drawable.icon)

使用BitmapFactory加载Bitmap时,若资源为res目录下资源,加载进内存时会做一步转换操作(根据当前设备的dpi与资源目录对应的dpi转换分辨率)

android_dpi.png

转换后宽 = 原宽 * (设备dpi / 目录对应dpi)

转换后高 = 原高 * (设备dpi / 目录对应dpi)

优化思路

优化目的无非就是减少其占用内存大小,可从压缩和资源复用回收入手。压缩要么就是改变其宽高,要么就是减少其单个像素占用内存大小。

可从以下几个方式入手达成优化目的

  • 采样率压缩:图片质量会下降,慎用
  • 质量压缩:图片细节会丢失,慎用
  • 低色彩格式压缩:有透明度图片不适合
  • bitmap复用
  • 硬件bitmap:API26及之后设备
  • bitmap回收

采样率压缩

通过降低bitmap分辨率减少内存占用,使用BitmapFactory.Options类的inSampleSize属性控制采样率。该属性值表示解码后的bitmap宽高为原bitmap的1/inSampleSize。

例:inSampleSize=2,压缩后内存为原图的1/4

BitmapFactory.Options参数

inSampleSize:采样率(通过改变图片宽高达成优化目的)

inPreferredConfig:色彩模式

inJustDecodeBounds:是否仅解码图像边界(为ture,可获取图像宽高而不加载至内存)

inPurgeable:是否可被系统回收

inMutable:是否可变(软硬画布时会用到,硬画布获取bitmap无法直接在canvas上绘制),可copy后再绘制val newBitmap = b.copy(Bitmap.Config.ARGB_8888, b.isMutable)

// 加载本地资源图片,并进行inSampleSize压缩处理
fun loadBitmap(context: Context, @DrawableRes resId: Int, reqWidth: Int, reqHeight: Int): Bitmap {
    val options = BitmapFactory.Options().apply {
        inJustDecodeBounds = true
    }
    BitmapFactory.decodeResource(context.resources, resId, options)

    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)

    options.inJustDecodeBounds = false
    return BitmapFactory.decodeResource(context.resources, resId, options)
}

// 计算采样率
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
    val (w, h) = options.run { outWidth to outHeight }
    var inSampleSize = 1
    if (w > reqWidth || h > reqHeight) {
        val halfWidth = w / 2
        val halfHeight = h / 2
        while ((halfWidth / inSampleSize) >= reqWidth && (halfHeight / inSampleSize) >= reqHeight) {
            inSampleSize *= 2
        }
    }
    return inSampleSize
}

质量压缩

  • 质量压缩不会减少图片的像素,而是在保持像素前提下通过改变图片的位深和透明度来实现压缩目的
  • 压缩后bitmap所占用内存不会改变,压缩后数据被写入文件
  • png是无损压缩,改变quality对其无作用

Bitmap.compress(format: Bitmap.CompressFormat, quality: Int, stream: OutputStream)

fun compress(context: Context, resId: Int = R.drawable.image, quality: Int = 100) {
    val bitmap = BitmapFactory.decodeResource(context.resources, resId)
    val outStream = ByteArrayOutputStream()
    bitmap.compress(Bitmap.CompressFormat.PNG, quality, outStream)
    val bytes = outStream.toByteArray()
}

fun test() {
    // 资源大小4.93KB
    val bitmap = BitmapFactory.decodeResource(resources, R.drawable.icon_launcher)

    // 9
    LogTool.i("original count: ${bitmap.width * bitmap.height / 1024}")
    val outputStream = ByteArrayOutputStream()
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)

    // JPEG 100-9 50-9    PNG 100-9 50-9
    LogTool.i("compress count: ${bitmap.width * bitmap.height / 1024}")

    // JPEG 100-6 50-1    PNG 100-4 50-4
    LogTool.i("compress file: ${outputStream.toByteArray().size / 1024}")
}

低色彩格式压缩

Bitmap支持多种色彩格式,每种格式所占字节不同。可通过修改格式降低内存

  • ARGB_8888:每个像素包含8位透明度及红绿蓝各8位,共4个字节
  • ARGB_4444:每个像素包含透明度红绿蓝各4位,共2个字节
  • RGB_565:每个像素包含5位红、6位绿、5位蓝,共2个字节
  • ALPHA_8:每个像素包含8位透明度,共1个字节
fun bitmapConfig(context: Context, resId: Int = R.drawable.image, config: Bitmap.Config = Bitmap.Config.RGB_565) {
    val options = BitmapFactory.Options()
    options.inPreferredConfig = config
    val bitmap = BitmapFactory.decodeResource(context.resources, resId, options)
}

bitmap复用

bitmap对象可复用,避免频繁创建和销毁bitmap对象

// 方式一:BitmapFactory.Options属性
options.inBitmap = bitmap

// 方式二:使用LruCache缓存bitmap对象
val lruCache = LruCache<String, Bitmap>(200)

fun getBitmap(key: String, context: Context): Bitmap? {
    var bitmap = lruCache.get(key)
    if (bitmap == null) {
        bitmap = BitmapFactory.decodeResource(context.resources, R.drawable.default)
        lruCache.put(key, bitmap)
    }
    return bitmap
}

硬件bitmap

Android8(26)引入硬件Bitmap,可将Bitmap数据存储至GPU内存中,从而减少内存占用,并提高绘制效率

Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.HARDWARE);
val canvas = Canvas(bitmap)
canvas.drawPoint(x, y, paint)

bitmap回收

不使用时及时回收,防止内存泄露

bitmap.recycle()