Android 图片内存到底怎么算

面试里说自己做过 Android 内存优化,聊到图片压缩时很多人会顺手说"用了微信的压缩算法"。真正的追问往往马上就来了:你压缩的是文件大小,还是 Bitmap 内存?一张图片加载到内存后到底占多少?

一句话结论: 图片文件大小不等于 Bitmap 内存大小;面试回答要先区分"磁盘编码后的字节数"和"解码后的像素缓冲区",再用 宽 × 高 × 每像素字节数getByteCount()getAllocationByteCount() 和采样解码把工程优化讲完整。

本文按 Android 常见图片优化面试场景整理。Android Framework 的底层内存归属会随版本调整,文中的源码类名以官方 API、AOSP Bitmap / BitmapFactory 以及 Luban、Glide 的公开实现职责为锚点;具体字段和分支不要当成所有版本都完全一致。

1. 先把面试官的问题翻译成人话

面试官听到"我做过图片压缩"后,真正想确认的是三件事:

  1. 你是否知道图片上传压缩和图片内存优化不是同一件事。
  2. 你是否知道 Bitmap 的内存由解码后的像素决定,而不是原始文件体积决定。
  3. 你是否能把鲁班、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 质量参数重新编码
输出更适合上传的文件

这条链路里有两个点特别容易讲错:

  1. Encode["JPEG 质量编码"] 主要改变文件大小。只改 quality,不改宽高,重新解码后的 Bitmap 内存通常还是接近原来的大小。
  2. 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. 小结

图片优化最容易答偏,是因为"压缩"这个词同时被用于上传、存储、解码和显示。面试里不要急着报库名,先把链路拆开:

  1. 文件大小由编码格式、质量和尺寸共同决定。
  2. Bitmap 内存主要由解码后的宽、高和每像素字节数决定。
  3. getByteCount() 看当前内容,getAllocationByteCount() 看底层分配容量。
  4. inSampleSize、目标尺寸和 Glide downsample 才是展示侧降内存的主入口。
  5. Luban 类策略更偏上传压缩;只有尺寸变小,才会让后续 Bitmap 内存下降。

把这几层说清楚,面试官继续追问公式、源码 API、鲁班边界和 Glide 优化时,就不会被"压缩过了为什么还 OOM"这个问题带偏。

13. 面试计算题:多大分辨率的图片占多少内存

面试官很可能不会停在"公式是什么",而是现场给你一个分辨率,让你直接算。做题时按这个模板来:

text 复制代码
Bitmap 内存 = 宽 x 高 x 每像素字节数
ARGB_8888 = 4 字节每像素
RGB_565 = 2 字节每像素
MiB = bytes / 1024 / 1024

题目 1:1080 x 1920ARGB_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 3000ARGB_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 3200ARGB_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
相关推荐
Godikov2 小时前
弱网环境离线优先:Android 终端数据可靠同步架构实战
android
爱编程的小新☆5 小时前
Fake GPS 虚拟定位保姆级使用教学
android·移动开发·gps·虚拟定位
执明wa6 小时前
Android RecyclerView 多类型, 多种 Item
android·xml·开发语言·设计模式·android studio
silianpan6 小时前
CAD 文档预览 UTS 插件
android·ios·harmonyos
小麦在野6 小时前
独立 App 开发系列:用 GitHub Pages 托管隐私政策和用户协议
android·github
JMchen6 小时前
Android自定义View三大核心流程:measure、layout、draw完全解密
android·android studio
silianpan6 小时前
Office 文档预览 UTS 插件
android·微信小程序·harmonyos
明君6897 小时前
一套专注 Android 逆向分析的 Skills
android·android逆向·agents·skills