12.04 数据解析-图片加载
图片是 UI 资源消耗的大户:一张 4000×3000 的照片解码成 Bitmap 要占 48MB 内存(4000×3000×4 字节),塞进一个 100dp 的头像位就是灾难。所以 Android 端"加载图片"从来不是"下载+setImageBitmap"这么简单——下载、下采样、解码、缓存、复用每一步都有坑。Glide 把这套流水线封装成一行代码,是本节主线(末尾对比 Coil)。
# 一、Bitmap 内存账:为什么必须用加载库
Bitmap 内存 ≈ 宽 × 高 × 每像素字节数
每像素字节数:ARGB_8888=4(默认,含透明通道) / RGB_565=2(无透明)
| 场景 | 直接 new/decode 的后果 |
|---|---|
| 原图 4000×3000 显示到 100×100 头像 | 占 48MB,实际只需要 40KB |
| 列表快速滚动 | 反复解码 → GC 风暴 → 掉帧 |
| 大图叠加 | 内存超限 → OOM 崩溃 |
加载库的本质价值:只解码目标 View 大小的 Bitmap(下采样)+ 三级缓存避免重复解码下载 + 生命周期感知的线程调度。
# 二、Glide 基本用法
implementation 'com.github.bumptech.glide:glide:4.16.0'
// 网络来源的图片不需要额外配置(Glide内置HttpUrlConnection,也自动接OkHttp集成库)
// 一行代码:URL/本地文件/资源id/URI 都可以作为参数
Glide.with(imageView) // with传入生命周期载体(Activity/Fragment/View)
.load("https://example.com/avatar.png")
.placeholder(R.drawable.ic_loading) // 加载中占位
.error(R.drawable.ic_error) // 失败占位(含解码失败)
.fallback(R.drawable.ic_empty) // load(null)时的兜底
.centerCrop() // 裁剪变换(还有fitCenter/circleCrop等)
.transition(DrawableTransitionOptions.withCrossFade()) // 淡入
.into(imageView);
Glide.with 的生命周期感知:传入 Activity/Fragment 后,页面销毁时自动取消未完成请求;列表滚动时配合 RecyclerView 复用自动取消旧请求——这是"用库"而不是"手写线程池下载"的核心理由之一。
常用变换:
centerCrop():等比缩放填满并居中裁剪(头像/封面常用)fitCenter():等比缩放到完全可见(全图查看)circleCrop():圆形裁剪- 自定义:继承
BitmapTransformation(加水印、圆角等)
# 三、缓存管线:三级命中即止
理解三点即可应对 90% 的缓存问题:
- 缓存 key 精确到"URL + 尺寸 + 变换参数":同一 URL 不同尺寸/裁剪是不同条目。列表里同一图片以两种大小出现,会解码/缓存两份——正常行为,不是 bug。
- 磁盘缓存有两种策略:
DiskCacheStrategy.ALL:原始图 + 变换后结果都存DiskCacheStrategy.RESOURCE:只存变换后结果(默认)DiskCacheStrategy.AUTOMATIC:Glide 智能选择(远程图存原始数据便于复用多种尺寸)
- 命中即止:活动缓存/内存缓存命中是纯内存操作;磁盘命中省掉网络但仍有解码 IO;全未命中才走网络。这就是"第二次进入页面秒开"的原理。
// 常用缓存配置
Glide.with(this).load(url)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC) // 默认即可
.skipMemoryCache(true) // 特殊场景:跳过内存缓存
.into(view);
// 主动清缓存(设置页"清除缓存"功能)
Glide.get(context).clearMemory(); // 仅主线程:清内存缓存
Glide.get(context).clearDiskCache(); // 仅子线程:清磁盘缓存
# 四、列表与高频坑
# 4.1 RecyclerView 复用错图(经典)
现象:快速滚动列表,item 显示了别的位置的图片。
原因:item 复用时,位置 8 的 ImageView 被复用给位置 18,位置 8 的旧请求还在进行,先返回就"顶掉"了 18 的正确图。
Glide 已内置解决:into(imageView) 时会记录 target 与 view 的绑定,新请求自动取消 view 上的旧请求。会踩坑的多半是绕过了 Glide 的异步 API(自己 Handler 延迟设置、或缓存了 ImageView 引用跨 item 使用)。排查口诀:错图 = 出现了"非本次 load 请求"的赋值。
# 4.2 其他高频坑
- preload 预加载:ViewPager 翻页预载下一页大图:
Glide.with(ctx).load(url).preload(),之后into直接命中内存缓存。 - thumbnail 缩略链:大图先显示低分辨率版本:
.thumbnail(0.25f)(同时加载 1/4 尺寸版本,谁先好显示谁)。 - override 指定尺寸:
override(200, 200)——View 尺寸不定(或目标不是 View)时显式指定,缓存 key 也随之变化。 - GIF/视频帧:
load直接支持 GIF 动图与本地视频第一帧,无需额外库。 - AppGlideModule 定制:全局配置(缓存目录大小、OkHttp 集成
@GlideModule注解类),配合@GlideApp生成流式 API。 - 不要在列表 item 里拿 Glide 的 bitmap 做像素操作:缓存的 Bitmap 可能被复用(reconfigure),要操作先
copy()。
# 五、Coil 对比(Kotlin 时代)
| 对比 | Glide | Coil |
|---|---|---|
| 语言 | Java | Kotlin(官方出品) |
| 并发模型 | 自己的线程池 | 协程(与 20 章呼应) |
| 体积/方法数 | 较大 | 轻(依赖 OkHttp/协程) |
| API | Builder 链式 | 扩展函数 imageView.load(url) {} |
| 缓存机制 | 活动缓存+内存+磁盘 | 内存+磁盘(二级,原理一致) |
| 生态 | 最成熟、功能最全 | 快速成长,Kotlin 项目新宠 |
// Coil 用法预览(Kotlin,20章后无压力)
imageView.load("https://example.com/avatar.png") {
placeholder(R.drawable.ic_loading)
error(R.drawable.ic_error)
transformations(CircleCropTransformation())
}
选型:存量项目/复杂需求(视频帧、动图编辑、老设备兼容)用 Glide;纯 Kotlin 新项目可 Coil。两者缓存思想完全一致,切换学习成本极低。
# 结束
本节讲解了图片加载:Bitmap 内存账(宽×高×4,加载库防 OOM 的本质是按需下采样)、Glide 基本用法(placeholder/error/变换/生命周期感知)、三级缓存管线(配图:活动缓存→内存→磁盘→网络源,key 含 URL+尺寸+变换)、列表复用错图的成因与 Glide 的内置解决,以及 Glide 与 Coil 的选型对比。至此第 12 章数据解析与图片加载完成:JSON(12.01)→ XML 与模型设计(12.02)→ 图片(本节),数据篇收官。下一章进入组件篇:13 四大组件。更多内容请参考:Glide 文档 (opens new window)。