Android学习总结之Bitmap篇
一、质量压缩(面试高频:原理与适用场景)
1. 核心原理(面试官必问)
质量压缩针对有损压缩格式(如 JPEG),通过丢弃图像中人类视觉不敏感的高频信息(如色彩过渡细节),在保持视觉尺寸不变的前提下减小文件大小。
- 关键参数:
Bitmap.compress(CompressFormat.JPEG, quality, outputStream)
,其中quality
(0-100)控制压缩程度,数值越低压缩率越高(文件越小,质量越差)。 - 内存占用不变:压缩后的图片文件大小减小,但解码后的
Bitmap
内存占用(像素数 × 单个像素字节)与原图一致(因未改变像素尺寸)。
2. 源码级实现与 Android 底层逻辑
// 质量压缩核心代码(面试需结合源码说明)
public static void qualityCompression(Bitmap bitmap, int quality, String filePath) { try (FileOutputStream fos = new FileOutputStream(filePath)) { // 调用Native层压缩逻辑(Android Framework层) bitmap.compress(CompressFormat.JPEG, quality, fos); } catch (IOException e) { e.printStackTrace(); }
}
- Native 层处理:
Bitmap.compress
最终调用android.graphics.Bitmap.nativeCompress
,通过 libjpeg 库实现压缩,根据quality
参数调整 DCT(离散余弦变换)的量化表,丢弃高频系数。 - 注意:PNG 格式不支持质量压缩(因 PNG 是无损格式),调用时
quality
参数会被忽略。
3. 适用场景(面试必答)
- 优化目标:减小图片文件大小(如网络传输、本地存储),而非降低内存占用(因内存占用由像素尺寸决定)。
- 典型场景:
- 上传用户头像前压缩(兼顾清晰度与流量)。
- 生成缩略图时保持视觉尺寸不变(如聊天表情包压缩)。
4. 优缺点(面试陷阱:与采样率压缩对比)
- 优点:
- 操作简单,无需计算尺寸,直接通过 API 实现。
- 保持图片视觉尺寸(宽高不变),适合需要固定显示大小的场景。
- 缺点:
- 有损压缩,过度压缩会导致马赛克、色块(如
quality<30
时明显失真)。 - 无法减少内存占用(仅减小文件大小),对预防 OOM 无帮助。
- 有损压缩,过度压缩会导致马赛克、色块(如
二、采样率压缩(面试核心:内存优化关键)
1. 核心原理(面试官深挖:内存计算公式)
通过inSampleSize
按比例缩小图片像素尺寸,使解码后的Bitmap
宽高为原图的1/inSampleSize
,内存占用降为原图的1/(inSampleSize²)
。
- 计算公式:
内存占用 = 宽 × 高 × 单个像素字节数(如ARGB_8888为4字节) 压缩后内存 = (原图宽/inSampleSize) × (原图高/inSampleSize) × 4
- 源码级实现:通过
BitmapFactory.Options.inSampleSize
控制,仅支持**≥1 的整数**,Android 4.4 + 允许非 2 的幂次(如 3、5),但会向下取整为最接近的 2 的幂次(如 3→2,5→4)。
2. 源码级流程(面试需画流程图)
public static Bitmap decodeSampledBitmap(String path, int reqWidth, int reqHeight) { // 第一步:获取原图尺寸(不加载内存) final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; BitmapFactory.decodeFile(path, options); // 第二步:计算inSampleSize(核心逻辑) options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight); // 第三步:按采样率解码(加载压缩后的Bitmap) options.inJustDecodeBounds = false; return BitmapFactory.decodeFile(path, options);
} private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) { int inSampleSize = 1; final int rawWidth = options.outWidth; final int rawHeight = options.outHeight; // 宽高均大于目标尺寸时,循环计算最大采样率 while (rawWidth / inSampleSize > reqWidth || rawHeight / inSampleSize > reqHeight) { inSampleSize *= 2; // 早期版本需为2的幂次,现支持动态计算 } return inSampleSize;
}
- Native 层解码:
BitmapFactory.decodeFile
调用nativeDecodeAsset
,根据inSampleSize
对像素数据进行 subsampling(亚采样),跳过部分像素点。
3. 适用场景(面试高频:ListView/RecyclerView 优化)
- 优化目标:减少
Bitmap
内存占用,预防 OOM(核心手段)。 - 典型场景:
- 列表项图片(如 RecyclerView 显示大量图片,每个 Item 仅需显示 200x200,原图可能为 2000x2000)。
- 加载高清图片时(如查看大图前先加载低分辨率预览图)。
4. 优缺点(面试对比质量压缩)
- 优点:
- 显著降低内存占用(按采样率平方级减少),是预防 OOM 的核心手段。
- 无损压缩(仅丢弃冗余像素,保留采样后像素的完整信息)。
- 缺点:
- 降低图片分辨率,可能导致细节丢失(如文字边缘模糊)。
- 需提前计算目标尺寸(reqWidth/reqHeight),依赖控件实际大小(需通过
getMeasuredWidth()
获取)。
三、面试高频问题与参考答案
1. “质量压缩和采样率压缩的核心区别是什么?”
- 答:
- 质量压缩:不改变像素尺寸,仅减小文件大小(适用于网络传输),内存占用不变,属有损压缩。
- 采样率压缩:按比例缩小像素尺寸,显著减少内存占用(适用于大图加载),属无损压缩(保留采样后像素)。
2. “如何计算 inSampleSize 才能既保证清晰度又减少内存?”
- 答:
- 通过
inJustDecodeBounds
获取原图宽高(outWidth/outHeight
)。 - 目标尺寸取控件实际宽高(如 ImageView 的
getMeasuredWidth()
)。 - 计算
inSampleSize
为原图宽高与目标宽高的最大比例(向上取 2 的幂次,Android 4.4 + 可非 2 幂次)。
inSampleSize = Math.max(rawWidth/reqWidth, rawHeight/reqHeight); inSampleSize = Integer.highestOneBit(inSampleSize); // 取最接近的2的幂次(旧版兼容)
- 通过
3. “为什么采样率压缩能有效预防 OOM?请写出内存占用计算公式。”
- 答:
- 内存占用与像素数成正比,采样率为
n
时,像素数降为1/n²
,内存占用同比例减少。 - 公式:
原图内存 = width × height × 4(ARGB_8888) 压缩后内存 = (width/n) × (height/n) × 4 = 原图内存 / n²
- 例:原图 2000x2000(16MB),采样率 4 后为 500x500(1MB),内存减少 93.75%。
- 内存占用与像素数成正比,采样率为
4. “质量压缩时,JPEG 和 PNG 有什么区别?”
- 答:
- JPEG:支持质量压缩(0-100),有损压缩,不支持透明通道,适合照片。
- PNG:不支持质量压缩(压缩参数被忽略),无损压缩,支持透明通道,适合图标 / 图形。
四、面试加分项:进阶优化策略
1. 图片格式优化(源码级支持)
- WebP 格式:Android 4.0 + 支持有损 WebP,4.3 + 支持无损 / 透明 WebP,同等质量下文件大小比 JPEG 小 25%-50%。
bitmap.compress(CompressFormat.WEBP, 80, fos); // 压缩为WebP格式
- Argb8888 vs Rgb565:后者单个像素占 2 字节(内存减半),但色彩精度低,适合对色彩不敏感的场景(如旧版 Android 列表图片)。
2. 内存缓存与磁盘缓存(结合 LruCache/DiskLruCache)
- 内存缓存:使用
LruCache
存储解码后的Bitmap
,按 LRU 淘汰,避免重复解码。// 计算缓存大小为可用内存的1/8(面试必答公式) int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); int cacheSize = maxMemory / 8; LruCache<String, Bitmap> memoryCache = new LruCache<>(cacheSize);
- 磁盘缓存:使用
DiskLruCache
存储压缩后的文件,减少网络请求或文件解码次数。
3. 硬件加速与复用(高级优化)
- Bitmap 复用:通过
BitmapFactory.Options.inBitmap
复用现有Bitmap
的内存(需尺寸、格式一致),减少内存分配 / 回收开销。options.inBitmap = reusableBitmap; options.inSampleSize = 2; options.inMutable = true; // 允许修改复用的Bitmap
- 硬件加速:开启 View 层级硬件加速(
android:hardwareAccelerated="true"
),将图片渲染转移到 GPU,减轻 CPU 压力。
使用场景对比
场景选择策略
1. 优先选择质量压缩的场景
-
目标:减小文件大小(如文件存储、网络传输),且需保持显示尺寸不变
- 典型场景:
- 上传图片到服务器(降低流量消耗),如用户头像、朋友圈图片(显示时需保持原图尺寸)。
- 保存截图到本地(希望文件更小,但预览时需完整尺寸)。
- 决策依据:
- 图片显示尺寸等于或接近原图尺寸(无需缩放显示),但需减小存储 / 传输体积。
- 允许一定程度的视觉损失(如 JPEG 质量设为 60-80 时,肉眼难以察觉明显失真)。
- 注意事项:
- 质量压缩对 PNG 无效(PNG 是无损格式,
quality
参数会被忽略)。 - 过度压缩(如质量 < 30)会导致色块、边缘模糊,需通过调试找到质量与大小的平衡点。
- 质量压缩对 PNG 无效(PNG 是无损格式,
- 典型场景:
-
代码示例(质量压缩):
bitmap.compress(Bitmap.CompressFormat.JPEG, 60, outputStream); // 保持尺寸,减小文件大小
2. 优先选择采样率压缩的场景
-
目标:降低内存占用(避免 OOM),且显示尺寸远小于原图尺寸
- 典型场景:
- 列表视图(如 RecyclerView)加载大量图片,原图尺寸(如 4000×3000)远大于显示尺寸(如 200×200)。
- 内存受限的低端设备加载高清图片,需提前缩放至显示所需尺寸。
- 决策依据:
- 显示区域的宽高 ≤ 原图宽高的 1/2(需计算
inSampleSize
,使加载后的像素尺寸接近显示尺寸)。 - 核心目标是减少
Bitmap
占用的内存(如加载 100 张图片时,内存占用从 100MB 降至 25MB)。
- 显示区域的宽高 ≤ 原图宽高的 1/2(需计算
- 实现步骤(关键源码逻辑):
- 获取原图尺寸(不加载像素数据):
BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; // 仅解码图片边界,不加载像素 BitmapFactory.decodeFile(imagePath, options);
- 计算最佳
inSampleSize
(保证加载后的尺寸 ≥ 显示尺寸):int reqWidth = 200; // 显示宽度 int reqHeight = 200; // 显示高度 int width = options.outWidth; int height = options.outHeight; int inSampleSize = 1; while (width / 2 >= reqWidth && height / 2 >= reqHeight) {width /= 2;height /= 2;inSampleSize *= 2; // 最终 inSampleSize 为 2 的幂次,确保解码效率 }
- 按采样率加载图片:
options.inJustDecodeBounds = false; options.inSampleSize = inSampleSize; Bitmap scaledBitmap = BitmapFactory.decodeFile(imagePath, options); // 内存占用降低为 1/inSampleSize²
- 获取原图尺寸(不加载像素数据):
- 典型场景:
-
注意事项:
inSampleSize
必须是 2 的幂次(如 1, 2, 4),非 2 的幂次会被向下取整为最接近的 2 的幂次(Android 源码逻辑)。- 采样率压缩后,若需进一步减小文件大小(如保存到本地),可结合质量压缩(先缩放尺寸,再压缩质量)。
3. 两者结合使用的场景
- 目标:同时优化内存占用和文件大小
- 典型场景:
- 从相册选取图片后,先按显示尺寸采样率压缩(降低内存),再按中等质量压缩(减小文件以便上传)。
- 生成缩略图:先通过采样率压缩到目标尺寸(如 500×500),再用质量压缩(如 JPEG 质量 70)保存。
- 执行顺序:先采样率压缩(减少像素数),再质量压缩(优化存储体积)。
- 原因:采样率压缩直接减少像素数量,质量压缩在已缩小的像素基础上进一步优化,效率更高。
- 典型场景:
面试高频问题延伸
1. 为什么质量压缩不影响 Bitmap 的内存占用?
- 答:
Bitmap
的内存占用由像素尺寸(宽 × 高)和像素格式(如 ARGB_8888 每个像素占 4 字节)决定。质量压缩仅改变存储的文件大小(如 JPEG 编码时丢弃细节),但解码后的Bitmap
宽高不变,因此内存占用不变。
2. 采样率压缩时,如何计算 inSampleSize 才能避免图片模糊?
- 答:
inSampleSize
应保证加载后的尺寸 略大于或等于显示尺寸。例如,显示区域为 200×200,原图为 2000×2000,则inSampleSize=10
(2000/10=200),加载后的尺寸刚好匹配显示区域,不会模糊。若inSampleSize
过大(如 20),加载后尺寸为 100×100,显示时需拉伸至 200×200,反而会模糊,此时应取inSampleSize=10
。
3. 能否对 PNG 图片进行采样率压缩?
- 答:可以。采样率压缩是通过
inSampleSize
缩放像素尺寸,与图片格式无关(PNG/JPEG 均适用)。但 PNG 是无损格式,质量压缩对其无效(设置quality
参数会被忽略),因此 PNG 只能通过采样率压缩来减少内存占用和文件大小。
总结:选择公式
- 若需控制内存占用(防 OOM),且显示尺寸 < 原图尺寸 → 优先采样率压缩(必须做)。
- 若需减小文件大小(存储 / 传输),且显示尺寸 = 原图尺寸 → 优先质量压缩(可选做)。
- 复杂场景(如先显示再保存)→ 先采样率压缩(降内存),再质量压缩(降文件大小)(最佳实践)。