Android图形系统Graphics来源、内存占用量统计、为什么很大,如何优化
一、为什么 dump meminfo 里 Graphics 会很大?
在 Android 中,dumpsys meminfo 里的 Graphics,通常不是指普通 Java 对象内存,而是:
- 图形相关的原生内存
- GPU / 图形缓冲区相关内存
- Surface、Texture、Bitmap 上传后的图形资源
- Hardware Bitmap / GraphicBuffer / RenderNode / EGL / GL 资源等
也就是说,Graphics 大,往往说明:
- 界面上显示了很多图片
- 图片被解码后又上传到了 GPU
- 存在很多 Surface / BufferQueue / 离屏缓冲
- 使用了硬件加速,导致图像资源以图形缓冲的形式存在
- 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 放内存里"这么简单。
一个图片从磁盘到屏幕,往往要经历:
- 文件在磁盘上
- jpg/png/webp/heif 等压缩格式
- 解码成像素数据
- 例如 ARGB_8888,每像素 4 字节
- Bitmap 存在内存中
- 可能在 Java 管理的对象里引用,但像素数据可能在 native
- 绘制时上传到 GPU / 图形缓冲
- 用于硬件加速渲染
- 最终进入 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 对象",而是因为:
- Coil 把图片解码成 Bitmap / Drawable
- 这些图片被 ImageView 显示
- Android 硬件加速渲染时,会把内容转成图形资源
- 窗口自身还有 Surface buffer
- 列表滚动 + 预取 + 内存缓存 + 复用池,会让短时间内活跃图片数量变多
- 某些情况下会使用硬件位图(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:必须做
- 明确设置缩略图请求尺寸
- 列表页不要加载原图
- 减少或去掉 bitmap transformation
- 列表页关闭 crossfade
P1:强烈建议实验
- 评估 allowHardware(false) 是否让 Graphics 更可控
- 减少预加载数量
- 简化 placeholder
- 控制 Memory Cache 大小
P2:进一步打磨
- 统一 item 尺寸
- 减少圆角/阴影/透明/复杂动画
- 页面退出及时解除引用
- 针对快速滚动做降级加载策略
二十三、一个最终的核心理解
可以把 Coil 在列表页中的作用理解成:
Coil 决定"解码出什么样的图、保留多少图、以什么配置输出图";
Android 图形系统决定"这些图为了显示要占用多少 Graphics"。
所以要优化 Graphics,关键就是控制这三件事:
- 每张图解码得多大
- 同时活跃多少张图
- 这些图是否走更重的图形路径(hardware bitmap、transformation、过渡动画、复杂 UI 效果)
二十四、一个简短版建议
如果图片列表页 Graphics 很大,先直接这样试:
imageView.load(uri) {
size(itemWidthPx, itemHeightPx)
crossfade(false)
allowHardware(false) // 做AB实验确认
}
并且:
- 列表页只用缩略图
- 去掉每张图的 transformation
- 减少预加载
- 简化 placeholder
通常这几步就能显著改善 Graphics。