bitmap缩放怎么优化性能

71 阅读3分钟

一、优先在“解码阶段”缩小(最省)

1) 老 API(<28):

BitmapFactory.Options.inSampleSize

fun decodeSampledBitmap(path: String, reqW: Int, reqH: Int): Bitmap? {
    val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
    BitmapFactory.decodeFile(path, opts)

    // 计算采样率(向下取整,系统会近似为 2 的幂)
    var inSample = 1
    val (w, h) = opts.outWidth to opts.outHeight
    if (h > reqH || w > reqW) {
        val halfH = h / 2
        val halfW = w / 2
        while ((halfH / inSample) >= reqH && (halfW / inSample) >= reqW) {
            inSample *= 2
        }
    }

    return BitmapFactory.decodeFile(path, BitmapFactory.Options().apply {
        inSampleSize = inSample
        inPreferredConfig = Bitmap.Config.RGB_565   // 不透明图可用,解码更快更省内存
        inMutable = true                             // 方便复用
        // inBitmap = 可从池中复用的 Bitmap(尺寸/格式兼容)
    })
}

要点:

  • 先测尺寸再解码(inJustDecodeBounds=true),按目标尺寸算 inSampleSize。

  • 不透明图用 RGB_565,内存直接减半;透明图仍用 ARGB_8888。

  • 结合 inBitmap + 对象池(或用 Glide 的 BitmapPool)减少分配/GC。

2) 新 API(≥28):

ImageDecoder.setTargetSize

val src = ImageDecoder.createSource(File(path))
val bmp = ImageDecoder.decodeBitmap(src) { decoder, _, _ ->
    decoder.allocator = ImageDecoder.ALLOCATOR_HARDWARE // 硬件位图(仅显示,不可像素级改写)
    decoder.setTargetSize(reqW, reqH)                   // 直接在解码阶段缩放到目标尺寸
    // decoder.setMemorySizePolicy(ImageDecoder.MEMORY_POLICY_LOW_RAM) // 低内存设备可选
}

要点:

  • 直接目标尺寸解码,最省 CPU/内存;EXIF 方向也会自动处理。

  • 需要像素操作就不要用 ALLOCATOR_HARDWARE,用默认/软件分配。

3) 密度缩放(Skia 解码内置缩放)

对固定 UI 尺寸的图片,可设置 inDensity/inTargetDensity,让解码时带比例缩放(比二次 createScaledBitmap 更省):

BitmapFactory.Options().apply {
    inScaled = true
    inDensity = srcW
    inTargetDensity = reqW
}

二、避免“解码后再缩放”(能不做就不做)

  • Bitmap.createScaledBitmap(...) 会做一次重采样 + 新分配;如果已经按目标尺寸解码,大多数场景不需要再调它。

  • 如仅用于显示,交给 GPU:ImageView 的 scaleType / Compose 的 contentScale(CENTER_CROP/FIT_CENTER/...)由渲染管线完成变换,不产生新 Bitmap。

三、显示阶段的做法(尽量不分配新图)

  • Canvas 绘制缩放:canvas.drawBitmap(bitmap, srcRect, dstRect, paint),Skia 在 GPU/RenderThread 做采样;paint.isFilterBitmap = true(或默认 FILTER 标志)保证质量。

  • Compose:Image(painter, contentScale = ContentScale.Crop/Fit/FillWidth...),只传目标 Modifier.size/width/height,前面解码已对齐目标尺寸

四、大图/长图/超大分辨率

  • 分块解码:BitmapRegionDecoder(或 ≥28 用 ImageDecoder + setTargetSampleSize + 自己切 tile)只解码可视区域或缩略图,滑动时按需解码。

  • 缩略图需求:用 MediaStore/ThumbnailUtils 或图片库(Glide/Coil)的 downsample,直接拉小。

五、内存与复用(关键性能点)

  • 维护 LruCache<String, Bitmap> 或直接用 Glide/Coil

    • Glide:override(w, h) + downsample() + diskCacheStrategy(AUTOMATIC);复用 BitmapPool
    • Coil:size(w, h) + allowHardware(true/false) + 内置内存/磁盘缓存。
  • 复用:inMutable=true + inBitmap=pool.get(...),但需确保尺寸/格式兼容(API19+ 更宽松)。

六、线程与异步

  • 解码/缩放一定放 IO 线程(Dispatchers.IO);UI 线程只做赋值与绘制。

  • 滚动列表中加载:分页 + 占位图 + 取消无效请求,避免同时大量解码。

七、一些“性价比”技巧

  • WebP/AVIF(有损)在网络/存储层省流 + 快速解码;但这属于编码策略,不是缩放本身。
  • 仅显示(无需像素处理)→ 尽量使用 硬件位图(≥O):减少 Java 层拷贝与 GC。
  • 需要读写像素 → 用软件位图;避免硬件↔软件频繁转换。

选型小结(决策表)

  • 已知目标显示尺寸:用 inSampleSize(<28)或 ImageDecoder.setTargetSize(≥28)直接解码到目标大小 ✅

  • 需要频繁缩略显示:统一走图片库(Glide/Coil)并指定 override/size,交给库完成下采样与缓存 ✅

  • 巨型长图/地图:分块/惰性解码(BitmapRegionDecoder)✅

  • UI 缩放:尽量让 GPU 做(scaleType/contentScale),不要再创建新的缩放 Bitmap ✅

  • 性能瓶颈:检查是否“先大图解码再缩放”、是否重复 createScaledBitmap、是否在主线程解码、是否缺少缓存/复用 ❌