一、优先在“解码阶段”缩小(最省)
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、是否在主线程解码、是否缺少缓存/复用 ❌