在 Android 开发中,图像处理是一个核心且复杂的领域,而 Bitmap 作为 Android 中表示图像的基本单位,贯穿了从简单图片显示到复杂图像编辑的各个场景。然而,Bitmap 处理不当往往会导致应用性能下降、内存溢出(OOM)等问题,成为许多开发者的痛点。本文将从 Bitmap 的基础概念出发,全面覆盖其创建、加载、处理、优化等各个方面,结合实际案例和最佳实践,帮助开发者彻底掌握 Android Bitmap 的使用技巧。
一、Bitmap 基础概念
1.1 什么是 Bitmap
Bitmap(位图)是一种将图像像素化的存储格式,它通过记录图像中每个像素的颜色信息来精确表示图像。在 Android 中,android.graphics.Bitmap类是处理位图的核心类,负责管理图像数据和提供各种图像处理方法。
与矢量图(Vector)相比,Bitmap 具有以下特点:
- 优点:能够精确表示复杂图像细节,渲染速度快
- 缺点:放大后会失真,文件体积和内存占用通常较大
- 适用场景:照片、复杂图像、需要像素级操作的场景
在 Android 系统中,Bitmap 广泛应用于:
- 界面元素(图标、背景、按钮等)
- 图片展示(相册、社交应用、电商商品图等)
- 图像编辑(裁剪、滤镜、涂鸦等)
- 自定义控件绘制
1.2 Bitmap 的内部结构
理解 Bitmap 的内部结构对于优化其内存占用至关重要。一张 Bitmap 图像由以下几个关键部分组成:
1.像素数据(Pixel Data):这是 Bitmap 占用内存的主要部分,存储了每个像素的颜色信息。
2.宽度和高度(Width & Height):以像素为单位的图像尺寸,直接影响内存占用。
3.像素格式(Pixel Format):决定每个像素占用的字节数,常见格式包括:
- ARGB_8888:每个像素占 4 字节(Alpha、Red、Green、Blue 各 8 位),画质最佳
- RGB_565:每个像素占 2 字节(Red 5 位、Green 6 位、Blue 5 位),无透明度
- ARGB_4444:每个像素占 2 字节,画质较差,已不推荐使用
- ALPHA_8:仅存储透明度,每个像素占 1 字节
- 密度(Density):图像的像素密度(dpi),影响在不同密度屏幕上的显示尺寸。
- 配置信息:包括是否有 mipmap、是否可修改等属性。
示例:计算 Bitmap 内存占用
Bitmap 的内存占用可以通过以下公式计算:
内存大小 = 宽度 × 高度 × 每个像素占用的字节数
以一张 1920×1080 的图片为例:
- 使用ARGB_8888格式:1920 × 1080 × 4 = 8,294,400 字节 ≈ 8MB
- 使用RGB_565格式:1920 × 1080 × 2 = 4,147,200 字节 ≈ 4MB
这意味着一张高清图片可能轻易占用数 MB 内存,当同时加载多张图片时,很容易触发 OOM。
1.3 Android 中 Bitmap 的内存管理变迁
Android 系统对 Bitmap 内存的管理方式随着版本迭代发生过重要变化,了解这些变化有助于更好地进行内存优化:
1.Android 2.2 及之前(API ≤ 8):
- Bitmap 的像素数据存储在 native 内存中
- 回收时机不确定,可能导致 native 内存泄漏
2.Android 3.0 到 Android 7.0(API 9 - 24):
- 像素数据移至 Java 堆内存
- 可通过Bitmap.recycle()主动释放内存
- 受 Java GC 管理,降低了内存泄漏风险,但增加了 Java 堆压力
3.Android 8.0 及之后(API ≥ 26):
- 像素数据又回到 native 内存,但由 Bitmap 对象在 Java 堆中持有引用
- 当 Bitmap 对象被 GC 回收时,native 内存会自动释放
- 无需手动调用recycle(),系统管理更智能
这种变迁反映了 Android 系统在 Bitmap 内存管理上的不断优化,也要求开发者根据目标版本调整内存管理策略。
二、Bitmap 的创建与加载
2.1 从资源文件加载 Bitmap
从应用的资源文件(res/drawable、res/mipmap 等)加载 Bitmap 是最常见的场景之一。Android 提供了BitmapFactory类来简化这一过程。
基本用法:
// 从资源文件加载Bitmap
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.image)
// 显示到ImageView
imageView.setImageBitmap(bitmap)
进阶用法:使用 Options 控制加载
BitmapFactory.Options类提供了丰富的参数来控制 Bitmap 的加载过程,是优化内存占用的关键:
val options = BitmapFactory.Options().apply {
// 仅获取图像尺寸,不加载像素数据
inJustDecodeBounds = true
// 先解码一次获取尺寸
BitmapFactory.decodeResource(resources, R.drawable.large_image, this)
// 计算采样率(见2.5节)
inSampleSize = calculateInSampleSize(this, targetWidth, targetHeight)
// 现在真正加载图像
inJustDecodeBounds = false
// 设置像素格式(降低内存占用)
inPreferredConfig = Bitmap.Config.RGB_565
// 根据设备密度调整
inDensity = resources.displayMetrics.densityDpi
inTargetDensity = imageView.resources.displayMetrics.densityDpi
inScaled = true
}
val optimizedBitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image, options)
注意事项:
- 不同 drawable 目录(如 drawable-hdpi、drawable-xhdpi)会根据设备密度自动缩放图像
- 尽量将图片放在合适密度的目录,避免系统自动缩放导致的内存浪费
- 对于大型图片,务必使用inSampleSize降低采样率
2.2 从文件加载 Bitmap
从本地文件系统加载 Bitmap(如相机拍摄的照片)也是常见需求:
// 从文件路径加载
val file = File(Environment.getExternalStorageDirectory(), "photo.jpg")
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
// 带选项的加载
val options = BitmapFactory.Options().apply {
inPreferredConfig = Bitmap.Config.ARGB_8888
inSampleSize = 2 // 1/2尺寸加载
}
val optimizedBitmap = BitmapFactory.decodeFile(file.absolutePath, options)
从输入流加载:
// 从输入流加载(如文件输入流、网络输入流)
val inputStream = FileInputStream(file)
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream.close() // 记得关闭流
注意事项:
- 从外部存储加载需要申请READ_EXTERNAL_STORAGE权限(Android 10 之前)
- Android 10 及以上推荐使用MediaStore API 访问媒体文件
- 始终记得关闭输入流,避免资源泄漏
2.3 从网络加载 Bitmap
从网络加载图片是现代应用的常见功能,通常需要结合异步处理:
// 简单实现(实际项目建议使用Glide等库)
fun loadBitmapFromNetwork(url: String, imageView: ImageView) {
// 在后台线程执行
CoroutineScope(Dispatchers.IO).launch {
try {
val connection = URL(url).openConnection() as HttpURLConnection
connection.doInput = true
connection.connect()
val inputStream = connection.inputStream
// 解码Bitmap
val bitmap = BitmapFactory.decodeStream(inputStream)
inputStream.close()
connection.disconnect()
// 在主线程更新UI
withContext(Dispatchers.Main) {
imageView.setImageBitmap(bitmap)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
注意事项:
- 网络操作必须在后台线程执行,避免阻塞主线程
- 需要申请INTERNET权限
- 简单实现缺乏缓存、错误处理等功能,实际项目建议使用成熟库
- 大图片需要设置合理的inSampleSize
2.4 创建空白 Bitmap
有时需要创建空白 Bitmap 进行自定义绘制:
// 创建指定尺寸和格式的空白Bitmap
val width = 500
val height = 500
val config = Bitmap.Config.ARGB_8888
val blankBitmap = Bitmap.createBitmap(width, height, config)
// 从现有Bitmap创建新Bitmap(共享像素数据)
val mutableBitmap = blankBitmap.copy(Bitmap.Config.ARGB_8888, true) // true表示可修改
使用 Canvas 绘制:
// 创建可绘制的Bitmap
val bitmap = Bitmap.createBitmap(400, 400, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap) // 将Bitmap与Canvas关联
// 使用Canvas绘制
val paint = Paint().apply {
color = Color.RED
style = Paint.Style.FILL
}
canvas.drawCircle(200f, 200f, 100f, paint) // 绘制圆形
// 显示结果
imageView.setImageBitmap(bitmap)
2.5 采样率(inSampleSize)计算
inSampleSize是控制 Bitmap 内存占用的关键参数,它表示图像的缩放比例:
- inSampleSize = 1:原始尺寸加载
- inSampleSize = 2:宽高各为原来的 1/2,像素数为 1/4,内存为 1/4
- 取值必须是 2 的幂次方(Android 会自动向下取最接近的 2 的幂次方)
计算合适的采样率:
/**
* 计算合适的采样率
* @param options 包含原始图像尺寸的Options
* @param reqWidth 目标宽度
* @param reqHeight 目标高度
* @return 计算得到的采样率
*/
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
// 原始图像尺寸
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
// 如果原始尺寸大于目标尺寸,计算采样率
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
// 找到最大的inSampleSize,使采样后的尺寸不小于目标尺寸
while (halfHeight / inSampleSize >= reqHeight &&
halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
使用示例:
// 加载一张适合ImageView尺寸的图片
val options = BitmapFactory.Options().apply {
inJustDecodeBounds = true
BitmapFactory.decodeResource(resources, R.drawable.large_image, this)
// 目标尺寸设为ImageView的尺寸
val targetWidth = imageView.width
val targetHeight = imageView.height
// 计算采样率
inSampleSize = calculateInSampleSize(this, targetWidth, targetHeight)
inJustDecodeBounds = false
}
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.large_image, options)
注意:imageView.width在布局未完成时可能为 0,此时需要使用其他方式获取目标尺寸(如预设尺寸或屏幕尺寸)。
三、Bitmap 的处理与操作
3.1 缩放 Bitmap
除了加载时通过采样率缩放,还可以在运行时对已加载的 Bitmap 进行缩放:
/**
* 缩放Bitmap到指定尺寸
* @param bitmap 原始Bitmap
* @param newWidth 新宽度
* @param newHeight 新高度
* @return 缩放后的Bitmap
*/
fun scaleBitmap(bitmap: Bitmap, newWidth: Int, newHeight: Int): Bitmap {
// 计算缩放比例
val scaleWidth = newWidth.toFloat() / bitmap.width
val scaleHeight = newHeight.toFloat() / bitmap.height
// 创建矩阵用于缩放
val matrix = Matrix()
matrix.postScale(scaleWidth, scaleHeight)
// 进行缩放
return Bitmap.createBitmap(
bitmap, 0, 0,
bitmap.width, bitmap.height,
matrix, true
)
}
按比例缩放:
/**
* 按比例缩放Bitmap
* @param bitmap 原始Bitmap
* @param scale 缩放比例(0.5f表示缩小到1/2)
* @return 缩放后的Bitmap
*/
fun scaleBitmap(bitmap: Bitmap, scale: Float): Bitmap {
return Bitmap.createScaledBitmap(
bitmap,
(bitmap.width * scale).toInt(),
(bitmap.height * scale).toInt(),
true // 是否使用双线性过滤,使缩放更平滑
)
}
注意:
- 缩放操作会创建新的 Bitmap 对象,原始 Bitmap 需要手动回收
- 缩放是耗时操作,应在后台线程执行
- createScaledBitmap比使用 Matrix 更简单,但灵活性较低
3.2 裁剪 Bitmap
裁剪 Bitmap 可以提取图像的特定区域:
/**
* 裁剪Bitmap的指定区域
* @param bitmap 原始Bitmap
* @param x 起始X坐标
* @param y 起始Y坐标
* @param width 裁剪宽度
* @param height 裁剪高度
* @return 裁剪后的Bitmap
*/
fun cropBitmap(bitmap: Bitmap, x: Int, y: Int, width: Int, height: Int): Bitmap {
// 确保裁剪区域在Bitmap范围内
val safeX = x.coerceIn(0, bitmap.width)
val safeY = y.coerceIn(0, bitmap.height)
val safeWidth = width.coerceIn(0, bitmap.width - safeX)
val safeHeight = height.coerceIn(0, bitmap.height - safeY)
return Bitmap.createBitmap(bitmap, safeX, safeY, safeWidth, safeHeight)
}
示例:裁剪中心区域
/**
* 裁剪Bitmap的中心正方形区域
*/
fun cropCenterSquare(bitmap: Bitmap): Bitmap {
val size = minOf(bitmap.width, bitmap.height)
val x = (bitmap.width - size) / 2
val y = (bitmap.height - size) / 2
return cropBitmap(bitmap, x, y, size, size)
}
3.3 旋转与翻转
使用 Matrix 可以实现 Bitmap 的旋转和翻转:
/**
* 旋转Bitmap
* @param bitmap 原始Bitmap
* @param degrees 旋转角度(顺时针)
* @return 旋转后的Bitmap
*/
fun rotateBitmap(bitmap: Bitmap, degrees: Float): Bitmap {
val matrix = Matrix()
matrix.postRotate(degrees)
return Bitmap.createBitmap(
bitmap, 0, 0,
bitmap.width, bitmap.height,
matrix, true
)
}
/**
* 水平翻转Bitmap
*/
fun flipHorizontal(bitmap: Bitmap): Bitmap {
val matrix = Matrix()
matrix.postScale(-1f, 1f) // 水平翻转
return Bitmap.createBitmap(
bitmap, 0, 0,
bitmap.width, bitmap.height,
matrix, true
)
}
/**
* 垂直翻转Bitmap
*/
fun flipVertical(bitmap: Bitmap): Bitmap {
val matrix = Matrix()
matrix.postScale(1f, -1f) // 垂直翻转
return Bitmap.createBitmap(
bitmap, 0, 0,
bitmap.width, bitmap.height,
matrix, true
)
}
注意:旋转操作可能会改变 Bitmap 的宽高(如旋转 90 度或 270 度),需要注意后续处理。
3.4 颜色处理与滤镜
通过ColorMatrix可以实现各种颜色滤镜效果:
/**
* 应用灰度滤镜
*/
fun applyGrayscaleFilter(bitmap: Bitmap): Bitmap {
// 创建可修改的Bitmap
val result = bitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(result)
// 创建灰度颜色矩阵
val colorMatrix = ColorMatrix().apply {
setSaturation(0f) // 饱和度为0即灰度
}
// 创建画笔并设置颜色滤镜
val paint = Paint().apply {
colorFilter = ColorMatrixColorFilter(colorMatrix)
}
// 应用滤镜
canvas.drawBitmap(result, 0f, 0f, paint)
return result
}
/**
* 调整亮度
* @param brightness 亮度值(-255到255)
*/
fun adjustBrightness(bitmap: Bitmap, brightness: Int): Bitmap {
val result = bitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(result)
val colorMatrix = ColorMatrix().apply {
set(floatArrayOf(
1f, 0f, 0f, 0f, brightness.toFloat(),
0f, 1f, 0f, 0f, brightness.toFloat(),
0f, 0f, 1f, 0f, brightness.toFloat(),
0f, 0f, 0f, 1f, 0f
))
}
val paint = Paint().apply {
colorFilter = ColorMatrixColorFilter(colorMatrix)
}
canvas.drawBitmap(result, 0f, 0f, paint)
return result
}
使用 PorterDuff 混合模式:
/**
* 应用颜色叠加效果
*/
fun applyColorOverlay(bitmap: Bitmap, color: Int, alpha: Int): Bitmap {
val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(result)
// 绘制原始图像
canvas.drawBitmap(bitmap, 0f, 0f, null)
// 创建叠加画笔
val paint = Paint().apply {
this.color = color
this.alpha = alpha
xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP) // 叠加模式
}
// 绘制叠加颜色
canvas.drawRect(0f, 0f, bitmap.width.toFloat(), bitmap.height.toFloat(), paint)
return result
}
3.5 合成与水印
将多张 Bitmap 合成一张,或添加水印:
/**
* 给Bitmap添加文字水印
*/
fun addTextWatermark(bitmap: Bitmap, text: String): Bitmap {
val result = bitmap.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(result)
// 创建文字画笔
val paint = Paint().apply {
color = Color.WHITE
textSize = 48f
alpha = 128 // 半透明
typeface = Typeface.DEFAULT_BOLD
isAntiAlias = true // 抗锯齿
}
// 计算文字位置(右下角)
val textWidth = paint.measureText(text)
val x = result.width - textWidth - 20
val y = result.height - 40f
// 绘制文字阴影
paint.color = Color.BLACK
canvas.drawText(text, x + 2, y + 2, paint)
// 绘制文字
paint.color = Color.WHITE
canvas.drawText(text, x, y, paint)
return result
}
/**
* 合并两张Bitmap(底部图和顶部图)
*/
fun mergeBitmaps(base: Bitmap, overlay: Bitmap, x: Int, y: Int): Bitmap {
val result = base.copy(Bitmap.Config.ARGB_8888, true)
val canvas = Canvas(result)
// 在指定位置绘制叠加图
canvas.drawBitmap(overlay, x.toFloat(), y.toFloat(), null)
return result
}
3.6 保存 Bitmap 到文件
将处理后的 Bitmap 保存到存储设备:
/**
* 保存Bitmap到文件
* @param bitmap 要保存的Bitmap
* @param file 目标文件
* @param format 保存格式(JPEG或PNG)
* @param quality 质量(0-100,仅对JPEG有效)
* @return 是否保存成功
*/
fun saveBitmapToFile(
bitmap: Bitmap,
file: File,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 90
): Boolean {
if (quality < 0 || quality > 100) {
throw IllegalArgumentException("Quality must be between 0 and 100")
}
var out: OutputStream? = null
try {
out = FileOutputStream(file)
return bitmap.compress(format, quality, out)
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
out?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
return false
}
使用示例:
// 保存为JPEG
val jpegFile = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "image.jpg")
saveBitmapToFile(bitmap, jpegFile, Bitmap.CompressFormat.JPEG, 80)
// 保存为PNG(无损)
val pngFile = File(getExternalFilesDir(Environment.DIRECTORY_PICTURES), "image.png")
saveBitmapToFile(bitmap, pngFile, Bitmap.CompressFormat.PNG)
注意:
- PNG 格式支持透明度,但文件体积通常较大
- JPEG 格式不支持透明度,但可以通过 quality 参数控制压缩率
- Android 10 及以上推荐使用MediaStore API 保存到公共目录
四、Bitmap 内存管理与优化
4.1 避免内存溢出(OOM)
内存溢出是 Bitmap 处理中最常见的问题,尤其是在加载大量图片或高分辨率图片时。以下是避免 OOM 的关键策略:
1.合理设置采样率:根据显示需求加载合适尺寸的图片,而非原始尺寸。
2.选择合适的像素格式:
- 不需要透明度时使用RGB_565(内存占用为ARGB_8888的一半)
- 仅需透明度时使用ALPHA_8
3.及时回收不再使用的 Bitmap:
// 当Bitmap不再需要时
if (bitmap != null && !bitmap.isRecycled) {
bitmap.recycle() // 释放native内存
// 帮助GC回收
bitmap = null
}
注意:Android 8.0 及以上系统会自动管理回收,手动调用recycle()的必要性降低,但仍可作为优化手段。
4.使用弱引用缓存:
// 使用WeakReference存储Bitmap,允许GC在内存紧张时回收
val weakBitmap = WeakReference<Bitmap>(bitmap)
// 使用时检查是否已被回收
val bitmap = weakBitmap.get()
if (bitmap != null && !bitmap.isRecycled) {
// 使用Bitmap
}
5.限制同时加载的图片数量:在列表等场景中,仅加载当前可见区域的图片。
6.监控内存使用:
// 获取内存信息
val memoryInfo = ActivityManager.MemoryInfo()
(getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
// 当可用内存不足时采取措施(如清理缓存)
if (memoryInfo.lowMemory) {
clearImageCache()
}
4.2 内存缓存(LruCache)
LruCache(最近最少使用缓存)是 Android 提供的高效内存缓存类,非常适合缓存 Bitmap:
class BitmapMemoryCache(maxSize: Int) : LruCache<String, Bitmap>(maxSize) {
/**
* 计算每个Bitmap的大小
*/
override fun sizeOf(key: String, value: Bitmap): Int {
// 返回Bitmap的字节数
return value.byteCount
}
/**
* 当Bitmap被移除缓存时调用,可用于回收资源
*/
override fun entryRemoved(
evicted: Boolean,
key: String?,
oldValue: Bitmap?,
newValue: Bitmap?
) {
super.entryRemoved(evicted, key, oldValue, newValue)
// 如果是因为内存不足被移除,主动回收
if (evicted && oldValue != null && !oldValue.isRecycled) {
oldValue.recycle()
}
}
}
// 初始化缓存(通常在Application或单例中)
fun initBitmapCache(context: Context) {
// 获取应用可用内存的1/8作为缓存大小
val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
val memoryClass = activityManager.memoryClass // 应用可用内存(MB)
val cacheSize = (memoryClass / 8) * 1024 * 1024 // 转换为字节
bitmapCache = BitmapMemoryCache(cacheSize)
}
// 使用缓存
fun loadBitmapWithCache(key: String, loader: () -> Bitmap): Bitmap? {
// 先从缓存获取
bitmapCache.get(key)?.let { return it }
// 缓存未命中,加载图片
val bitmap = loader()
// 存入缓存
if (bitmap != null) {
bitmapCache.put(key, bitmap)
}
return bitmap
}
最佳实践:
- 缓存大小通常设为应用可用内存的 1/8
- 缓存键(key)应唯一且稳定(如图片 URL 的哈希值)
- 在onTrimMemory回调中根据内存紧张程度调整缓存:
override fun onTrimMemory(level: Int) { super.onTrimMemory(level) when (level) { // 内存不足,清理所有缓存 TRIM_MEMORY_COMPLETE -> bitmapCache.evictAll() // 内存紧张,清理部分缓存 TRIM_MEMORY_MODERATE -> bitmapCache.trimToSize(bitmapCache.maxSize() / 2) // 低内存警告,准备清理 TRIM_MEMORY_UI_HIDDEN -> bitmapCache.trimToSize(bitmapCache.maxSize() / 4) } }
4.3 磁盘缓存(DiskLruCache)
磁盘缓存用于持久化存储 Bitmap,避免重复下载或解码,Android 官方推荐使用DiskLruCache(需自行实现或使用第三方库):
class BitmapDiskCache(private val directory: File, maxSize: Long) {
private val diskLruCache = DiskLruCache.open(directory, 1, 1, maxSize)
/**
* 从磁盘缓存获取Bitmap
*/
fun getBitmap(key: String): Bitmap? {
val safeKey = key.md5() // 使用MD5哈希作为键
val snapshot = diskLruCache.get(safeKey) ?: return null
return try {
val inputStream = snapshot.getInputStream(0)
BitmapFactory.decodeStream(inputStream)
} finally {
snapshot.close()
}
}
/**
* 将Bitmap存入磁盘缓存
*/
fun putBitmap(key: String, bitmap: Bitmap, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG): Boolean {
val safeKey = key.md5()
val editor = diskLruCache.edit(safeKey) ?: return false
return try {
val outputStream = editor.newOutputStream(0)
val success = bitmap.compress(format, 80, outputStream)
if (success) {
editor.commit()
} else {
editor.abort()
}
success
} catch (e: Exception) {
editor.abort()
false
}
}
/**
* 移除缓存
*/
fun remove(key: String): Boolean {
val safeKey = key.md5()
return diskLruCache.remove(safeKey)
}
/**
* 清理所有缓存
*/
fun clear() {
diskLruCache.delete()
}
/**
* 关闭缓存
*/
fun close() {
diskLruCache.close()
}
// MD5哈希工具方法
private fun String.md5(): String {
val bytes = MessageDigest.getInstance("MD5").digest(toByteArray())
return bytes.joinToString("") { "%02x".format(it) }
}
}
// 初始化磁盘缓存
fun initDiskCache(context: Context) {
// 缓存目录(应用私有目录)
val cacheDir = File(context.cacheDir, "bitmap_cache")
if (!cacheDir.exists()) {
cacheDir.mkdirs()
}
// 缓存大小设为50MB
val cacheSize = 50L * 1024 * 1024
diskCache = BitmapDiskCache(cacheDir, cacheSize)
}
磁盘缓存最佳实践:
- 缓存目录使用应用私有缓存目录(context.cacheDir),系统会在内存不足时自动清理
- 缓存大小根据应用需求设置(通常 10-100MB)
- 定期清理过期缓存(如超过 7 天的缓存)
- 避免在主线程进行磁盘操作
4.4 三级缓存策略
结合内存缓存、磁盘缓存和网络加载的三级缓存策略是高效加载图片的标准方案:
class ImageLoader(
private val memoryCache: BitmapMemoryCache,
private val diskCache: BitmapDiskCache,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
/**
* 加载图片(三级缓存)
*/
fun loadImage(
url: String,
targetWidth: Int,
targetHeight: Int,
onSuccess: (Bitmap) -> Unit,
onError: (Exception) -> Unit
) {
CoroutineScope(ioDispatcher).launch {
try {
// 1. 先从内存缓存获取
var bitmap = memoryCache.get(url)
if (bitmap != null) {
withContext(Dispatchers.Main) { onSuccess(bitmap) }
return@launch
}
// 2. 内存缓存未命中,从磁盘缓存获取
bitmap = diskCache.getBitmap(url)
if (bitmap != null) {
// 放入内存缓存
memoryCache.put(url, bitmap)
withContext(Dispatchers.Main) { onSuccess(bitmap) }
return@launch
}
// 3. 磁盘缓存未命中,从网络加载
bitmap = downloadBitmap(url, targetWidth, targetHeight)
if (bitmap != null) {
// 存入磁盘缓存和内存缓存
diskCache.putBitmap(url, bitmap)
memoryCache.put(url, bitmap)
withContext(Dispatchers.Main) { onSuccess(bitmap) }
return@launch
}
// 所有来源都失败
withContext(Dispatchers.Main) {
onError(Exception("Failed to load image from all sources"))
}
} catch (e: Exception) {
withContext(Dispatchers.Main) { onError(e) }
}
}
}
/**
* 从网络下载并解码Bitmap
*/
private suspend fun downloadBitmap(url: String, targetWidth: Int, targetHeight: Int): Bitmap? {
return withContext(ioDispatcher) {
val connection = URL(url).openConnection() as HttpURLConnection
connection.doInput = true
connection.connect()
val inputStream = connection.inputStream
val options = BitmapFactory.Options().apply {
// 先获取尺寸
inJustDecodeBounds = true
BitmapFactory.decodeStream(inputStream, null, this)
inputStream.reset() // 重置流以便重新解码
// 计算采样率
inSampleSize = calculateInSampleSize(this, targetWidth, targetHeight)
inJustDecodeBounds = false
inPreferredConfig = Bitmap.Config.RGB_565
}
val bitmap = BitmapFactory.decodeStream(inputStream, null, options)
inputStream.close()
connection.disconnect()
bitmap
}
}
}
使用示例:
// 初始化图片加载器
val imageLoader = ImageLoader(bitmapCache, diskCache)
// 加载图片
imageLoader.loadImage(
url = "https://example.com/image.jpg",
targetWidth = imageView.width,
targetHeight = imageView.height,
onSuccess = { bitmap ->
imageView.setImageBitmap(bitmap)
},
onError = { e ->
e.printStackTrace()
imageView.setImageResource(R.drawable.error_placeholder)
}
)
五、列表中的 Bitmap 优化
在RecyclerView或ListView中显示大量图片是 Bitmap 优化的典型场景,处理不当会导致滑动卡顿甚至 OOM。
5.1 RecyclerView 中的图片优化
1.使用 ViewHolder 模式:避免重复创建视图和 Bitmap 对象
class ImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val imageView: ImageView = itemView.findViewById(R.id.image_view)
var currentUrl: String? = null // 记录当前加载的URL,用于避免图片错位
}
2.取消滑动时的加载:滑动过程中暂停图片加载,减少资源消耗
class PausableImageLoader : ImageLoader {
private var isPaused = false
private val pendingRequests = mutableListOf<ImageRequest>()
// 暂停加载
fun pause() {
isPaused = true
}
// 恢复加载
fun resume() {
isPaused = false
synchronized(pendingRequests) {
pendingRequests.forEach { request ->
loadImage(request)
}
pendingRequests.clear()
}
}
// 重写加载方法
fun loadImage(request: ImageRequest) {
if (isPaused) {
synchronized(pendingRequests) {
pendingRequests.add(request)
}
} else {
super.loadImage(
request.url,
request.width,
request.height,
request.onSuccess,
request.onError
)
}
}
}
// 在RecyclerView滚动时暂停/恢复加载
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> imageLoader.resume() // 停止滚动时恢复
else -> imageLoader.pause() // 滚动时暂停
}
}
})
3.图片错位解决方案:由于 RecyclerView 的复用机制,快速滑动时可能出现图片错位
// 在绑定ViewHolder时
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
val item = items[position]
holder.currentUrl = item.url
// 先设置占位图
holder.imageView.setImageResource(R.drawable.placeholder)
// 加载图片
imageLoader.loadImage(
url = item.url,
targetWidth = holder.imageView.width,
targetHeight = holder.imageView.height,
onSuccess = { bitmap ->
// 检查是否是当前item的图片
if (holder.currentUrl == item.url) {
holder.imageView.setImageBitmap(bitmap)
}
},
onError = {
if (holder.currentUrl == item.url) {
holder.imageView.setImageResource(R.drawable.error)
}
}
)
}
4.预计算图片尺寸:提前确定 ImageView 的尺寸,避免解码时尺寸为 0
// 在布局中固定ImageView尺寸(推荐)
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:scaleType="centerCrop"/>
// 或在代码中计算
val displayMetrics = resources.displayMetrics
val imageSize = (120 * displayMetrics.density).toInt() // 120dp转换为像素
5.2 分页加载与回收
对于大量图片列表,采用分页加载减少同时加载的图片数量:
class ImagePagingAdapter : PagingDataAdapter<ImageItem, ImageViewHolder>(diffCallback) {
// ... 实现Adapter相关代码
override fun onViewRecycled(holder: ImageViewHolder) {
super.onViewRecycled(holder)
// 当ViewHolder被回收时,取消加载并清理资源
holder.currentUrl?.let { cancelLoading(it) }
holder.imageView.setImageBitmap(null) // 清除图片
}
}
5.3 缩略图与渐进式加载
对于大图,先加载缩略图再加载高清图,提升用户体验:
fun loadImageWithThumbnail(
url: String,
thumbnailUrl: String,
imageView: ImageView
) {
// 1. 先加载缩略图
imageLoader.loadImage(
url = thumbnailUrl,
targetWidth = imageView.width / 4, // 缩略图尺寸为目标的1/4
targetHeight = imageView.height / 4,
onSuccess = { thumbnail ->
imageView.setImageBitmap(thumbnail)
// 2. 再加载高清图
imageLoader.loadImage(
url = url,
targetWidth = imageView.width,
targetHeight = imageView.height,
onSuccess = { highRes ->
// 使用淡入动画切换
val fadeIn = AlphaAnimation(0f, 1f).apply {
duration = 300
}
imageView.setImageBitmap(highRes)
imageView.startAnimation(fadeIn)
}
)
}
)
}
六、高级优化技巧
6.1 使用硬件加速
Android 的硬件加速可以显著提升 Bitmap 的绘制性能,默认情况下是开启的。可以通过以下方式控制:
在 Manifest 中为应用或 Activity 开启:
<application
android:hardwareAccelerated="true" ...>
<activity
android:name=".MyActivity"
android:hardwareAccelerated="true"/>
</application>
在 View 级别控制:
<View
android:layerType="hardware" // 硬件加速
... />
<View
android:layerType="software" // 软件渲染
... />
代码中设置:
// 启用硬件加速
view.setLayerType(View.LAYER_TYPE_HARDWARE, null)
// 禁用硬件加速
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
注意:
- 硬件加速不支持所有绘图操作,某些自定义绘制可能需要禁用
- 可通过View.isHardwareAccelerated()检查是否启用了硬件加速
6.2 图片预加载与预解码
在合适的时机提前加载即将需要的图片:
class ImagePreloader(private val imageLoader: ImageLoader) {
// 预加载图片到缓存
fun preloadImages(urls: List<String>, width: Int, height: Int) {
CoroutineScope(Dispatchers.IO).launch {
urls.forEach { url ->
// 仅加载到缓存,不显示
imageLoader.loadImageToCache(url, width, height)
}
}
}
}
// 在进入图片列表前预加载
fun onPrepareToEnterGallery() {
val upcomingImageUrls = getUpcomingImageUrls() // 获取即将显示的图片URL
imagePreloader.preloadImages(upcomingImageUrls, 200, 200)
}
6.3 使用 BitmapRegionDecoder 加载超大图
对于超大图片(如地图、高分辨率扫描件),使用BitmapRegionDecoder加载局部区域:
class LargeImageLoader(private val context: Context) {
private var decoder: BitmapRegionDecoder? = null
private var imageWidth = 0
private var imageHeight = 0
/**
* 初始化解码器
*/
fun init(inputStream: InputStream) {
decoder = BitmapRegionDecoder.newInstance(inputStream, false)
imageWidth = decoder?.width ?: 0
imageHeight = decoder?.height ?: 0
}
/**
* 加载指定区域
*/
fun loadRegion(rect: Rect, sampleSize: Int = 1): Bitmap? {
val options = BitmapFactory.Options().apply {
inSampleSize = sampleSize
inPreferredConfig = Bitmap.Config.RGB_565
}
return decoder?.decodeRegion(rect, options)
}
/**
* 释放资源
*/
fun release() {
decoder?.recycle()
decoder = null
}
// 获取图片原始尺寸
fun getImageWidth() = imageWidth
fun getImageHeight() = imageHeight
}
// 使用示例(显示大图的某个区域)
val inputStream = assets.open("large_map.jpg")
largeImageLoader.init(inputStream)
// 加载图片的一块区域(x=100, y=200, width=500, height=500)
val rect = Rect(100, 200, 600, 700)
val regionBitmap = largeImageLoader.loadRegion(rect)
imageView.setImageBitmap(regionBitmap)
这种方式特别适合实现图片查看器的缩放和平移功能,只加载当前可见区域。
6.4 使用 RenderScript 进行高效图像处理
RenderScript 是 Android 提供的高性能计算框架,适合进行复杂的图像处理:
/**
* 使用RenderScript应用模糊效果
*/
fun applyBlur(context: Context, bitmap: Bitmap, radius: Float): Bitmap {
// 创建输出Bitmap
val output = Bitmap.createBitmap(bitmap.width, bitmap.height, bitmap.config)
// 初始化RenderScript
val rs = RenderScript.create(context)
val input = Allocation.createFromBitmap(rs, bitmap)
val outputAlloc = Allocation.createFromBitmap(rs, output)
// 创建模糊脚本
val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
script.setRadius(radius)
script.setInput(input)
script.forEach(outputAlloc)
// 复制结果到输出Bitmap
outputAlloc.copyTo(output)
// 释放资源
input.destroy()
outputAlloc.destroy()
script.destroy()
rs.destroy()
return output
}
注意:
- RenderScript 特别适合计算密集型操作(如模糊、降噪、边缘检测)
- Android 17 及以上支持,对于低版本需要使用支持库
- 效果相同的情况下,RenderScript 通常比 Java 实现快 10-100 倍
6.5 减少 Bitmap 拷贝
频繁的 Bitmap 拷贝会消耗大量 CPU 和内存,应尽量避免:
1.直接复用 Bitmap:
// 复用已有的Bitmap(需确保尺寸和格式兼容)
fun decodeWithReuse(
inputStream: InputStream,
reuseBitmap: Bitmap
): Bitmap? {
val options = BitmapFactory.Options().apply {
inMutable = true
inBitmap = reuseBitmap // 复用此Bitmap
}
return BitmapFactory.decodeStream(inputStream, null, options)
}
复用条件:
- Android 3.0(API 11)及以上支持
- 复用的 Bitmap 必须是可变的(isMutable == true)
- 新 Bitmap 的内存不能大于复用 Bitmap 的内存(Android 4.4 之前)
2.直接在原始 Bitmap 上绘制:
// 避免创建新Bitmap,直接在原始Bitmap上绘制(需确保可修改)
fun drawOnOriginal(bitmap: Bitmap, drawAction: Canvas.() -> Unit): Bitmap {
if (!bitmap.isMutable) {
// 如果不可修改,只能创建副本
return bitmap.copy(Bitmap.Config.ARGB_8888, true).apply {
Canvas(this).drawAction()
}
}
// 直接在原始Bitmap上绘制
Canvas(bitmap).drawAction()
return bitmap
}
七、第三方库的使用
手动处理 Bitmap 的各种优化细节非常繁琐,实际项目中推荐使用成熟的图片加载库,它们已经内置了各种优化策略。
7.1 Glide
Glide 是 Google 推荐的图片加载库,以易用性和性能著称:
添加依赖:
dependencies {
implementation 'com.github.bumptech.glide:glide:4.14.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.14.2'
}
基本使用:
// 加载网络图片
Glide.with(context)
.load("https://example.com/image.jpg")
.into(imageView)
// 加载资源图片
Glide.with(context)
.load(R.drawable.image)
.into(imageView)
// 加载文件图片
Glide.with(context)
.load(file)
.into(imageView)
高级配置:
Glide.with(context)
.load(url)
.placeholder(R.drawable.placeholder) // 加载中占位图
.error(R.drawable.error) // 错误占位图
.fallback(R.drawable.fallback) // URL为空时的占位图
.override(500, 500) // 指定尺寸
.centerCrop() // 裁剪方式
.circleCrop() // 圆形裁剪
.thumbnail(0.5f) // 先加载缩略图(原图的50%)
.transition(DrawableTransitionOptions.withCrossFade()) // 淡入动画
.diskCacheStrategy(DiskCacheStrategy.ALL) // 缓存策略
.priority(Priority.HIGH) // 优先级
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>?,
isFirstResource: Boolean
): Boolean {
// 加载失败处理
return false
}
override fun onResourceReady(
resource: Drawable?,
model: Any?,
target: Target<Drawable>?,
dataSource: DataSource?,
isFirstResource: Boolean
): Boolean {
// 加载成功处理
return false
}
})
.into(imageView)
Glide 的优势:
- 自动管理生命周期,避免内存泄漏
- 内置三级缓存,性能优异
- 支持多种图片格式和数据源
- 自动处理图片尺寸和内存优化
- 丰富的变换和过渡效果
7.2 Picasso
Picasso 是 Square 公司开发的轻量级图片加载库:
添加依赖:
dependencies {
implementation 'com.squareup.picasso:picasso:2.71828'
}
基本使用:
Picasso.get()
.load("https://example.com/image.jpg")
.into(imageView)
高级用法:
Picasso.get()
.load(url)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.resize(500, 500)
.centerCrop()
.rotate(90f) // 旋转
.transform(CropCircleTransformation()) // 圆形变换
.priority(Picasso.Priority.HIGH)
.fetch() // 仅下载不显示
自定义变换:
class GrayscaleTransformation : Transformation {
override fun transform(source: Bitmap): Bitmap {
// 实现灰度变换
val result = Bitmap.createBitmap(
source.width,
source.height,
source.config
)
val canvas = Canvas(result)
val paint = Paint()
val colorMatrix = ColorMatrix()
colorMatrix.setSaturation(0f)
paint.colorFilter = ColorMatrixColorFilter(colorMatrix)
canvas.drawBitmap(source, 0f, 0f, paint)
source.recycle() // 回收原始Bitmap
return result
}
override fun key(): String = "grayscale"
}
// 使用自定义变换
Picasso.get()
.load(url)
.transform(GrayscaleTransformation())
.into(imageView)
7.3 Coil
Coil 是一个基于 Kotlin 协程的现代图片加载库:
添加依赖:
dependencies {
implementation 'io.coil-kt:coil:2.4.0'
}
基本使用:
// 加载图片
imageView.load("https://example.com/image.jpg")
// 更详细的配置
imageView.load(url) {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
crossfade(true)
transformations(CircleCropTransformation())
size(500)
}
Coil 的优势:
- 完全基于 Kotlin 和协程,与 Kotlin 生态无缝集成
- 性能优异,启动速度快
- 支持 Jetpack Compose
- 内置多种变换和缓存策略
7.4 库的选择建议
库 |
优势 |
劣势 |
适用场景 |
Glide |
功能全面,生命周期管理完善,缓存策略优秀 |
体积较大 |
大多数应用,尤其是需要复杂功能的场景 |
Picasso |
轻量,API 简洁,易集成 |
功能相对简单 |
简单场景,对包体积敏感的应用 |
Coil |
基于协程,现代架构,性能好 |
相对较新,生态不如 Glide 成熟 |
Kotlin 项目,尤其是使用 Jetpack Compose 的应用 |
建议:
- 新项目优先考虑 Glide 或 Coil
- 简单需求可选择 Picasso
- Kotlin 项目推荐使用 Coil,与协程配合更佳
- 避免为了微小差异在项目中引入多个图片库
八、常见问题与解决方案
8.1 图片拉伸与变形
问题:图片显示时出现拉伸或变形。
解决方案:
1.正确设置scaleType:
<!-- 常用的scaleType -->
<ImageView
android:scaleType="centerCrop" <!-- 保持比例,裁剪填充 -->
<!-- 或 -->
android:scaleType="fitCenter" <!-- 保持比例,适应视图 -->
... />
2.确保 ImageView 尺寸与图片比例一致:
// 加载图片后调整ImageView尺寸以保持比例
fun adjustImageViewRatio(imageView: ImageView, bitmap: Bitmap) {
val ratio = bitmap.width.toFloat() / bitmap.height.toFloat()
imageView.layoutParams.height = (imageView.width / ratio).toInt()
imageView.requestLayout()
}
3.使用占位图时,确保占位图与目标图片比例一致。
8.2 图片加载缓慢或卡顿
问题:图片加载速度慢,或导致 UI 卡顿。
解决方案:
1.确保在后台线程进行图片解码和处理
2.使用合适的采样率,避免加载过大图片
3.实现三级缓存,减少重复加载
4.滑动列表中使用暂停 / 恢复加载机制
5.对大图使用缩略图渐进式加载
6.考虑使用 WebP 等更高效的图片格式
8.3 内存溢出(OOM)
问题:加载图片时抛出OutOfMemoryError。
解决方案:
1.严格控制图片尺寸,使用合适的采样率
2.优先使用RGB_565格式
3.及时回收不再使用的 Bitmap
4.实现内存缓存并设置合理大小
5.监控内存状态,在内存不足时清理缓存
6.避免同时加载大量图片
8.4 图片错位(RecyclerView 中)
问题:在 RecyclerView 快速滑动时,图片显示混乱或错位。
解决方案:
1.在 ViewHolder 中记录当前加载的 URL
2.加载完成后检查 URL 是否匹配
3.复用 ViewHolder 时清除旧图片
4.使用占位图减少视觉混乱
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
holder.bind(item)
}
fun bind(item: Item) {
// 记录当前URL
currentUrl = item.url
// 清除旧图片
imageView.setImageResource(R.drawable.placeholder)
// 加载新图片
loadImage(item.url) { bitmap ->
// 检查是否是当前项
if (currentUrl == item.url) {
imageView.setImageBitmap(bitmap)
}
}
}
8.5 图片旋转问题
问题:加载的图片方向不正确(尤其是相机拍摄的照片)。
解决方案:
1.读取图片的 EXIF 信息获取旋转角度:
/**
* 读取图片的旋转角度
*/
fun getImageRotation(file: File): Int {
try {
val exif = ExifInterface(file.absolutePath)
val orientation = exif.getAttributeInt(
ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_NORMAL
)
return when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> 90
ExifInterface.ORIENTATION_ROTATE_180 -> 180
ExifInterface.ORIENTATION_ROTATE_270 -> 270
else -> 0
}
} catch (e: Exception) {
e.printStackTrace()
return 0
}
}
2.加载图片时应用旋转:
fun loadImageWithRotation(context: Context, file: File, imageView: ImageView) {
val rotation = getImageRotation(file)
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
val rotatedBitmap = if (rotation != 0) {
rotateBitmap(bitmap, rotation.toFloat())
} else {
bitmap
}
imageView.setImageBitmap(rotatedBitmap)
bitmap.recycle() // 回收原始Bitmap
}
3.第三方库(如 Glide)会自动处理 EXIF 旋转信息,推荐使用。
九、总结与展望
Bitmap 处理是 Android 开发中的核心技术之一,也是性能优化的关键领域。从基础的加载和显示,到复杂的内存管理和性能优化,每一个环节都需要开发者深入理解 Bitmap 的特性和 Android 系统的工作机制。
本文全面介绍了 Bitmap 的基础知识、创建加载、处理操作、内存管理、优化技巧和第三方库使用,涵盖了从简单到复杂的各种场景。掌握这些知识不仅能够解决日常开发中的图片处理问题,更能帮助开发者构建高性能、低内存占用的优秀应用。
随着 Android 系统的不断演进,Bitmap 的处理方式也在持续优化。从早期的手动内存管理,到现代系统的自动内存回收;从基础的BitmapFactory,到功能强大的 Glide、Coil 等库,Bitmap 处理的便捷性和性能都在不断提升。
未来,随着硬件性能的提升和新图片格式(如 WebP、HEIF)的普及,Android 的 Bitmap 处理将更加高效。同时,Jetpack Compose 等新 UI 框架也为图片处理带来了新的方式和挑战。
作为开发者,我们需要不断学习和适应这些变化,在掌握基础原理的同时,善用系统 API 和第三方库,在功能实现和性能优化之间找到平衡,为用户提供流畅、稳定的图片体验。
Bitmap 处理的优化是一个持续迭代的过程,没有一劳永逸的解决方案。只有结合具体应用场景,不断测试、分析和优化,才能真正掌握这门技术,构建出优秀的 Android 应用。