Android 中如何压缩并保存图片,提高 RecyclerView 加载速度?
拍照完成后,可以生成并保存一份适合列表显示的小图,让 RecyclerView 直接加载该文件。已有的 Bitmap 可以通过 Bitmap.compress() 写入文件,也可以写入可写 Uri 对应的输出流。
处理时要缩小图片的像素尺寸,并避免在进入页面或绑定列表项时重复处理。仅降低 JPEG 的 quality,通常不能解决大图解码后的内存占用问题。
区分图片尺寸和文件压缩质量
照片保存在磁盘上时通常采用 JPEG 等编码格式。加载为 Bitmap 后,内存占用主要取决于宽、高和像素格式。
即使 JPEG 文件变小,只要解码后的宽高没有变化,Bitmap 的内存占用通常也不会明显降低。
生成列表小图可以按以下顺序处理:
- 根据列表图片控件的实际显示尺寸确定目标尺寸,注意使用像素,而不是直接使用 dp 数值。
- 解码原图时进行采样,避免先把完整照片全部读入内存。
- 根据需要处理照片方向,再缩放到合适尺寸。
- 将结果编码保存为小图,并记录文件路径或
Uri。
使用 BitmapFactory 时,可以先通过 inJustDecodeBounds 读取尺寸,再设置 inSampleSize 进行采样解码。采样得到的是近似尺寸,必要时可以再缩放一次。拍摄的照片还可能携带 EXIF 方向信息,处理时需要考虑。
拍照后保存一份列表图片
可以保留原图,另外生成一份列表小图,让列表加载和查看大图分别使用合适的文件。
下面的 Kotlin 示例只负责保存,传入的 scaledBitmap 应当已经完成采样、方向处理和缩放。示例使用 Kotlin 协程,需要项目已经配置相应依赖。
import android.content.Context
import android.graphics.Bitmap
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun saveListImage(
context: Context,
scaledBitmap: Bitmap
): File = withContext(Dispatchers.IO) {
val directory = File(context.filesDir, "list_images")
check(directory.isDirectory || directory.mkdirs()) {
"Cannot create image directory"
}
val outputFile = File.createTempFile("photo_", ".jpg", directory)
try {
outputFile.outputStream().use { output ->
check(
scaledBitmap.compress(
Bitmap.CompressFormat.JPEG,
85,
output
)
) {
"Cannot encode bitmap"
}
}
outputFile
} catch (error: Exception) {
outputFile.delete()
throw error
}
}
85 是示例质量参数,可以根据图片内容和清晰度要求调整。它控制 JPEG 编码质量,不会缩小图片宽高。JPEG 不保留透明通道,需要透明背景时应选择支持透明度的格式。
保存成功后,把小图路径写入 SQL 数据库,供列表使用。只有新增或更换照片时才需要重新生成小图,不必每次进入页面都转换已有图片。
采样解码和缩放也应放到后台执行,不能只把最后的文件写入移出主线程。
如果确实需要写回原来的 ImageURI
Uri 是资源地址,不能通过给它赋一个 Bitmap 来替换图片。需要打开该地址的输出流,再将 Bitmap 编码写入。
以下代码适用于拥有写权限、支持截断写入且预期内容为 JPEG 的目标 Uri。参数使用 Android 的 Uri 类型;原问题中的 ImageURI 可以作为传入值。
import android.net.Uri
suspend fun overwriteJpeg(
context: Context,
destination: Uri,
bitmap: Bitmap
) = withContext(Dispatchers.IO) {
val output = context.contentResolver
.openOutputStream(destination, "wt")
?: error("Cannot open destination URI")
output.use {
check(bitmap.compress(Bitmap.CompressFormat.JPEG, 85, it)) {
"Cannot encode bitmap"
}
}
}
写回时需要注意:
"wt"表示写入并截断已有内容,但具体是否支持取决于对应的内容提供程序,不能假设所有content://地址都支持这种写法。- 打开输出流前,必须完成对原图的读取和处理,否则截断文件后可能无法继续读取原图。
- 直接覆盖时,编码或写入失败可能损坏原文件。对于自己管理的
filesDir文件,可以先写入新文件,成功后更新数据库引用,确认不再需要旧文件时再清理。 - JPEG 编码不会自动保留原照片的 EXIF 元数据,文件扩展名及 MIME 类型也应与实际编码一致。
如果列表或图片加载组件按 Uri 缓存图片,覆盖文件后还可能继续显示旧缓存。可以使用新的小图文件路径,或在图片变化时更新缓存标识,避免这个问题。
RecyclerView 加载时还要做什么
onBindViewHolder() 应只负责把当前数据对应的图片交给加载组件,不要在里面批量压缩照片、解码完整原图或同步读写文件。
列表加载还应注意:
- 按显示尺寸解码图片,并复用内存或磁盘缓存。
- ViewHolder 重新绑定时取消或替换旧图片请求,避免异步结果显示到错误的列表项。
- 小图尚未生成时先显示占位内容,生成完成后更新对应列表项。
这样,拍照后的处理只需进行一次。进入列表时加载需要显示的图片,不必反复等待全部照片转换完成。
备注:内容仅供参考。
