Android图形系统Graphics来源、内存占用量统计、为什么很大,如何优化

Android图形系统Graphics来源、内存占用量统计、为什么很大,如何优化

一、为什么 dump meminfo 里 Graphics 会很大?

在 Android 中,dumpsys meminfo 里的 Graphics,通常不是指普通 Java 对象内存,而是:

  • 图形相关的原生内存
  • GPU / 图形缓冲区相关内存
  • Surface、Texture、Bitmap 上传后的图形资源
  • Hardware Bitmap / GraphicBuffer / RenderNode / EGL / GL 资源等

也就是说,Graphics 大,往往说明:

  1. 界面上显示了很多图片
  2. 图片被解码后又上传到了 GPU
  3. 存在很多 Surface / BufferQueue / 离屏缓冲
  4. 使用了硬件加速,导致图像资源以图形缓冲的形式存在
  5. Bitmap 虽然 Java Heap 看起来不大,但底层图形内存很大

二、dump meminfo 里的 Graphics 到底统计的是什么?

先说结论:dumpsys meminfo 里的各项不是完全按"Java对象种类"统计,而是按 内核/系统对进程内存的归类 来统计的。

常见几项会看到:

  • Java Heap:Java/Kotlin 对象堆内存
  • Native Heap:malloc/new 等 native 分配
  • Graphics:图形系统相关内存
  • Stack
  • Code
  • Private Other
  • System
  • TOTAL / PSS / RSS

其中 Graphics 主要会包含与图形相关的内存映射和分配,常见来源有:

  • gralloc 分配的图形 buffer
  • ashmem / dmabuf 中用于图形的共享内存
  • Surface buffer
  • GPU texture backing store
  • Hardware renderer 使用的缓存
  • Skia / HWUI / OpenGL / Vulkan 相关图形资源

在新一些 Android 版本上,Graphics 很多统计底层会跟 dmabuf / ION / gralloc 之类的图形缓冲机制相关。

三、为什么图片应用特别容易让 Graphics 很大?

因为图片显示不只是"加载成 Bitmap 放内存里"这么简单。

一个图片从磁盘到屏幕,往往要经历:

  1. 文件在磁盘上
    • jpg/png/webp/heif 等压缩格式
  2. 解码成像素数据
    • 例如 ARGB_8888,每像素 4 字节
  3. Bitmap 存在内存中
    • 可能在 Java 管理的对象里引用,但像素数据可能在 native
  4. 绘制时上传到 GPU / 图形缓冲
    • 用于硬件加速渲染
  5. 最终进入 Surface Buffer
    • 交给 SurfaceFlinger 合成显示

所以一张图可能同时涉及:

  • 磁盘占用
  • Java 对象占用
  • Native 内存占用
  • Graphics 内存占用

这就是你看到 Graphics 很大 的根本原因:
屏幕显示图片时,不仅有 Bitmap 解码内存,还有图形渲染链路上的额外缓存和 buffer。

四、Android 系统是怎么统计 Graphics 的?

严格说,Android 并不是"按一张图一张图去算"的,而是根据进程相关的 图形缓冲/映射页 来归类统计。

1)PSS / RSS 视角

dump meminfo 常见的是按 PSS(Proportional Set Size)统计:

  • Private Dirty / Private Clean
  • PSS
  • RSS

其中图形内存如果是共享缓冲(例如 GraphicBuffer),可能会按共享比例分摊到多个进程。

比如一个 Surface buffer 可能涉及:

  • App 进程
  • SurfaceFlinger
  • GPU 驱动

那么 meminfo 里未必全部算到 app 独占,有时是按共享页折算。

2)Graphics 的统计来源

不同 Android 版本实现不完全一致,但大方向是:

  • 从 /proc/pid/smaps、smaps_rollup
  • 图形驱动导出的内存信息
  • dmabuf / ION / gralloc 分配记录
  • Debug MemoryInfo 中的 graphics 字段

系统会把一些已知图形相关区域归类到 Graphics。所以它不是代码里"new 了多少 Bitmap"这么直观,而是系统看到这些页面属于图形用途,就记到 Graphics。

五、以相册/图片列表应用为例:多宫格小图展示时,Graphics 内存怎么产生?

假设有一个 RecyclerView,屏幕上显示多行多列的小图,比如 3 列 x 6 行,共 18 张缩略图。

1)磁盘上的原始图片

相册里的原图可能很大,比如:

  • 4000 x 3000 JPEG

磁盘文件可能只有 2MB~8MB,

但这只是压缩文件大小,和内存占用不是一回事

2)解码成缩略图 Bitmap

如果为了小宫格,只解码成 300 x 300:

ARGB_8888

每像素 4 字节:

300 × 300 × 4 = 360000 字节 ≈ 351 KB

18 张大概:

351 KB × 18 ≈ 6.2 MB

这只是解码后的像素内存

如果解码策略不好,直接按原图尺寸解码,比如 4000x3000:

4000 × 3000 × 4 = 48,000,000 字节 ≈ 45.8 MB/张

18 张就非常夸张了。

所以图片列表最怕的就是 过大解码

3)绘制时进入 GPU/图形系统

如果启用了硬件加速(Android UI 默认就是),Bitmap 在显示时通常会参与 GPU 渲染:

  • 可能被上传为 texture
  • 可能进入 RenderThread / HWUI 的缓存
  • 可能占用 GraphicBuffer 或其他图形资源

这部分就可能体现在 Graphics 里。

也就是说,同一张图片可能有:

  • 一份解码像素内存
  • 一份 GPU 纹理/图形缓存
  • 再加上窗口自身的前后台缓冲

4)窗口本身也有 buffer

Activity 对应一个 Window / Surface,通常至少会有双缓冲甚至三缓冲。

比如一个全屏 1080 x 2400 界面:

1080 × 2400 × 4 ≈ 9.9 MB

如果三缓冲:

≈ 9.9 MB × 3 = 29.7 MB

这还没算图片本身,只是窗口缓冲

因此即使界面只是显示小图,Graphics 也可能先天就有十几 MB 到几十 MB。

六、多宫格列表中,Graphics 具体由哪些部分组成?

可以拆成几类:

1)窗口 Surface Buffer

整个页面渲染输出到 Surface 的 buffer:

  • front buffer
  • back buffer
  • 可能还有 pending buffer

这个占用和屏幕分辨率强相关。

2)图片纹理 / 硬件位图

小图显示出来时,系统可能把它们保留成图形资源:

  • GPU texture
  • HardwareBitmap
  • Render cache

如果列表里快速滚动,为了流畅,缓存可能比屏幕上正在显示的数量更多。

3)离屏渲染缓存

如果 item 上有这些效果:

  • 圆角裁剪
  • alpha
  • 阴影
  • transform
  • clipPath
  • 某些复杂动画

可能触发额外离屏 buffer / layer。

这类也会增加 Graphics。

4)图片库自身的缓存策略

比如 Glide / Coil:

  • Bitmap Pool
  • Memory Cache
  • Hardware Bitmap
  • 预加载(preload)
  • 过渡动画产生的额外图形资源

尤其 Glide 在某些场景下会使用 HARDWARE Bitmap(Android O+),这会让内存更偏向 Graphics/Native,而不是 Java Heap。

七、Graphics 内存怎么计算?

这要分层看。

1)单张 Bitmap 的理论内存

最基础公式:

内存大小 = 宽 × 高 × 每像素字节数

常见格式:

  • ARGB_8888 = 4 字节/像素
  • RGB_565 = 2 字节/像素
  • ALPHA_8 = 1 字节/像素
  • RGBA_F16 = 8 字节/像素

例子:

  • 200 x 200 小图,ARGB_8888
    200 × 200 × 4 = 160,000 ≈ 156 KB
  • 300 x 300 小图
    300 × 300 × 4 ≈ 351 KB

2)窗口缓冲的理论内存

公式一样:

SurfaceBuffer = 屏幕宽 × 屏幕高 × 4 × buffer个数

例如:

  • 1080 × 2400 × 4 ≈ 9.9 MB
  • 三缓冲约 29.7 MB

所以哪怕 app 页面只有一些小图,Graphics 也不会很低。

3)图形缓存不是简单 1:1

实际 Graphics 比"Bitmap 像素总和"更复杂,因为还有:

  • 行对齐(stride)
  • buffer 对齐到 page
  • mipmap / GPU 对齐
  • 缓存复本
  • 共享页按 PSS 折算

所以不能简单说:

18 张图 × 351KB = Graphics 全部大小

实际可能:

  • 解码 bitmap:6.2MB
  • 纹理缓存:额外几 MB 到十几 MB
  • 窗口 buffer:20~30MB
  • 过渡/离屏层:若干 MB

最终 dump meminfo 里 Graphics 看到 30MB、50MB、80MB 都不奇怪。

八、为什么小图很多时,Graphics 依然可能很高?

因为"看起来小"不等于"占用小"。

几个常见原因:

1)按原图解码后再缩放显示

这是最常见问题。

UI 上显示 100x100,但内存里可能还是 4000x3000。

显示大小 ≠ 解码大小

2)缓存了屏幕外图片

RecyclerView 为流畅滚动,通常会保留:

  • 当前可见
  • 即将可见
  • 复用池中的一些 item
  • 图片库的 memory cache / bitmap pool

所以实际在内存里的图片数量 > 屏幕显示数量。

3)硬件位图进入 Graphics

Android O+ 上如果使用 Bitmap.Config.HARDWARE,像素数据更偏底层图形内存,不一定明显反映到 Java Heap。

会觉得:

  • Java Heap 不大
  • 但 Graphics 很大

这很正常。

4)窗口缓冲本来就大

特别是高分屏手机:

  • 1440p / 2K 屏
  • 高刷新率下合成频繁
  • 多 buffer

都会使 Graphics 基线变高。

九、以多宫格相册为例,做一个粗略计算

假设:

  • 屏幕:1080 × 2400
  • 宫格:3 列
  • 每个 item 图片显示区域:大约 320 × 320
  • 屏幕可见 15 张
  • 预加载 15 张
  • 图片格式:ARGB_8888

1)缩略图解码内存

单张:

320 × 320 × 4 = 409,600 ≈ 400 KB

30 张:

400 KB × 30 ≈ 12 MB

2)窗口三缓冲

1080 × 2400 × 4 × 3 ≈ 29.7 MB

3)其他图形缓存

比如 HWUI / 纹理缓存 / 圆角 / 动画 / 共享缓冲

保守估 5~15 MB

4)总量

那你看到 Graphics 可能在:

  • 30MB+
  • 40MB+
  • 50MB+

这是完全可能的。

如果错误地解码得更大,比如每张按 800x800 解码:

800 × 800 × 4 ≈ 2.44 MB/张

30 张就是:

≈ 73 MB

这时候 Graphics / Native / Total PSS 就会非常高。

十、Graphics 与 Bitmap 内存、Native Heap、Java Heap 的关系

很多人容易混淆这几个。

1)Java Heap

主要是 Java 对象壳子,比如:

  • Bitmap 对象本身
  • ImageView 对象
  • Adapter 数据结构

Bitmap 像素数据本体 不一定都在 Java Heap。

2)Native Heap

Bitmap 像素、Skia 解码缓存、图片库 native 分配等,很多会在 native。

3)Graphics

如果这些像素进一步成为图形资源、硬件位图、窗口缓冲、纹理等,会体现在 Graphics。

所以同样是"图片导致的内存",可能分散在:

  • Java Heap
  • Native Heap
  • Graphics
  • Private Other

这取决于 Android 版本、Bitmap 配置、图形栈实现。

十一、如何验证某个页面 Graphics 为什么高?

可以这样排查。

1)看 dumpsys meminfo

Bash

adb shell dumpsys meminfo your.package.name

重点看:

  • Java Heap
  • Native Heap
  • Graphics
  • TOTAL PSS

如果 Graphics 特别高,说明更偏图形资源/缓冲。

2)看 gfxinfo

Bash

adb shell dumpsys gfxinfo your.package.name

可以辅助看渲染、layer、缓存情况。

3)Android Studio Profiler

看:

  • Native 内存走势
  • Java 内存走势
  • 图片页面进入前后差值

4)看图片加载库配置

比如 Glide:

  • 是否启用硬件位图
  • override 是否合理
  • 是否使用 centerCrop / fitCenter
  • memory cache / bitmap pool 大小
  • preload 数量

5)排查是否过大解码

关键检查:

  • 是否根据 ImageView 目标尺寸 decode
  • 是否用了 inSampleSize
  • 是否直接加载原图到缩略图控件

这是第一大元凶。

十二、在多宫格相册里,Graphics 内存优化思路

1)按目标尺寸解码

最重要。

不要把原图解码后再缩小显示。

应该直接按宫格尺寸生成缩略图。

例如控件显示 200dp,就按接近这个像素尺寸解码。

2)使用缩略图而不是原图

相册类 app 最好区分:

  • thumbnail
  • preview
  • original

列表页只用 thumbnail。

3)控制预加载数量

预加载太积极会让 Graphics 和 Native 都涨得很快。

4)合理使用图片库参数

例如 Glide 可考虑:

  • .override(width, height)
  • .thumbnail()
  • 根据场景决定是否允许 hardware bitmap
  • 限制缓存大小

5)减少复杂 item 效果

如果每个小图 item 都有:

  • 圆角
  • 阴影
  • 半透明蒙层
  • 选中动画
  • 缩放动画

会增加离屏渲染和图形缓存。

6)及时释放页面资源

页面退出时:

  • 停止预加载
  • 清理 adapter 持有引用
  • 避免静态缓存持有 bitmap
  • 让 RecyclerView 脱离窗口后可回收

十三、一个容易记住的理解方式

可以把 Graphics 理解成:

"为了把内容显示到屏幕上,Android 图形系统额外持有的那部分内存"

对于相册/图片墙场景:

  • 图片越多
  • 解码越大
  • 硬件加速越多
  • 屏幕分辨率越高
  • 图形缓存越激进

那么 dump meminfo 里的 Graphics 就越容易变大。

十四、三个核心问题

1)为什么 Graphics 很大?

因为 Android 图形栈会为图片显示、窗口缓冲、GPU纹理、硬件位图、离屏渲染等分配大量图形相关内存,这些会计入 Graphics,而不仅仅是 Java Heap。

2)Android 怎么统计 Graphics?

系统依据进程关联的图形缓冲、gralloc/dmabuf/共享图形页、Surface/纹理等底层内存映射做归类统计,通常以 PSS/RSS 等方式反映在 dumpsys meminfo 中。

3)多宫格图片展示里 Graphics 怎么产生、怎么算?

主要来自:

  • 页面窗口的 Surface buffer
  • 缩略图解码后的像素数据参与图形渲染
  • 上传成 GPU 纹理/硬件位图
  • 列表滚动中的缓存与预加载
  • 可能的离屏渲染层

粗略计算方式:

  • 单图像素内存:宽 × 高 × 4
  • 窗口缓冲:屏幕宽 × 屏幕高 × 4 × buffer数
  • 再加缓存、共享、对齐等额外开销

十五、Coil整条图像显示链路

在图片列表页中,Graphics 上涨,通常不是因为 "Coil 缓存了一堆 Java 对象",而是因为:

  1. Coil 把图片解码成 Bitmap / Drawable
  2. 这些图片被 ImageView 显示
  3. Android 硬件加速渲染时,会把内容转成图形资源
  4. 窗口自身还有 Surface buffer
  5. 列表滚动 + 预取 + 内存缓存 + 复用池,会让短时间内活跃图片数量变多
  6. 某些情况下会使用硬件位图(Hardware Bitmap)
    • 这类像素不在 Java Heap,往往更容易体现在 Native/Graphics 相关统计里

所以看到的是:

  • Java Heap 没怎么暴涨
  • 但 dump meminfo 里 Graphics 在涨

这在 Coil + RecyclerView + ImageView 场景里非常常见。

十六、先建立一个 Coil 图片加载到屏幕的完整链路

以列表页一个宫格小图为例,Coil 会走这条链路:

1)发起请求

例如:

imageView.load(uri) {

size(200, 200)

crossfade(true)

}

Coil 创建 ImageRequest。

2)命中缓存 or 拉取数据

Coil 会按顺序尝试:

  • Memory Cache
  • Disk Cache
  • 网络 / ContentResolver / File

如果是相册场景,通常数据源可能是:

  • content://media/...
  • 文件路径
  • 网络 URL

3)解码

Coil 会根据数据源把图片 decode 成:

  • Bitmap
  • 或某种 Drawable

Android 上最核心还是 Bitmap 链路。

这里会涉及:

  • 原图尺寸
  • EXIF 旋转
  • 采样缩放(inSampleSize)
  • 输出配置(ARGB_8888 / HARDWARE 等)

4)变换

如果你配置了:

  • circleCrop
  • roundedCorners
  • blur
  • grayscale

Coil 可能会生成新的输出 Bitmap。

这一步非常重要,因为: 每做一次 bitmap transformation,往往就会产生额外像素内存和中间结果。

5)设置到 ImageView

Coil 最后把结果设置到 ImageView。

如果是 BitmapDrawable 或其他可绘制对象,进入 View 树绘制流程。

6)Android 图形系统渲染

当 View 被绘制到屏幕:

  • UI 线程记录绘制命令
  • RenderThread / HWUI 参与渲染
  • 图片内容可能被上传/引用为 GPU 纹理或图形资源
  • Window 的 Surface buffer 接收最终输出
  • SurfaceFlinger 合成

这个阶段是 Graphics 内存上涨的关键来源。

十七、Coil 场景里,Graphics 为什么会上涨?

拆成几类。

1)Window Surface Buffer 是固定大头,和 Coil 无关但会叠加

只要页面显示出来,就有窗口缓冲。

例如 1080 × 2400 屏幕:

1080 × 2400 × 4 ≈ 9.9 MB

如果双缓冲 / 三缓冲:

约 20MB ~ 30MB

所以页面一打开,Graphics 本来就不低。

如果列表页有很多图片,Coil 只是让这个页面"更丰富",额外图形资源会叠加上去。

2)ImageView 显示 Bitmap 后,图片内容会参与硬件加速渲染

RecyclerView 列表里的每个 item 都有 ImageView。

Coil 解码好的 Bitmap 一旦真正绘制到屏幕:

  • 可能被 HWUI 使用
  • 可能转成硬件纹理/缓存
  • 可能保留在图形缓存中以提高滚动流畅度

这部分会让 Graphics 上涨。

注意一个常见误区:

"我的 Bitmap 不是已经在内存里了吗?为什么 Graphics 还涨?"

因为:

  • Bitmap 像素内存 是一层
  • 用于显示的图形缓存/纹理资源 是另一层

可以理解为: Coil 负责把图准备好,Android 图形系统负责把它画出来。画出来就可能产生 Graphics。

3)Hardware Bitmap 会让占用更偏向 Graphics/Native,而不是 Java Heap

Coil 在 Android O+ 上,很多场景允许使用 Bitmap.Config.HARDWARE。

这类 Bitmap 的特点:

  • 不可变
  • 像素数据位于更底层的图形内存/硬件位图区域
  • 适合显示
  • 通常减少 Java Heap 压力
  • 但容易让你在 meminfo 里看到 Graphics/Native 更高

所以如果用 Coil 默认策略加载很多缩略图,可能会发现:

  • Java Heap 还行
  • 但是 Graphics 明显上涨

这并不一定是泄漏,而是硬件位图和图形渲染路径导致的统计表现。

4)列表滚动时,不只是"屏幕上这些图"在内存里

RecyclerView + Coil 的实际活跃图片集合,通常包括:

  • 当前屏幕可见 item
  • 即将进入屏幕的 item
  • 已加载完但暂未被淘汰的 Memory Cache
  • Bitmap Pool 中可复用的 bitmap
  • 过渡动画期间的旧图/新图
  • 复用 View 上暂时保留的 drawable

所以哪怕页面上只看到 12 张图,实际内存里活跃的图可能是:

  • 20 张
  • 30 张
  • 甚至更多

如果这些图都已经进入绘制链路,Graphics 就会跟着涨。

5)Crossfade、placeholder、error drawable 也可能增加短时 Graphics 占用

Coil 很常用:

imageView.load(url) {

crossfade(true)

placeholder(R.drawable.xxx)

}

这会造成短时间内:

  • placeholder 在画
  • 新图在画
  • crossfade 期间两层内容叠加
  • 某些复杂 item 可能触发额外 layer 或缓存

单张图问题不大,但列表里几十个 item 同时发生,就会放大 Graphics 波动。

6)Transformation 会制造额外 bitmap,进而提高图形占用

例如:

transformations(RoundedCornersTransformation(...))

很多人以为圆角只是"显示效果",但对于 Coil 来说,transformations 往往意味着:

  • 先 decode 成 bitmap
  • 再创建一张新 bitmap 做处理
  • 再输出给 ImageView

这样会产生:

  • 原始解码 bitmap
  • 变换后的 bitmap
  • 可能的中间缓冲
  • 最终显示时再进入图形渲染

所以在列表页里对每张小图都做 transformation,Graphics/Native 都很容易抬高。

7)如果请求尺寸不准确,会 decode 过大图,Graphics 迅速变大

这是最关键的一点。

假设小宫格实际显示仅 120dp × 120dp,

但没有给 Coil 明确 size,或者请求发生时 View 尺寸尚未确定,Coil 可能:

  • 按较大尺寸解码
  • 甚至接近原图尺寸解码
  • 再由 ImageView 缩放显示

显示上看不出来问题,但内存里已经很大了。

例如:

  • 实际显示:300 × 300
  • 实际解码:1200 × 1200

那么单张图内存:

1200 × 1200 × 4 ≈ 5.5 MB

而不是本该的:

300 × 300 × 4 ≈ 351 KB

10 张就差了几十 MB。

而这些大 bitmap 一旦显示,Graphics 也会更高。

十八、Coil 中哪些实现细节会影响 Graphics?

1)allowHardware(true/false)

这是非常关键的开关。

allowHardware(true)(默认很多场景允许)

优点:

  • 更适合显示
  • 可能减少 Java Heap 压力
  • 绘制效率通常更好

缺点:

  • 占用更偏向底层图形资源
  • meminfo 中 Graphics/Native 可能更高
  • 某些场景不方便二次处理

allowHardware(false)

优点:

  • 使用软件 bitmap
  • 统计上更多落在 Native/Bitmap 侧,而非硬件位图路径
  • 某些变换/像素处理更灵活

缺点:

  • 显示链路上未必最优
  • 可能增加 CPU 或 native bitmap 压力

注意:
它不一定让"总内存"更低,只是会改变内存分布。

但在某些列表场景,为了抑制 Graphics,禁用 hardware bitmap 确实可能有帮助。

2)size(...)

Coil 是否能按目标尺寸解码,极其重要。

例如:

imageView.load(uri) {

size(200, 200)

}

或者让 Coil 等待真实 view size:

imageView.load(uri) {

precision(Precision.INEXACT)

}

如果尺寸拿不准,Coil 可能解码得偏大。

Graphics 优化里最重要的第一条就是:请求尺寸必须接近显示尺寸。

3)precision(EXACT / INEXACT)

  • EXACT:更严格匹配目标尺寸
  • INEXACT:允许近似,更利于效率和复用

在列表缩略图场景:

  • 一般没必要追求特别精确
  • 用近似尺寸往往更省资源

4)scale(FIT / FILL)

不同缩放策略会影响实际解码目标。

如果你用 FILL 或者裁剪型展示,而请求尺寸又不精确,可能会导致 decode 较大。

5)Memory Cache 与 Bitmap Pool

Coil 会保留内存缓存来提高滚动体验。

这带来的效果是:

  • 滚动回来秒开
  • 少重复解码
  • 但内存短期更高

虽然 Memory Cache 本身不等于 Graphics,

但缓存中的 bitmap 更容易再次被快速显示,持续占据图形路径相关资源。

五、列表页里 Graphics 上涨的典型链路,用一个具体例子讲

假设:

  • RecyclerView 3 列宫格
  • 屏幕一次可见 18 张图
  • 预取 12 张
  • 每张显示区域约 240 × 240 px
  • 没给 Coil 明确 size
  • 原图是 3000 × 2000

可能发生的是:

情况 A:优化得不好

Coil 由于尺寸信息不够准确,按 1000 × 1000 甚至更大解码。

单张:

1000 × 1000 × 4 = 4 MB

30 张活跃图:

约 120 MB

再叠加:

  • 窗口 Surface buffer 20~30 MB
  • 图形缓存若干 MB
  • transition / placeholder / transform 额外开销

这时 Graphics / Native / Total PSS 都会很难看。

情况 B:优化后

明确请求缩略图尺寸为 240 × 240。

单张:

240 × 240 × 4 ≈ 225 KB

30 张:

约 6.6 MB

再加窗口缓冲:

  • 20~30 MB

最终整体就合理很多。

这说明: 列表页 Graphics 高不高,根本上取决于"实际解码尺寸 × 活跃图片数量 × 图形渲染缓存"

十九、怎么优化 Coil 列表页里的 Graphics 占用?

下面按"最有效优先"的顺序说。

1)最重要:确保按显示尺寸加载,不要加载原图

这是第一优先级,没有之一。

推荐做法

给每个列表缩略图明确尺寸:

imageView.load(uri) {

size(itemWidthPx, itemHeightPx)

}

如果是固定宫格,提前算好尺寸。

例如 3 列布局:

val itemSize = (screenWidth - spacing * 4) / 3

然后:

imageView.load(uri) {

size(itemSize, itemSize)

}

为什么有效

因为会直接降低:

  • decode 像素内存
  • transformation 成本
  • 上传到图形系统的资源体积
  • cache 占用
  • Graphics 上涨幅度

2)缩略图列表优先禁用复杂 transformation

例如不要在列表里对每张图都做:

  • blur
  • circleCrop
  • 大圆角 bitmap transformation
  • 复杂自定义 transformation

更好的方式

能用 View 层实现就尽量用 View 层,比如:

  • 简单圆角:优先考虑 ShapeableImageView / outline / 裁剪方案
  • 不要对每张 bitmap 做像素级处理

因为 bitmap transformation 往往会新建 bitmap,增加内存和图形压力。

3)根据场景评估是否关闭硬件位图

如果明确观察到:

  • Java Heap 不大
  • Graphics 很高
  • 页面是大量缩略图列表
  • 并且更在意 meminfo 中 Graphics 峰值

可以尝试:

imageView.load(uri) {

allowHardware(false)

}

适用场景

  • 列表缩略图很多
  • 需要更稳定可控的内存表现
  • 有 transformation / palette / 像素读写需求
  • 某些机型硬件位图表现不理想

注意

这不是银弹。

它可能让 Graphics 下降一些,但 Native Heap 可能会上来。

所以要看目标:

  • 是控制 Graphics 指标
  • 还是控制总内存
  • 还是追求滑动流畅

通常需要 AB 对比验证。

4)减少同时活跃的请求数量和预加载数量

列表页很容易"加载得太积极"。

优化点:

  • 不要一次预加载太多屏外图片
  • 不要在快速滚动时还全速加载高清缩略图
  • 可以在 fling 时降低请求积极性

思路上类似:

  • 静止时加载高质量缩略图
  • 快速滚动时先低成本占位,停止后再补图

这样可以减少短时间内大量 bitmap 同时进入图形链路,抑制 Graphics 峰值。

5)控制 crossfade,列表缩略图不一定需要

crossfade(true) 很常见,但在密集列表里未必值得。

可以考虑:

  • 列表页关闭 crossfade
  • 详情页保留 crossfade

例如:

imageView.load(uri) {

crossfade(false)

}

原因:

  • 减少同一时刻两张内容叠加绘制
  • 减少过渡期额外图形负担
  • 滚动列表通常不需要精致淡入动画

6)占位图要小,不要用大图 placeholder

很多页面 placeholder 设计不当:

  • 一个 1080p 的大 png 作为所有缩略图占位图
  • 每个 item 都先画一遍大资源再缩放

这会明显增加图形和解码压力。

建议:

  • 用纯色 / shape / 很小的矢量或轻量资源做 placeholder
  • 列表缩略图占位图尽量简单

7)合理设置 Coil 的内存缓存大小

Coil 的 ImageLoader 可以配置 Memory Cache 和 Disk Cache。

如果缓存过大:

  • 回看列表很爽
  • 但峰值和常驻内存偏高

如果缓存过小:

  • 反复解码
  • CPU 和 I/O 压力上来

所以建议根据业务平衡,而不是一味调大。

例如可定制:

val imageLoader = ImageLoader.Builder(context)

.memoryCache {

MemoryCache.Builder(context)

.maxSizePercent(0.15)

.build()

}

.build()

如果页面是大量宫格图,且内存紧张,可以适当保守。

8)使用稳定尺寸,提升复用率

如果列表 item 尺寸不稳定,例如:

  • 瀑布流动态高度
  • 不同卡片尺寸频繁变化

那么缓存命中和 bitmap 复用都会变差,导致更多解码和更多活跃图形资源。

相反,如果宫格图尺寸统一:

  • 更利于缓存
  • 更利于 bitmap 复用
  • 更利于控制 Graphics 峰值

9)页面退出时避免"缓存住整页图"

如果 Adapter、Fragment、ViewModel、静态单例等不小心持有:

  • ImageView
  • Drawable
  • Bitmap 引用
  • request target

会导致页面退出后图像资源不能及时释放。

要确保:

  • View 销毁时请求可取消
  • 不持有 View 引用
  • Adapter 不残留大对象
  • 不做无边界自定义内存缓存

10)高密度列表页尽量避免过度 UI 效果

例如每个小图 item 都有:

  • 大圆角
  • 阴影
  • 半透明蒙层
  • 复杂选中动画
  • scale / alpha 动画

这些都会增加 GPU 负担和图形缓存压力。

对列表页建议:

  • 样式尽量扁平
  • 效果尽量轻量
  • 动画只在必要时启用

十九、Coil 在图片列表页中的推荐配置思路

下面给一个"缩略图列表页"的典型建议。

示例 1:基础缩略图加载

imageView.load(uri) {

size(itemSizePx, itemSizePx)

crossfade(false)

allowHardware(false) // 可按实验结果决定是否关闭

}

适合:

  • 宫格缩略图
  • 大量图片同时出现
  • 更关注内存稳定性

示例 2:如果想保留硬件位图

imageView.load(uri) {

size(itemSizePx, itemSizePx)

crossfade(false)

allowHardware(true)

}

适合:

  • 更看重显示性能
  • 实测 Graphics 可接受
  • 没有复杂变换

示例 3:避免列表页复杂变换

尽量少这样:

imageView.load(uri) {

size(itemSizePx, itemSizePx)

transformations(RoundedCornersTransformation(16f))

}

更建议由 View 外观承担圆角,而不是 bitmap transformation。

二十、如何判断 Coil 导致的是"正常 Graphics 增长"还是"异常问题"?

可以看这几个现象。

正常增长的特征

  • 打开图片列表页,Graphics 上升
  • 滚动时波动
  • 返回页面后逐渐回落
  • 多次进入退出后不会无限增长
  • Java Heap 不一定大,但 Graphics/Native 有波动

这通常是正常的图像显示开销。

可疑异常的特征

  • 页面退出后 Graphics 长时间不回落
  • 多次进入列表页,Graphics 只涨不降
  • 同一页面每次进入都叠加更多占用
  • 停止滚动后占用仍持续上涨
  • OOM 或系统频繁杀进程

这时要怀疑:

  • View/Drawable 引用未释放
  • 自定义缓存有泄漏
  • 请求尺寸过大
  • transformation 生成的大图未及时回收
  • 特定机型 GPU / hardware bitmap 行为异常

二十一、实际排查建议:怎么验证是 Coil 哪个点让 Graphics 上涨?

可以做对照实验。

实验 1:只改 size

对比:

版本 A

imageView.load(uri)

版本 B

imageView.load(uri) {

size(itemSizePx, itemSizePx)

}

如果 B 明显更好,说明主要问题是 过大解码

实验 2:只改 allowHardware

对比:

A

allowHardware(true)

B

allowHardware(false)

如果 B 下 Graphics 降了、但 Native 稍升,说明你之前有较多 hardware bitmap / 图形资源占用

实验 3:关闭 crossfade

如果关闭后 Graphics 波动变小,说明列表里过渡动画有放大作用。

实验 4:去掉 transformation

如果去掉后占用明显下降,说明 bitmap 变换成本很高。

二十二、一个比较实用的优化优先级清单

如果现在就要落地优化,建议按这个顺序:

P0:必须做

  1. 明确设置缩略图请求尺寸
  2. 列表页不要加载原图
  3. 减少或去掉 bitmap transformation
  4. 列表页关闭 crossfade

P1:强烈建议实验

  1. 评估 allowHardware(false) 是否让 Graphics 更可控
  2. 减少预加载数量
  3. 简化 placeholder
  4. 控制 Memory Cache 大小

P2:进一步打磨

  1. 统一 item 尺寸
  2. 减少圆角/阴影/透明/复杂动画
  3. 页面退出及时解除引用
  4. 针对快速滚动做降级加载策略

二十三、一个最终的核心理解

可以把 Coil 在列表页中的作用理解成:

Coil 决定"解码出什么样的图、保留多少图、以什么配置输出图";
Android 图形系统决定"这些图为了显示要占用多少 Graphics"。

所以要优化 Graphics,关键就是控制这三件事:

  1. 每张图解码得多大
  2. 同时活跃多少张图
  3. 这些图是否走更重的图形路径(hardware bitmap、transformation、过渡动画、复杂 UI 效果)

二十四、一个简短版建议

如果图片列表页 Graphics 很大,先直接这样试:

imageView.load(uri) {

size(itemWidthPx, itemHeightPx)

crossfade(false)

allowHardware(false) // 做AB实验确认

}

并且:

  • 列表页只用缩略图
  • 去掉每张图的 transformation
  • 减少预加载
  • 简化 placeholder

通常这几步就能显著改善 Graphics。

相关推荐
黄林晴1 小时前
Android Show I/O 2026:开发者该关注这几件事
android
Kapaseker1 小时前
最简单的 Compose 动画 — animateDpAsState
android·kotlin
问心无愧05131 小时前
ctf show web 入门46
android·前端·笔记
凛_Lin~~2 小时前
lifecycle源码解析 (版本2.5.1)
android·java·安卓·lifecycle
唐诺2 小时前
Android 与 iOS 核心差异
android·ios
UXbot2 小时前
Vibecoding 工具如何一次性生成 Web + iOS + Android 三端 APP?功能架构深度解读
android·前端·ui·ios·交互·软件构建·ai编程
鹏晨互联2 小时前
Jetpack Compose vs XML:fillMaxSize、fillMaxHeight、fillMaxWidth 全面对比
android·xml
Android小码家2 小时前
ptrace 内存追踪
android