如何根据屏幕密度和控件大小计算 Bitmap 的最优采样率(inSampleSize)?
解读
面试官问的不是“inSampleSize 怎么用”,而是“怎么算”。
国内大厂(华米 OV、字节、阿里)的实战场景是:
- 图片源来自云端 CDN,原图动辄 5000×4000;
- 控件大小受“折叠屏+多窗口+字体缩放”三重因素动态变化;
- 低端机(720p、2 GB 内存)必须一次解码成功,不能 OOM;
- 还要兼顾“Google Play 政策”与“工信部 32 位下架令”,so 里不能用私有 skia API。
因此,候选人必须给出“可落地公式 + 边界保护 + 代码片段”,而不是背官方文档。
知识点
- BitmapFactory.Options.inSampleSize 语义
- 只能是 2 的幂,否则向下取最接近的 2 的幂;
- 最终解码尺寸 = ceil(原宽/inSampleSize) × ceil(原高/inSampleSize)。
- 屏幕密度相关量
- densityDpi(DisplayMetrics.densityDpi)
- 控件实际像素尺寸:View 的 width/height 或 ConstraintLayout 计算后的 size。
- 最小内存占用模型
- 目标像素数 ≤ 控件像素数 × 1.2(留 20 % 缩放余量,避免重复解码);
- 单像素字节数:ARGB_8888=4,RGB_565=2,在 Kotlin 中通过 inPreferredConfig 指定。
- 国内 CDN 常见参数
- 带“w=”、“h=”的锐化缩略图,先拿宽高再决定是否二次采样;
- 弱网场景下,先下 1/4 分辨率占位,再后台下原图,需两次 inSampleSize 计算。
- 兼容性陷阱
- Android 7.0 文件权限,必须 FileDescriptor 解码;
- Android 10+ 分区存储,不能直接 getRealPath,用 ContentResolver 打开 InputStream;
- 折叠屏旋转后,控件尺寸变化,需监听 ViewTreeObserver.OnGlobalLayoutListener 重新计算。
答案
步骤化公式(可直接写白板):
-
读原图尺寸
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeStream(input, null, options) val srcWidth = options.outWidth val srcHeight = options.outHeight -
取控件目标像素
val dm = resources.displayMetrics val targetPx = (控件dp * dm.density + 0.5f).toInt() -
计算基础采样率
fun computeInSampleSize( srcWidth: Int, srcHeight: Int, targetWidth: Int, targetHeight: Int ): Int { var inSampleSize = 1 if (srcHeight > targetHeight || srcWidth > targetWidth) { val halfHeight = srcHeight / 2 val halfWidth = srcWidth / 2 while (halfHeight / inSampleSize >= targetHeight && halfWidth / inSampleSize >= targetWidth) { inSampleSize *= 2 } } return inSampleSize } -
密度修正(国内低端机 720p 场景)
若 densityDpi ≤ 240 且可用内存 < 512 MB,再 ×2 保守降级:val am = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager val largeHeap = context.applicationInfo.flags and ApplicationInfo.FLAG_LARGE_HEAP != 0 val maxMem = if (largeHeap) am.largeMemoryClass else am.memoryClass if (!largeHeap && maxMem <= 512 && dm.densityDpi <= 240) { inSampleSize = inSampleSize.coerceAtLeast(2) } -
解码 & 复用内存池(Android 8.0+)
val reuse = BitmapFactory.Options().apply { inSampleSize = calculated inPreferredConfig = Bitmap.Config.RGB_565 // 省 50 % inMutable = true inBitmap = bitmapPool.getReusable(calculated) // 自己维护 LruCache }
一句话总结:
inSampleSize = 大于等于 max(原宽/目标宽,原高/目标高) 的最小 2 的幂,再按“低端机+密度”二次修正,确保解码后单张 Bitmap 占用 ≤ 控件像素×1.2×2 字节,即可在国内 90 % 机型上零 OOM。
拓展思考
- 折叠屏动态分辨率
监听 androidx.window:window 库,onLayoutChanged 时重新计算目标宽高,避免“展开后模糊”。 - 超分场景
若后台 AI 超分模块可用,可先 inSampleSize=4 解码 1/16 小图,TensorFlow Lite 超分后再上屏,省电 30 %。 - 隐私沙盒影响
Android 14 限制 QUERY_ALL_PACKAGES,图片选择器返回 content:// 不再带真实路径,上述流程必须基于 InputStream,不可假设 File。 - 可测试性
用 Compose Test 的onNodeWithTag("image").onGloballyPositioned{}拿到像素尺寸,配合 MockK 注入 DisplayMetrics,可在 JVM 单元测试直接断言 inSampleSize 值,无需真机。