面试里说自己做过 Android 内存优化,聊到图片压缩时很多人会顺手说"用了微信的压缩算法"。真正的追问往往马上就来了:你压缩的是文件大小,还是 Bitmap 内存?一张图片加载到内存后到底占多少?
一句话结论: 图片文件大小不等于 Bitmap 内存大小;面试回答要先区分"磁盘编码后的字节数"和"解码后的像素缓冲区",再用 宽 × 高 × 每像素字节数、getByteCount()、getAllocationByteCount() 和采样解码把工程优化讲完整。
本文按 Android 常见图片优化面试场景整理。Android Framework 的底层内存归属会随版本调整,文中的源码类名以官方 API、AOSP Bitmap / BitmapFactory 以及 Luban、Glide 的公开实现职责为锚点;具体字段和分支不要当成所有版本都完全一致。
1. 先把面试官的问题翻译成人话
面试官听到"我做过图片压缩"后,真正想确认的是三件事:
- 你是否知道图片上传压缩和图片内存优化不是同一件事。
- 你是否知道 Bitmap 的内存由解码后的像素决定,而不是原始文件体积决定。
- 你是否能把鲁班、
BitmapFactory.Options、Glide 这类工具放回同一条链路里讲清楚。
#mermaid-svg-2zAchyxdBdF0SDD3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-2zAchyxdBdF0SDD3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2zAchyxdBdF0SDD3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2zAchyxdBdF0SDD3 .error-icon{fill:#552222;}#mermaid-svg-2zAchyxdBdF0SDD3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2zAchyxdBdF0SDD3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2zAchyxdBdF0SDD3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2zAchyxdBdF0SDD3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2zAchyxdBdF0SDD3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2zAchyxdBdF0SDD3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2zAchyxdBdF0SDD3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2zAchyxdBdF0SDD3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2zAchyxdBdF0SDD3 .marker.cross{stroke:#333333;}#mermaid-svg-2zAchyxdBdF0SDD3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2zAchyxdBdF0SDD3 p{margin:0;}#mermaid-svg-2zAchyxdBdF0SDD3 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2zAchyxdBdF0SDD3 .cluster-label text{fill:#333;}#mermaid-svg-2zAchyxdBdF0SDD3 .cluster-label span{color:#333;}#mermaid-svg-2zAchyxdBdF0SDD3 .cluster-label span p{background-color:transparent;}#mermaid-svg-2zAchyxdBdF0SDD3 .label text,#mermaid-svg-2zAchyxdBdF0SDD3 span{fill:#333;color:#333;}#mermaid-svg-2zAchyxdBdF0SDD3 .node rect,#mermaid-svg-2zAchyxdBdF0SDD3 .node circle,#mermaid-svg-2zAchyxdBdF0SDD3 .node ellipse,#mermaid-svg-2zAchyxdBdF0SDD3 .node polygon,#mermaid-svg-2zAchyxdBdF0SDD3 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2zAchyxdBdF0SDD3 .rough-node .label text,#mermaid-svg-2zAchyxdBdF0SDD3 .node .label text,#mermaid-svg-2zAchyxdBdF0SDD3 .image-shape .label,#mermaid-svg-2zAchyxdBdF0SDD3 .icon-shape .label{text-anchor:middle;}#mermaid-svg-2zAchyxdBdF0SDD3 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2zAchyxdBdF0SDD3 .rough-node .label,#mermaid-svg-2zAchyxdBdF0SDD3 .node .label,#mermaid-svg-2zAchyxdBdF0SDD3 .image-shape .label,#mermaid-svg-2zAchyxdBdF0SDD3 .icon-shape .label{text-align:center;}#mermaid-svg-2zAchyxdBdF0SDD3 .node.clickable{cursor:pointer;}#mermaid-svg-2zAchyxdBdF0SDD3 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2zAchyxdBdF0SDD3 .arrowheadPath{fill:#333333;}#mermaid-svg-2zAchyxdBdF0SDD3 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2zAchyxdBdF0SDD3 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2zAchyxdBdF0SDD3 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2zAchyxdBdF0SDD3 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2zAchyxdBdF0SDD3 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2zAchyxdBdF0SDD3 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2zAchyxdBdF0SDD3 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2zAchyxdBdF0SDD3 .cluster text{fill:#333;}#mermaid-svg-2zAchyxdBdF0SDD3 .cluster span{color:#333;}#mermaid-svg-2zAchyxdBdF0SDD3 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-2zAchyxdBdF0SDD3 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2zAchyxdBdF0SDD3 rect.text{fill:none;stroke-width:0;}#mermaid-svg-2zAchyxdBdF0SDD3 .icon-shape,#mermaid-svg-2zAchyxdBdF0SDD3 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2zAchyxdBdF0SDD3 .icon-shape p,#mermaid-svg-2zAchyxdBdF0SDD3 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2zAchyxdBdF0SDD3 .icon-shape .label rect,#mermaid-svg-2zAchyxdBdF0SDD3 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2zAchyxdBdF0SDD3 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2zAchyxdBdF0SDD3 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2zAchyxdBdF0SDD3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 阶段三:真正进入内存和渲染
阶段二:进入 Android 解码链路
阶段一:图片还只是编码文件
磁盘或网络图片文件
文件格式:JPEG PNG WebP AVIF
关注点:上传体积和下载流量
BitmapFactory 执行解码
先读取宽高和方向信息
按目标控件尺寸计算采样率
选择 Bitmap Config
分配像素缓冲区
内存成本:宽高乘每像素字节数
ImageView 显示或纹理上传
这张图里,File 阶段看到的是 JPEG、PNG、WebP、AVIF 这类编码后的文件体积;Buffer 阶段看到的是 Bitmap 像素缓冲区。图片压缩如果只改变 JPEG 质量,文件会变小,但只要解码后的宽高和像素格式没有变,Bitmap 内存通常不会跟着变小。
2. 三种大小必须分开说
| 名称 | 你看到的是什么 | 常见误区 |
|---|---|---|
| 文件大小 | 磁盘、网络传输、上传接口里的字节数 | 以为 200 KB 图片解码后也只占 200 KB |
| 解码尺寸 | Bitmap 的宽、高、Bitmap.Config |
只看原图分辨率,不看目标 View 尺寸 |
| 内存占用 | 像素缓冲区、行跨度、底层分配容量 | 忽略 rowBytes、复用池和硬件位图 |
举个最容易被问住的例子:
text
一张 4000 x 3000 的 JPEG 文件,磁盘上可能只有 2 MB。
如果按 ARGB_8888 解码:
4000 x 3000 x 4 = 48,000,000 bytes
约等于 45.78 MiB
文件很小,是因为 JPEG 做了编码压缩;Bitmap 很大,是因为它已经被解码成一格一格的像素。UI 渲染需要像素,不能直接拿 JPEG 的压缩流去画。
3. Bitmap 内存的基础公式
理想近似公式是:
text
Bitmap 内存约等于 width x height x bytesPerPixel
常见 Bitmap.Config 可以这样记:
| Config | 每像素字节数 | 说明 |
|---|---|---|
ARGB_8888 |
4 | 默认常见配置,包含透明度,质量较好 |
RGB_565 |
2 | 没有 alpha,颜色质量下降,老项目偶尔用于缩内存 |
ALPHA_8 |
1 | 只存透明度 |
RGBA_F16 |
8 | 宽色域和高精度场景,内存更高 |
HARDWARE |
不能只按 Java 堆理解 | 像素通常在图形相关内存中,适合展示,不适合直接修改 |
例子:
text
1080 x 1920 x 4 = 8,294,400 bytes
8,294,400 / 1024 / 1024 = 7.91 MiB
这里故意写成 MiB,因为内存分析里经常用 1024 进位。日常口语说 8 MB 问题不大,但面试里能把 MB 和 MiB 的区别说出来,会显得你不是在背结论。
4. 源码和 API 里到底怎么取
从 Android API 层看,最常用的是这几个方法:
| API | 作用 | 面试怎么说 |
|---|---|---|
bitmap.getByteCount() |
当前 Bitmap 像素数据需要的字节数 | 适合回答"这张 Bitmap 当前内容要多少内存" |
bitmap.getAllocationByteCount() |
底层已分配的像素内存容量 | 复用 Bitmap 后可能大于 byteCount |
bitmap.getRowBytes() |
一行像素之间的字节跨度 | 旧版本或理解行对齐时常用 |
#mermaid-svg-NJAPqtFxlDG7o7gh{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-NJAPqtFxlDG7o7gh .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-NJAPqtFxlDG7o7gh .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-NJAPqtFxlDG7o7gh .error-icon{fill:#552222;}#mermaid-svg-NJAPqtFxlDG7o7gh .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-NJAPqtFxlDG7o7gh .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-NJAPqtFxlDG7o7gh .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-NJAPqtFxlDG7o7gh .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-NJAPqtFxlDG7o7gh .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-NJAPqtFxlDG7o7gh .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-NJAPqtFxlDG7o7gh .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-NJAPqtFxlDG7o7gh .marker{fill:#333333;stroke:#333333;}#mermaid-svg-NJAPqtFxlDG7o7gh .marker.cross{stroke:#333333;}#mermaid-svg-NJAPqtFxlDG7o7gh svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-NJAPqtFxlDG7o7gh p{margin:0;}#mermaid-svg-NJAPqtFxlDG7o7gh .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-NJAPqtFxlDG7o7gh .cluster-label text{fill:#333;}#mermaid-svg-NJAPqtFxlDG7o7gh .cluster-label span{color:#333;}#mermaid-svg-NJAPqtFxlDG7o7gh .cluster-label span p{background-color:transparent;}#mermaid-svg-NJAPqtFxlDG7o7gh .label text,#mermaid-svg-NJAPqtFxlDG7o7gh span{fill:#333;color:#333;}#mermaid-svg-NJAPqtFxlDG7o7gh .node rect,#mermaid-svg-NJAPqtFxlDG7o7gh .node circle,#mermaid-svg-NJAPqtFxlDG7o7gh .node ellipse,#mermaid-svg-NJAPqtFxlDG7o7gh .node polygon,#mermaid-svg-NJAPqtFxlDG7o7gh .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-NJAPqtFxlDG7o7gh .rough-node .label text,#mermaid-svg-NJAPqtFxlDG7o7gh .node .label text,#mermaid-svg-NJAPqtFxlDG7o7gh .image-shape .label,#mermaid-svg-NJAPqtFxlDG7o7gh .icon-shape .label{text-anchor:middle;}#mermaid-svg-NJAPqtFxlDG7o7gh .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-NJAPqtFxlDG7o7gh .rough-node .label,#mermaid-svg-NJAPqtFxlDG7o7gh .node .label,#mermaid-svg-NJAPqtFxlDG7o7gh .image-shape .label,#mermaid-svg-NJAPqtFxlDG7o7gh .icon-shape .label{text-align:center;}#mermaid-svg-NJAPqtFxlDG7o7gh .node.clickable{cursor:pointer;}#mermaid-svg-NJAPqtFxlDG7o7gh .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-NJAPqtFxlDG7o7gh .arrowheadPath{fill:#333333;}#mermaid-svg-NJAPqtFxlDG7o7gh .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-NJAPqtFxlDG7o7gh .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-NJAPqtFxlDG7o7gh .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NJAPqtFxlDG7o7gh .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-NJAPqtFxlDG7o7gh .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NJAPqtFxlDG7o7gh .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-NJAPqtFxlDG7o7gh .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-NJAPqtFxlDG7o7gh .cluster text{fill:#333;}#mermaid-svg-NJAPqtFxlDG7o7gh .cluster span{color:#333;}#mermaid-svg-NJAPqtFxlDG7o7gh div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-NJAPqtFxlDG7o7gh .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-NJAPqtFxlDG7o7gh rect.text{fill:none;stroke-width:0;}#mermaid-svg-NJAPqtFxlDG7o7gh .icon-shape,#mermaid-svg-NJAPqtFxlDG7o7gh .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-NJAPqtFxlDG7o7gh .icon-shape p,#mermaid-svg-NJAPqtFxlDG7o7gh .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-NJAPqtFxlDG7o7gh .icon-shape .label rect,#mermaid-svg-NJAPqtFxlDG7o7gh .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-NJAPqtFxlDG7o7gh .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-NJAPqtFxlDG7o7gh .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-NJAPqtFxlDG7o7gh :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 文件体积
理论估算
已有对象
是
否
面试官问:图片到底占多少内存
先判断问的是哪一种大小
编码后字节数:磁盘和网络看到的大小
受格式 质量 尺寸影响
公式:宽乘高乘每像素字节数
根据 Config 判断每像素字节数
再补充 rowBytes 和行对齐
优先读取 Bitmap API
getByteCount 表示当前像素数据大小
是否来自复用池或发生过复用
getAllocationByteCount 可能更接近实际分配
byteCount 通常足够回答当前内容
最终回答:说明口径和版本边界
源码事实可以这样理解:
| 源码锚点 | 关注点 | 为什么重要 |
|---|---|---|
BitmapFactory.decodeFile / decodeStream |
从编码数据解码出 Bitmap | 决定是否真的分配像素内存 |
BitmapFactory.Options.inJustDecodeBounds |
只读宽高,不分配完整像素 | 解码前计算采样率 |
BitmapFactory.Options.inSampleSize |
请求按比例采样解码 | 宽高缩小后,内存按面积下降 |
Bitmap.getByteCount() |
当前像素数据大小 | 比自己猜更可靠 |
Bitmap.getAllocationByteCount() |
底层分配容量 | 解释 Bitmap 复用池带来的差异 |
教学伪代码如下:
kotlin
// 简化代码:只表达计算思路,不处理硬件位图、色彩空间和异常输入。
fun estimateBitmapBytes(
width: Int,
height: Int,
bytesPerPixel: Int
): Long {
return width.toLong() * height.toLong() * bytesPerPixel.toLong()
}
fun Bitmap.pixelBytesForInterview(): Int {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
allocationByteCount
} else {
byteCount
}
}
allocationByteCount 的价值在于解释"当前 Bitmap 内容"和"底层分配容量"不是永远相等。比如图片库为了减少频繁分配,会从 BitmapPool 里拿一个更大的 Bitmap 复用,这时当前图片的 byteCount 可能比实际已分配容量小。
5. rowBytes × height 为什么还会被提到
老资料里经常出现这个公式:
text
bitmap.getRowBytes() x bitmap.getHeight()
它表达的是:一行像素不一定刚好等于 width × bytesPerPixel,底层可能因为对齐、格式或实现差异让行跨度更大。所以面试里比较稳的说法是:
估算用
宽 × 高 × bpp,拿到 Bitmap 对象后优先看getByteCount()或getAllocationByteCount();理解底层时再补充rowBytes × height和行对齐。
这句话比直接背一个公式更完整。
6. inSampleSize 才是图片内存优化的关键入口
如果原图是 4000 × 3000,目标 ImageView 只显示 1000 × 750,就没必要先解码完整原图再缩放。应该在解码阶段就采样:
text
宽高都变成原来的 1 / n
像素数量约变成原来的 1 / n²
内存也约变成原来的 1 / n²
例如宽高各缩小到一半,内存约降到四分之一。
kotlin
// 简化代码:适合学习 inJustDecodeBounds + inSampleSize 的流程。
fun decodeSampledBitmap(
filePath: String,
targetWidth: Int,
targetHeight: Int
): Bitmap {
val boundsOptions = BitmapFactory.Options().apply {
inJustDecodeBounds = true
}
BitmapFactory.decodeFile(filePath, boundsOptions)
val sampleSize = calculateInSampleSize(
srcWidth = boundsOptions.outWidth,
srcHeight = boundsOptions.outHeight,
targetWidth = targetWidth,
targetHeight = targetHeight
)
val decodeOptions = BitmapFactory.Options().apply {
inSampleSize = sampleSize
inPreferredConfig = Bitmap.Config.ARGB_8888
}
return BitmapFactory.decodeFile(filePath, decodeOptions)
}
fun calculateInSampleSize(
srcWidth: Int,
srcHeight: Int,
targetWidth: Int,
targetHeight: Int
): Int {
var sampleSize = 1
var halfWidth = srcWidth / 2
var halfHeight = srcHeight / 2
while (halfWidth / sampleSize >= targetWidth &&
halfHeight / sampleSize >= targetHeight
) {
sampleSize *= 2
}
return sampleSize.coerceAtLeast(1)
}
这段代码故意省略了 EXIF 方向、超长图、区域解码、硬件位图、色彩空间和并发控制。真实项目里,图片显示尺寸不只是 XML 里的 layout_width,还可能受屏幕密度、ImageView.ScaleType、列表 item 尺寸和服务端裁剪策略影响。
7. "用了微信算法"到底应该怎么说
面试中大家说的"微信压缩算法",通常指开源库 Luban 的思路:它模仿微信发送图片后的压缩效果,根据原图宽高、比例、文件大小等经验规则,决定是否压缩、目标尺寸和 JPEG 质量。
更严谨的说法是:
我们项目里用的是 Luban 这类经验型图片压缩策略,它不是一种新的底层编解码格式,也不是微信官方公开的完整算法。它主要降低上传和存储的文件大小;如果策略中同时缩小了解码尺寸,后续加载这张压缩后图片时,Bitmap 内存才会下降。
#mermaid-svg-t9X2iDU12fm6GVnf{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-t9X2iDU12fm6GVnf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-t9X2iDU12fm6GVnf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-t9X2iDU12fm6GVnf .error-icon{fill:#552222;}#mermaid-svg-t9X2iDU12fm6GVnf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-t9X2iDU12fm6GVnf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-t9X2iDU12fm6GVnf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-t9X2iDU12fm6GVnf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-t9X2iDU12fm6GVnf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-t9X2iDU12fm6GVnf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-t9X2iDU12fm6GVnf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-t9X2iDU12fm6GVnf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-t9X2iDU12fm6GVnf .marker.cross{stroke:#333333;}#mermaid-svg-t9X2iDU12fm6GVnf svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-t9X2iDU12fm6GVnf p{margin:0;}#mermaid-svg-t9X2iDU12fm6GVnf .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-t9X2iDU12fm6GVnf .cluster-label text{fill:#333;}#mermaid-svg-t9X2iDU12fm6GVnf .cluster-label span{color:#333;}#mermaid-svg-t9X2iDU12fm6GVnf .cluster-label span p{background-color:transparent;}#mermaid-svg-t9X2iDU12fm6GVnf .label text,#mermaid-svg-t9X2iDU12fm6GVnf span{fill:#333;color:#333;}#mermaid-svg-t9X2iDU12fm6GVnf .node rect,#mermaid-svg-t9X2iDU12fm6GVnf .node circle,#mermaid-svg-t9X2iDU12fm6GVnf .node ellipse,#mermaid-svg-t9X2iDU12fm6GVnf .node polygon,#mermaid-svg-t9X2iDU12fm6GVnf .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-t9X2iDU12fm6GVnf .rough-node .label text,#mermaid-svg-t9X2iDU12fm6GVnf .node .label text,#mermaid-svg-t9X2iDU12fm6GVnf .image-shape .label,#mermaid-svg-t9X2iDU12fm6GVnf .icon-shape .label{text-anchor:middle;}#mermaid-svg-t9X2iDU12fm6GVnf .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-t9X2iDU12fm6GVnf .rough-node .label,#mermaid-svg-t9X2iDU12fm6GVnf .node .label,#mermaid-svg-t9X2iDU12fm6GVnf .image-shape .label,#mermaid-svg-t9X2iDU12fm6GVnf .icon-shape .label{text-align:center;}#mermaid-svg-t9X2iDU12fm6GVnf .node.clickable{cursor:pointer;}#mermaid-svg-t9X2iDU12fm6GVnf .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-t9X2iDU12fm6GVnf .arrowheadPath{fill:#333333;}#mermaid-svg-t9X2iDU12fm6GVnf .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-t9X2iDU12fm6GVnf .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-t9X2iDU12fm6GVnf .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t9X2iDU12fm6GVnf .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-t9X2iDU12fm6GVnf .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t9X2iDU12fm6GVnf .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-t9X2iDU12fm6GVnf .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-t9X2iDU12fm6GVnf .cluster text{fill:#333;}#mermaid-svg-t9X2iDU12fm6GVnf .cluster span{color:#333;}#mermaid-svg-t9X2iDU12fm6GVnf div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-t9X2iDU12fm6GVnf .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-t9X2iDU12fm6GVnf rect.text{fill:none;stroke-width:0;}#mermaid-svg-t9X2iDU12fm6GVnf .icon-shape,#mermaid-svg-t9X2iDU12fm6GVnf .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-t9X2iDU12fm6GVnf .icon-shape p,#mermaid-svg-t9X2iDU12fm6GVnf .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-t9X2iDU12fm6GVnf .icon-shape .label rect,#mermaid-svg-t9X2iDU12fm6GVnf .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-t9X2iDU12fm6GVnf .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-t9X2iDU12fm6GVnf .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-t9X2iDU12fm6GVnf :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 重新编码
尺寸策略
输入与判断
否
是
原图文件进入压缩流程
读取文件大小 宽高 比例 方向
是否超过压缩阈值
不压缩:直接使用原文件
按长边短边和宽高比计算目标尺寸
按目标尺寸采样解码
修正 EXIF 方向
JPEG 质量参数重新编码
输出更适合上传的文件
这条链路里有两个点特别容易讲错:
Encode["JPEG 质量编码"]主要改变文件大小。只改 quality,不改宽高,重新解码后的 Bitmap 内存通常还是接近原来的大小。Scale["按长短边与比例计算目标尺寸"]会改变像素数量。只有尺寸真的变小,内存才会按面积下降。
一个教学版 Luban-like 流程可以写成这样:
kotlin
// 简化代码:表达 Luban 类策略的思想,不代表 Luban 当前版本源码逐行实现。
fun compressForUpload(input: File, output: File): File {
val info = readImageInfo(input)
if (info.sizeKb < 100 && info.shortSide < 1280) {
return input
}
val target = when {
info.ratio > 2.0f -> computeLongImageTarget(info)
info.longSide > 1920 -> scaleByLongSide(info, maxLongSide = 1920)
else -> info.size
}
val bitmap = decodeSampledBitmap(input.path, target.width, target.height)
val fixed = rotateByExif(bitmap, info.exifOrientation)
output.outputStream().use { stream ->
fixed.compress(Bitmap.CompressFormat.JPEG, 80, stream)
}
return output
}
生产实现还要处理 PNG 透明通道、WebP/AVIF 支持、压缩失败回退、临时文件清理、超大图 OOM、防止主线程执行和隐私路径隔离。
8. Glide 为什么经常比手写解码稳
手写 BitmapFactory.decodeFile() 最大的问题不是"不能用",而是容易漏掉目标尺寸、缓存、复用和生命周期。Glide 做得好的地方是把这些问题放进一条流水线:
#mermaid-svg-c28JA9TmMxDSopU5{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-c28JA9TmMxDSopU5 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-c28JA9TmMxDSopU5 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-c28JA9TmMxDSopU5 .error-icon{fill:#552222;}#mermaid-svg-c28JA9TmMxDSopU5 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-c28JA9TmMxDSopU5 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-c28JA9TmMxDSopU5 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-c28JA9TmMxDSopU5 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-c28JA9TmMxDSopU5 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-c28JA9TmMxDSopU5 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-c28JA9TmMxDSopU5 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-c28JA9TmMxDSopU5 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-c28JA9TmMxDSopU5 .marker.cross{stroke:#333333;}#mermaid-svg-c28JA9TmMxDSopU5 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-c28JA9TmMxDSopU5 p{margin:0;}#mermaid-svg-c28JA9TmMxDSopU5 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-c28JA9TmMxDSopU5 .cluster-label text{fill:#333;}#mermaid-svg-c28JA9TmMxDSopU5 .cluster-label span{color:#333;}#mermaid-svg-c28JA9TmMxDSopU5 .cluster-label span p{background-color:transparent;}#mermaid-svg-c28JA9TmMxDSopU5 .label text,#mermaid-svg-c28JA9TmMxDSopU5 span{fill:#333;color:#333;}#mermaid-svg-c28JA9TmMxDSopU5 .node rect,#mermaid-svg-c28JA9TmMxDSopU5 .node circle,#mermaid-svg-c28JA9TmMxDSopU5 .node ellipse,#mermaid-svg-c28JA9TmMxDSopU5 .node polygon,#mermaid-svg-c28JA9TmMxDSopU5 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-c28JA9TmMxDSopU5 .rough-node .label text,#mermaid-svg-c28JA9TmMxDSopU5 .node .label text,#mermaid-svg-c28JA9TmMxDSopU5 .image-shape .label,#mermaid-svg-c28JA9TmMxDSopU5 .icon-shape .label{text-anchor:middle;}#mermaid-svg-c28JA9TmMxDSopU5 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-c28JA9TmMxDSopU5 .rough-node .label,#mermaid-svg-c28JA9TmMxDSopU5 .node .label,#mermaid-svg-c28JA9TmMxDSopU5 .image-shape .label,#mermaid-svg-c28JA9TmMxDSopU5 .icon-shape .label{text-align:center;}#mermaid-svg-c28JA9TmMxDSopU5 .node.clickable{cursor:pointer;}#mermaid-svg-c28JA9TmMxDSopU5 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-c28JA9TmMxDSopU5 .arrowheadPath{fill:#333333;}#mermaid-svg-c28JA9TmMxDSopU5 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-c28JA9TmMxDSopU5 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-c28JA9TmMxDSopU5 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c28JA9TmMxDSopU5 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-c28JA9TmMxDSopU5 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c28JA9TmMxDSopU5 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-c28JA9TmMxDSopU5 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-c28JA9TmMxDSopU5 .cluster text{fill:#333;}#mermaid-svg-c28JA9TmMxDSopU5 .cluster span{color:#333;}#mermaid-svg-c28JA9TmMxDSopU5 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-c28JA9TmMxDSopU5 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-c28JA9TmMxDSopU5 rect.text{fill:none;stroke-width:0;}#mermaid-svg-c28JA9TmMxDSopU5 .icon-shape,#mermaid-svg-c28JA9TmMxDSopU5 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-c28JA9TmMxDSopU5 .icon-shape p,#mermaid-svg-c28JA9TmMxDSopU5 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-c28JA9TmMxDSopU5 .icon-shape .label rect,#mermaid-svg-c28JA9TmMxDSopU5 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-c28JA9TmMxDSopU5 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-c28JA9TmMxDSopU5 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-c28JA9TmMxDSopU5 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 解码和回调
缓存与复用判断
请求入口
是
否
是
否
Glide load 接收图片模型
RequestBuilder 构建图片请求
Target 提供显示尺寸
生成 EngineKey
活动资源是否命中
内存缓存是否命中
BitmapPool 尝试提供可复用 Bitmap
Downsampler 按目标尺寸解码
执行裁剪 变换 转码
写入活动资源或缓存
回调 ImageView Target
这张图能回答图片库的四个核心问题:
| 问题 | Glide 的典型做法 | 好处 |
|---|---|---|
| 解多大 | 根据 Target 尺寸、override 和变换计算 |
避免原图尺寸解码进内存 |
| 怎么复用 | BitmapPool 复用可变 Bitmap |
减少频繁分配和 GC 抖动 |
| 怎么命中 | 内存缓存、活动资源、磁盘缓存 | 降低重复解码和网络成本 |
| 何时停止 | 请求绑定 Activity 或 Fragment 生命周期 | 页面不可见或销毁时暂停、清理 |
注意,skipMemoryCache(true) 不是通用内存优化。它可能减少某些资源停留在内存缓存里的时间,但也可能让列表回滑时重复解码、重复分配,反而更抖。真正优先级更高的是:按显示尺寸加载、限制并发、复用缓存、及时清理生命周期外请求。
9. 图片压缩分四类,别混着答
| 类型 | 改变什么 | 是否直接降低 Bitmap 内存 |
|---|---|---|
| 质量压缩 | JPEG/WebP quality | 通常不直接降低,除非同时改变尺寸或格式 |
| 尺寸压缩 | 宽高像素数 | 直接降低,按面积下降 |
| 格式选择 | 编码格式、透明度、兼容性 | 影响文件体积和解码路径,不等于固定省内存 |
| 色彩配置 | 每像素字节数 | 直接影响,但会带来质量或能力损失 |
所以面试里可以这样补一句:
上传前压缩一般优先解决网络和存储;展示时优化要关注解码尺寸、Bitmap 配置、缓存和生命周期。两者有关联,但不能画等号。
10. 真正会导致 OOM 的常见链路
图片 OOM 通常不是"某一张图太大"这么简单,而是多个因素叠加:
| 场景 | 根因 | 排查工具 |
|---|---|---|
| 原图直接加载到列表 | 没有按 View 尺寸 downsample | Android Studio Memory Profiler、Glide 日志 |
| 长图或超宽图 | 单边过大,总像素数爆炸 | BitmapFactory.Options 读 bounds、线上尺寸日志 |
| 快速滑动 | 同时解码多张,缓存和临时对象抖动 | Perfetto、Memory Profiler |
| 缓存无上限 | 强引用或自定义 LRU 不合理 | Heap dump、LeakCanary |
| Activity 泄漏 | ImageView、Target、回调链持有页面 | LeakCanary |
| 硬件位图异常 | 内存不都体现在 Java heap | dumpsys meminfo、Android Studio Profiler |
工程上建议建立两类日志:
kotlin
data class BitmapDebugInfo(
val source: String,
val outWidth: Int,
val outHeight: Int,
val config: Bitmap.Config?,
val byteCount: Int,
val allocationByteCount: Int
)
fun Bitmap.toDebugInfo(source: String): BitmapDebugInfo {
return BitmapDebugInfo(
source = source,
outWidth = width,
outHeight = height,
config = config,
byteCount = byteCount,
allocationByteCount = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
allocationByteCount
} else {
byteCount
}
)
}
线上不建议无脑打印每张图的完整 URL 或本地路径,注意隐私和日志体积。更稳的是记录业务场景、图片来源类型、目标尺寸、实际解码尺寸和内存区间。
11. 面试回答模板
可以按这条顺序回答:
#mermaid-svg-jLDHVokUGoSsAHaA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-jLDHVokUGoSsAHaA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-jLDHVokUGoSsAHaA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-jLDHVokUGoSsAHaA .error-icon{fill:#552222;}#mermaid-svg-jLDHVokUGoSsAHaA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-jLDHVokUGoSsAHaA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-jLDHVokUGoSsAHaA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-jLDHVokUGoSsAHaA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-jLDHVokUGoSsAHaA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-jLDHVokUGoSsAHaA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-jLDHVokUGoSsAHaA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-jLDHVokUGoSsAHaA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-jLDHVokUGoSsAHaA .marker.cross{stroke:#333333;}#mermaid-svg-jLDHVokUGoSsAHaA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-jLDHVokUGoSsAHaA p{margin:0;}#mermaid-svg-jLDHVokUGoSsAHaA .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-jLDHVokUGoSsAHaA .cluster-label text{fill:#333;}#mermaid-svg-jLDHVokUGoSsAHaA .cluster-label span{color:#333;}#mermaid-svg-jLDHVokUGoSsAHaA .cluster-label span p{background-color:transparent;}#mermaid-svg-jLDHVokUGoSsAHaA .label text,#mermaid-svg-jLDHVokUGoSsAHaA span{fill:#333;color:#333;}#mermaid-svg-jLDHVokUGoSsAHaA .node rect,#mermaid-svg-jLDHVokUGoSsAHaA .node circle,#mermaid-svg-jLDHVokUGoSsAHaA .node ellipse,#mermaid-svg-jLDHVokUGoSsAHaA .node polygon,#mermaid-svg-jLDHVokUGoSsAHaA .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-jLDHVokUGoSsAHaA .rough-node .label text,#mermaid-svg-jLDHVokUGoSsAHaA .node .label text,#mermaid-svg-jLDHVokUGoSsAHaA .image-shape .label,#mermaid-svg-jLDHVokUGoSsAHaA .icon-shape .label{text-anchor:middle;}#mermaid-svg-jLDHVokUGoSsAHaA .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-jLDHVokUGoSsAHaA .rough-node .label,#mermaid-svg-jLDHVokUGoSsAHaA .node .label,#mermaid-svg-jLDHVokUGoSsAHaA .image-shape .label,#mermaid-svg-jLDHVokUGoSsAHaA .icon-shape .label{text-align:center;}#mermaid-svg-jLDHVokUGoSsAHaA .node.clickable{cursor:pointer;}#mermaid-svg-jLDHVokUGoSsAHaA .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-jLDHVokUGoSsAHaA .arrowheadPath{fill:#333333;}#mermaid-svg-jLDHVokUGoSsAHaA .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-jLDHVokUGoSsAHaA .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-jLDHVokUGoSsAHaA .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jLDHVokUGoSsAHaA .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-jLDHVokUGoSsAHaA .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jLDHVokUGoSsAHaA .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-jLDHVokUGoSsAHaA .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-jLDHVokUGoSsAHaA .cluster text{fill:#333;}#mermaid-svg-jLDHVokUGoSsAHaA .cluster span{color:#333;}#mermaid-svg-jLDHVokUGoSsAHaA div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-jLDHVokUGoSsAHaA .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-jLDHVokUGoSsAHaA rect.text{fill:none;stroke-width:0;}#mermaid-svg-jLDHVokUGoSsAHaA .icon-shape,#mermaid-svg-jLDHVokUGoSsAHaA .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-jLDHVokUGoSsAHaA .icon-shape p,#mermaid-svg-jLDHVokUGoSsAHaA .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-jLDHVokUGoSsAHaA .icon-shape .label rect,#mermaid-svg-jLDHVokUGoSsAHaA .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-jLDHVokUGoSsAHaA .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-jLDHVokUGoSsAHaA .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-jLDHVokUGoSsAHaA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 面试官追问:图片占用内存怎么算
第一步:区分文件大小和 Bitmap 内存
文件大小属于上传 下载 存储问题
Bitmap 内存属于解码后像素问题
第二步:给出核心公式
宽乘高乘每像素字节数
ARGB_8888 通常是四字节每像素
第三步:说 Android API
getByteCount 看当前像素数据
getAllocationByteCount 看底层分配容量
第四步:解释鲁班类压缩策略
质量压缩主要降低文件体积
尺寸缩小才会明显降低 Bitmap 内存
第五步:回到工程优化
按 View 尺寸解码
控制缓存 并发 生命周期
口语版可以这样说:
图片内存不能看文件大小。JPEG 可能只有几百 KB,但解码成 Bitmap 后要看宽、高和像素格式。常见 ARGB_8888 是 4 字节每像素,所以近似是
width × height × 4。拿到 Bitmap 对象后,我会看getByteCount();如果涉及 Bitmap 复用,Android 4.4 以后还要看getAllocationByteCount(),因为底层分配容量可能大于当前内容大小。老公式rowBytes × height可以用来理解行跨度和对齐。
然后继续接:
我们说的微信压缩,项目里一般是 Luban 这类经验策略:按宽高比、长短边和文件大小决定目标尺寸和 JPEG 质量。质量压缩主要降低上传文件大小;只有真正缩小宽高,后续解码出来的 Bitmap 内存才会下降。展示侧我更关注按控件尺寸解码、
inSampleSize、Glide 的 downsample、BitmapPool、内存缓存上限和生命周期清理。
最后补工程实践:
排查 OOM 时我会先看是不是原图尺寸进内存、列表并发解码、长图、缓存过大或页面泄漏。工具上用 Memory Profiler 看分配,LeakCanary 查泄漏,必要时用
dumpsys meminfo区分 Java heap、native 和 graphics 相关内存。
12. 小结
图片优化最容易答偏,是因为"压缩"这个词同时被用于上传、存储、解码和显示。面试里不要急着报库名,先把链路拆开:
- 文件大小由编码格式、质量和尺寸共同决定。
- Bitmap 内存主要由解码后的宽、高和每像素字节数决定。
getByteCount()看当前内容,getAllocationByteCount()看底层分配容量。inSampleSize、目标尺寸和 Glide downsample 才是展示侧降内存的主入口。- Luban 类策略更偏上传压缩;只有尺寸变小,才会让后续 Bitmap 内存下降。
把这几层说清楚,面试官继续追问公式、源码 API、鲁班边界和 Glide 优化时,就不会被"压缩过了为什么还 OOM"这个问题带偏。
13. 面试计算题:多大分辨率的图片占多少内存
面试官很可能不会停在"公式是什么",而是现场给你一个分辨率,让你直接算。做题时按这个模板来:
text
Bitmap 内存 = 宽 x 高 x 每像素字节数
ARGB_8888 = 4 字节每像素
RGB_565 = 2 字节每像素
MiB = bytes / 1024 / 1024
题目 1:1080 x 1920 的 ARGB_8888 图片占多少内存
text
1080 x 1920 x 4
= 8,294,400 bytes
= 7.91 MiB
面试回答: 如果按 ARGB_8888 解码,约占 7.91 MiB。如果是 RGB_565,则是:
text
1080 x 1920 x 2
= 4,147,200 bytes
= 3.96 MiB
所以同样分辨率下,RGB_565 理论上约省一半像素内存,但会牺牲透明度和颜色质量,不能无脑替换。
题目 2:4000 x 3000 的 JPEG 解码成 ARGB_8888 占多少内存
text
4000 x 3000 x 4
= 48,000,000 bytes
= 45.78 MiB
面试回答: 哪怕这张 JPEG 文件在磁盘上只有 2 MB,只要它完整解码成 4000 x 3000 的 ARGB_8888 Bitmap,像素内存大约就是 45.78 MiB。文件大小和 Bitmap 内存不是一个口径。
题目 3:同一张 4000 x 3000 图片,inSampleSize = 2 后占多少内存
inSampleSize = 2 可以粗略理解为宽高各缩小到原来的二分之一:
text
解码后宽高约为 2000 x 1500
2000 x 1500 x 4
= 12,000,000 bytes
= 11.44 MiB
面试回答: 宽高各缩小一半,像素总数变成四分之一,所以内存也约变成四分之一。原来 45.78 MiB,采样后约 11.44 MiB。
题目 4:1440 x 3200 的 ARGB_8888 全屏大图占多少内存
text
1440 x 3200 x 4
= 18,432,000 bytes
= 17.58 MiB
面试回答: 这类看起来只是"一张手机全屏图"的资源,解码后也可能接近 18 MiB。如果列表里同时出现多张、再叠加缓存和纹理上传,就很容易产生内存压力。
最后补一句边界
真实项目里,最终数值还可能受 rowBytes 行对齐、BitmapPool 复用、allocationByteCount、硬件位图和 Android 版本实现影响。面试中先用公式快速估算,再补一句"拿到对象后以 getByteCount() / getAllocationByteCount() 为准",会比只背一个数字稳很多。
14. 源码与参考入口
以下入口用来辅助定位源码职责;具体实现请以项目锁定版本为准。
| 主题 | 链接 |
|---|---|
Android Bitmap API |
android.graphics.Bitmap |
Android Bitmap.Config API |
android.graphics.Bitmap.Config |
Android BitmapFactory.Options API |
BitmapFactory.Options |
| Android 加载大图建议 | Loading Large Bitmaps Efficiently |
AOSP Bitmap.java |
Bitmap.java |
AOSP BitmapFactory.java |
BitmapFactory.java |
| Luban 开源项目 | Curzibn/Luban |
| Glide 资源复用文档 | Resource Reuse in Glide |
| Glide 配置文档 | Configuration in Glide |