背景
谷歌这位哥哥真的会整幺蛾子,好不容易把crash跟anr数据压下来一点,最近发现又给了我一条技术质量类型的整改建议,大致内容是
真的是一点不让我闲着,天天关心着我的kpi有没有达标,天天给我整技改,但话又说回来,这是一条位图的优化的建议,这个是说的我项目里面需要使用图片加载库,让他们自动处理降采样,缓存和内存管理,难道我项目里面的Glide没有做这些事情?还是说我应该去配置点啥?有点懵。。。看来有必要先去了解下如何才能真正优化项目中的位图
什么问题会导致Google Play Console 给出这种整改建议
首先要知道这条建议怎么来的,Google Play Console 的“位图优化”检测项会扫描 APK/AAB 中的位图资源,看看是否存在以下情况
- 大尺寸位图是否被直接打包进安装包:图片的实际分辨率远超显示所需,造成安装包体积膨胀和运行时内存浪费
- 是否未使用WebP等高效格式:仍在使用PNG/JPEG等未压缩格式
- 图片未做屏幕密度适配:缺少对不同dpi目录(mdpi / hdpi / xhdpi / xxhdpi / xxxhdpi)的区分
- 运行时未下采样:图片加载到内存时未根据实际显示尺寸做采样,导致内存浪费
如果你的项目存在以上情况,那么你的应用有可能也会在谷歌后台收到跟我一样的整改建议
图片加载库
这是最基本的一点,不要手动管理位图加载,使用成熟的图片加载库,它们会自动处理缓存、下采样、复用和回收,现在Android项目主要的图片加载库就是两个,一个是Glide,另一个是Coil
- Coil的关键配置:
ImageLoader、bitmapConfig、size - Glide的关键配置:
DecodeFormat、DownsampleStrategy、override()
Coil配置(Compose项目为例)
全局ImageLoader配置
Compose中使用 AsyncImage
列表中的SubcomposeAsyncImage(异步加载避免卡顿)
自定义ImageLoader拦截器(服务器端裁剪配合)
在 URL 后自动追加目标尺寸参数
在 ImageLoader中注册
Glide配置(传统Android项目为例)
全局RequestOptions(限制图片质量与下采样)
限制内存缓存与BitmapPool
列表页面的Glide优化
列表Item中明确指定目标尺寸,让Glide自动下采样
使用Glide的RequestManager生命周期管理
Painter参数陷阱:不要传递 Painter给Composable
为啥说不能传递Painter呢?因为Painter 接口没有 @Stable 注解,Compose 编译器无法稳定推断其是否变化,容易导致不必要的重组,其根本原因是Bitmap 的 equals() 计算成本很高,因此 Compose 编译器不会将 Painter 标记为稳定类型
避免Painter触发不必要的重组
传递资源id或URL
在ViewModel中的处理
避免在ViewModel中持有Bitmap或Painter
如果必须持有位图,使用ImageBitmap并注意生命周期
图片下采样(Downsampling)
加载图片的时候,要记住始终加载恰好满足显示需求的图片尺寸,避免将超大分辨率图片塞进小容器
使用图片库自动下采样
Coil和Glide在确定目标尺寸后会自动下采样
Coil的尺寸解析优先级
ImageRequest.Builder.size(width, height): 显式指定AsyncImage的Modifier.size(): 从布局约束推断- 无约束时 : 加载原始尺寸,这种行为尽量避免
Glide的尺寸解析
使用 override() 显式指定
手动下采样(BitmapFactory)
如果项目中必须手动处理位图,那么可以使用 inSampleSize 进行安全的下采样
PS:inSampleSize 必须是 2 的幂次(1, 2, 4, 8, 16...),系统会向下取整到最近的 2 的幂
服务器端裁剪优先
最佳实践:请求精确尺寸的图片
从后端 API 请求图片时,尽量带上目标宽高参数,让服务器返回裁剪后的图片,这样做的好处是
- 减少网络传输量(下载更快、流量更省)
- 减少磁盘缓存占用空间
- 减少设备端裁剪的内存开销
- 提升列表滚动流畅度
实现方式 如果使用的是Coil,那么可以用上面讲到的拦截器去做,如果用的是Glide,可以使用如下方式
后端配合确保CDN或图片服务支持以下参数
| 参数 | 说明 | 示例 |
|---|---|---|
w / width | 目标宽度 | ?w=200 |
h / height | 目标高度 | ?h=200 |
m / mode | 裁剪模式(fill / fit / crop) | &m=crop |
q / quality | 压缩质量 | &q=80 |
避免无约束的布局尺寸
不要在加载远程图片的 Composable 上使用 wrapContentSize 或无约束尺寸,这样做会导致
问题
当图片库无法推断目标边界时,会回退加载完整原始图片,然后就
- 内存占用远大于实际需要
- 加载延迟增加
- 列表滚动卡顿
原理
正确做法
设置明确尺寸
定义宽高比,让布局引擎计算出确切像素目标
列表中使用固定尺寸
避免无约束,导致加载原始尺寸
选择正确的像素格式
不同像素格式的内存占用差异显著:
| 格式 | 每像素位数 | 支持透明度 | 适用场景 | 相比 ARGB_8888 省多少 |
|---|---|---|---|---|
ARGB_8888 | 32 bit | 支持 | 需要透明通道(默认) | / |
RGB_565 | 16 bit | 不支持 | 不需要透明度的图片 | 节省一半内存 |
ARGB_4444 | 16 bit | 支持 | 不推荐(画质差) | 节省一半(不推荐) |
ALPHA_8 | 8 bit | 仅透明度 | 遮罩/蒙版 | 节省 75% |
不是特殊场景的情况下,应用中 80% 以上的图片RGB_565就够用了,这样可以立即将内存占用减半
配置方式
Coil
Glide
判断是否需要透明度
项目当中可以根据不同场景来判断图片是否需要透明度,比如以下四种场景就不需要
但是如果遇到如下场景,那么就要让图片支持透明度
GPU纹理上传优化
调用 ImageBitmap.prepareToDraw() 可在实际绘制前提前将纹理上传到 GPU,减少首帧渲染延迟
大多数图片加载库已内置此优化,只有在没有用到图片加载库,需要手动调用的时候,才要做如上处理
硬件位图(Hardware Bitmap)
Android 8.0(API 26)支持硬件位图,位图数据只存在于 GPU 内存中,减少 CPU 内存占用,如果使用Coil加载库,那么会自动使用硬件位图,如果使用Glide,Glide默认启用硬件位图,如果想要关闭,可使用以下方式
矢量图优先于位图
对于几何图形、图标等场景,始终优先使用矢量图( ShapeDrawable、VectorDrawable),这样做的好处是
- 矢量文件一个体积也就几百字节,远小于位图
- 可适配任意屏幕密度
- 不随分辨率增加而增大内存占用
- 支持主题色(
tint)
既然这样我们还要位图干什么呢?或者什么时候用位图比较合适,什么时候用矢量图比较合适,列了几个场景
- 图标,logo:适合用矢量图,因为图形简单,文件小,可缩放
- 照片、截图:适合用位图,因为复杂的色彩渐变,是矢量图无法做到的
- 动图图标:适合用矢量图,因为体积小
- 商品图:适合用位图,因为需要真实色彩还原
内存管理与释放
使用图片加载库时
图片库会管理 Bitmap池,在不再需要时将Bitmap释放回池中复用,保留内存缓冲区。不需要手动回收
手动管理时
给图片添加内边距的正确方式
在给图片添加内边距时,有人习惯使用letterboxing,直接修改图片尺寸添加透明边框,但是这样会改变图片尺寸,增大内存占用,正确的做法是保持图片尺寸不变,使用 InsetDrawable 或在父容器上添加 padding
避免大图打包进 APK/AAB
有的项目里面,一张静态资源图片可能会有几百k甚至几兆,直接加载到内存的话,内存一定会飙升,所以应当避免在项目中引入大图,如果一定要使用大图的话,可以先做下以下几件事情
- 转换为 WebP 格式(点右键 → Convert to WebP)
- 压缩图片,比如tinyPng
- 托管在服务器上按需下载
多密度适配
尽量将不同分辨率的图片放入对应目录,避免在低密度设备上加载高分辨率图片
性能监控与分析方法
adb 命令监控
Android Studio Memory Profiler
- 打开 Profiler → Memory 面板
- 录制场景:进入页面 → 加载图片 → 滚动 → 离开页面
- 关注以下指标:
- Java Heap:Bitmap 对象占用的堆内存
- Native Heap:native 层的 Bitmap 像素数据
- Graphics:GPU 纹理内存
- 检查内存是否在离开页面后回落(内存泄漏检测)
Coil内存监控
最后
这篇文章主要就是讲的在项目当中如何正确处理位图,防止被谷歌警告,但是有时候你会发现你的项目当中其实方方面面都把位图处理的很好了,但还是会收到谷歌的整改建议,那么这个就有可能你用的某一个sdk里面存在着这方面的问题,所以定期去查看用到的三方库官网的更新,有可能人家在新版本里面已经把问题解决了,你还在用老版本使劲分析原因,那真的是浪费时间精力了