图片压缩
问题
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 粗略降采样,再用 Matrix 或 Bitmap.createScaledBitmap() 精确缩放。
Q2: 相机拍照的图片如何高效压缩上传?
答案:
推荐流程:
- 采样压缩:根据目标尺寸计算
inSampleSize,避免加载原图到内存 - EXIF 旋转:读取 EXIF 信息,用 Matrix 旋转到正确方向
- 质量压缩:
compress()到目标文件大小 - 格式转换:使用 WebP 进一步减小体积