跳到主要内容

图片压缩

问题

Android 中有哪些图片压缩方案?如何在保证画质的前提下减小图片大小?

答案

压缩方式分类

方式原理内存文件大小画质
采样压缩降低像素数量(inSampleSize)✅ 降低✅ 降低略降
质量压缩改变编码压缩率❌ 不变✅ 降低可控
尺寸缩放Matrix 缩放✅ 降低✅ 降低可控
格式转换使用 WebP/AVIF❌ 不变✅ 降低几乎不变

采样压缩(最常用,降低内存)

fun decodeSampledBitmap(path: String, reqWidth: Int, reqHeight: Int): Bitmap {
val options = BitmapFactory.Options()

// 第一步:只获取尺寸,不加载像素
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(path, options)

// 第二步:计算采样率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false

return BitmapFactory.decodeFile(path, options)
}

fun calculateInSampleSize(
options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int
): Int {
val (width, height) = options.outWidth to options.outHeight
var inSampleSize = 1

if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
// inSampleSize 必须是 2 的幂
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}

质量压缩(减小文件体积)

fun compressQuality(bitmap: Bitmap, maxSizeKB: Int): ByteArray {
val baos = ByteArrayOutputStream()
var quality = 100

bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos)
while (baos.toByteArray().size / 1024 > maxSizeKB && quality > 10) {
baos.reset()
quality -= 10
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, baos)
}
return baos.toByteArray()
}
质量压缩不能减少内存

Bitmap.compress() 只影响输出文件大小,不影响 Bitmap 在内存中的占用。要减少内存占用需要使用采样压缩或尺寸缩放。

WebP 格式

// WebP 比 JPEG 小 25-34%,比 PNG 小 26%
fun saveAsWebP(bitmap: Bitmap, file: File) {
file.outputStream().use { out ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 80, out)
} else {
@Suppress("DEPRECATION")
bitmap.compress(Bitmap.CompressFormat.WEBP, 80, out)
}
}
}

Luban 压缩算法

Luban 模拟微信的压缩策略,根据图片尺寸自动选择采样率和压缩质量:

Luban.with(context)
.load(imageFile)
.ignoreBy(100) // 小于 100KB 不压缩
.setTargetDir(cacheDir.path)
.setCompressListener(object : OnCompressListener {
override fun onStart() {}
override fun onSuccess(file: File) { /* 压缩后的文件 */ }
override fun onError(e: Throwable) {}
})
.launch()

常见面试问题

Q1: inSampleSize 为什么只能是 2 的幂?

答案

BitmapFactory 内部使用的解码器会将 inSampleSize 向下取整到最近的 2 的幂(如 3 变成 2,5 变成 4)。这是因为基于 2 的幂的降采样可以通过简单的像素跳跃实现,不需要复杂的插值计算,解码速度更快。如果需要精确缩放,应先用 inSampleSize 粗略降采样,再用 MatrixBitmap.createScaledBitmap() 精确缩放。

Q2: 相机拍照的图片如何高效压缩上传?

答案

推荐流程:

  1. 采样压缩:根据目标尺寸计算 inSampleSize,避免加载原图到内存
  2. EXIF 旋转:读取 EXIF 信息,用 Matrix 旋转到正确方向
  3. 质量压缩compress() 到目标文件大小
  4. 格式转换:使用 WebP 进一步减小体积

相关链接