12.04 数据解析-图片加载

2025/5/25 Android 开发基础

图片是 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(加水印、圆角等)

# 三、缓存管线:三级命中即止

Glide缓存管线示意图

理解三点即可应对 90% 的缓存问题:

  1. 缓存 key 精确到"URL + 尺寸 + 变换参数":同一 URL 不同尺寸/裁剪是不同条目。列表里同一图片以两种大小出现,会解码/缓存两份——正常行为,不是 bug。
  2. 磁盘缓存有两种策略:
    • DiskCacheStrategy.ALL:原始图 + 变换后结果都存
    • DiskCacheStrategy.RESOURCE:只存变换后结果(默认)
    • DiskCacheStrategy.AUTOMATIC:Glide 智能选择(远程图存原始数据便于复用多种尺寸)
  3. 命中即止:活动缓存/内存缓存命中是纯内存操作;磁盘命中省掉网络但仍有解码 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 其他高频坑

  1. preload 预加载:ViewPager 翻页预载下一页大图:Glide.with(ctx).load(url).preload(),之后 into 直接命中内存缓存。
  2. thumbnail 缩略链:大图先显示低分辨率版本:.thumbnail(0.25f)(同时加载 1/4 尺寸版本,谁先好显示谁)。
  3. override 指定尺寸:override(200, 200)——View 尺寸不定(或目标不是 View)时显式指定,缓存 key 也随之变化。
  4. GIF/视频帧:load 直接支持 GIF 动图与本地视频第一帧,无需额外库。
  5. AppGlideModule 定制:全局配置(缓存目录大小、OkHttp 集成 @GlideModule 注解类),配合 @GlideApp 生成流式 API。
  6. 不要在列表 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)。