Glide 完全指南

140 阅读33分钟

一、Glide 的缓存机制是几层?分别是什么?

答: Glide 自身的缓存四级:① ActiveResources → ② MemoryCache → ③ 磁盘 Resource → ④ 磁盘 Data。

四级缓存总览(先看表,再分别介绍)

层级存什么数据结构Key引用类型何时写入
① ActiveResources正在被 ImageView 显示的图Map(Engine 内)EngineKey弱引用Resource 交给 Target 显示时立即写入;用完后移出,与 MemoryCache 同时写
② MemoryCache没人正在用、可复用的图(已解码+变换)LruCache(LinkedHashMap)EngineKey强引用仅当从 ActiveResources 移出时(acquired→0)才 put 进来,晚于 ①
③ 磁盘 Resource已解码+变换的图(磁盘文件)磁盘文件(DiskLruCache 等)EngineKey—(文件)DecodeJob 里解码+变换得到 Resource 后写,晚于 ④
④ 磁盘 Data原始数据(磁盘文件)磁盘文件(DiskLruCache 等)DataCacheKey—(文件)DecodeJob 里拿到原始 Data 后即写,早于 ③

查与写:查顺序 ①→②→③→④,未命中再走网络/本地;写不同步——内存先写 ①、用完后才写 ②,磁盘先写 ④、再写 ③。取缓存时 ①②③ 用 EngineKey,④ 用 DataCacheKey

每层 Key / Value 与容器

层级容器KeyValue
Map(Engine 内)EngineKeyWeakReference→EngineResource(图+acquired+回调)
LruCache(LruResourceCache)EngineKeyResource(强引用)
磁盘 key-value(DiskLruCache 等)EngineKey文件:已解码+变换的字节,读回即 Resource
磁盘 key-value(DiskLruCache 等)DataCacheKey文件:原始 Data 字节,读回需再解码+变换

LruCache vs DiskLruCache

对比LruCache(② 用)DiskLruCache(③④ 用)
位置内存,底层 LinkedHashMap磁盘,key→文件,key 经 hash 得文件名
共性key-value、容量上限、LRU 淘汰同上,逻辑像 Map 但数据在磁盘
说明有 maxSize,满则淘汰最久未用③④ 常用其管理磁盘文件,查用 key 找文件再读

EngineKey 与 DataCacheKey 的区别

对比EngineKeyDataCacheKey
用于哪几层① ② ③
包含什么Model、签名、目标宽高变换等,即「哪张图 + 多大 + 什么效果」只与数据源有关(如 Model、Signature),不含宽高、变换
同一 URL 多尺寸/多变换不同尺寸或不同变换 → 不同 Key,各存一份同一张图只一个 Key,只存一份,多请求复用

各层容量与淘汰

层级容量淘汰
无固定上限(由正在用的图数量决定)用完后移出并 put 进 ②
maxSize(常按可用内存比例)LRU 淘汰最久未用
③④共用总容量(如约 250MB)LRU,③④ 一起计入

取/存详细顺序见下文 6. 取缓存的顺序7. 存缓存的顺序。下面分述各级。


1. 第一级:ActiveResources(活动资源)

是什么
存的是「此刻正在被某个 ImageView(或别的 Target)显示」的图。只要这张图还在屏幕上被用着,就放在这里,不参与 MemoryCache 的 LRU 淘汰。

作用

  1. 保护:正在显示的图若只放在 MemoryCache,会被 LRU 按「最近最少用」淘汰,导致界面闪烁或重复解码;单独放在 ActiveResources 后,等没人用了再移回 MemoryCache,避免被误杀。
  2. 复用:同一张图(同一 URL、尺寸、变换)可能同时被多个 ImageView 显示(如列表里两个 item 同一头像)。第一份交给 A 后放进 ActiveResources;B 再请求时先查 ActiveResources,命中则直接复用同一份 Resource 给 B,不重复解码、不占两份内存。这里的「缓存」指当前多请求复用同一份正在用的图,不是「为以后再用」。

数据结构

  • 容器Map(如 HashMap,在 Engine 内),key 区分「是哪一张图」,value 指向「这份图在内存里的对象」。

  • KeyEngineKey。见下表生成规则;同一请求 → 同一 EngineKey。

  • ValueWeakReference 引用 EngineResource。不是 EngineResource 本身;EngineResource 内才包着「图 + acquired + 回调」。

  • Key:EngineKey 的生成规则
    请求条件完全一致 → 同一 Key;任一条件不同 → 不同 Key。参与生成的因素见下表,均参与 equals / hashCode

    因素含义示例
    Model数据来源,即「哪张图」URL、File、Uri
    Signature可选,用于缓存失效同一 URL 若服务端内容更新,可 signature(新对象)(如版本号、更新时间)使 EngineKey 变化,旧缓存失效
    width × height目标尺寸,即「要多大」ImageView 或 override(w, h)
    ResourceClass / TranscodeClass解码 / 转码类型Bitmap、Drawable
    Transformations即「什么效果」centerCrop、圆角、自定义变换
    Options影响解码结果的配置DecodeFormat 等

    因此:同一 URL、同一尺寸、同一套变换 → 同一 EngineKey;B 请求与 A 完全相同时,用同一 Key 在 Map 里可查到 A 正在用的 Resource,直接复用。

  • Value:存储内容
    Map 的 value = 对 EngineResource 的弱引用WeakReference<EngineResource>),不是 EngineResource 本身。
    弱引用指向的 EngineResource 内部包含:

    字段类型 / 含义
    Resource解码+变换后的结果,即要显示的那份图(如 BitmapDrawable、GifDrawable)
    acquiredint,当前有多少个 Target 在用;into(imageView) +1,释放/clear -1
    ResourceListener回调,acquired 从 1→0 时通知 Engine,便于移出 Map 并 put 进 MemoryCache

    概括:value = 弱引用 → EngineResource(图 + acquired + 释放回调)。

  • 为什么必须用弱引用
    若 Map 存强引用,会多占一份「主人」,图在无人使用后仍无法被 GC 回收,易泄漏。改为弱引用后,无人持有时图可被正常回收。

何时写入
Resource 交给 Target 显示时(如 into(imageView))只进 ActiveResources,写 MemoryCache;等 acquired→0 才移出并 put 进 ②。来源不限(网络、磁盘、MemoryCache),只要需要显示就先进 ①。

何时移出
acquired(当前有多少个 Target 在用这份 Resource)控制:

时机acquired 变化结果
into(imageView) 成功绑定+1留在 ActiveResources
ImageView 回收 / clear(imageView) / 页面销毁清理-1
acquired 从 1 → 0从 ActiveResources 移除,put 进 MemoryCache

小结:正在显示的图暂存区;弱引用+引用计数,防 LRU 误杀、多请求复用;用完后移回 ②。


2. 第二级:MemoryCache(内存缓存)

是什么
内存里的「可复用」图缓存:存已解码、已变换的图(Bitmap/Drawable),同一请求再来可直接用,无需再解码。有容量上限,用 LRU 淘汰最久未用的。

数据结构

  • 容器LruCache(底层 LinkedHashMap),实现类为 LruResourceCache。按 key 存 value,容量满时 LRU 淘汰。
  • KeyEngineKey。与 ① ActiveResources 一致(Model、签名、宽高、变换等),同一请求同一 Key。
  • ValueResource。即解码+变换后的结果(如 BitmapDrawable、GifDrawable 等),强引用持有;不包弱引用,Glide 靠容量上限 + LRU 控制内存。

何时写入
Resource 先只进 ① 用于显示;acquired 从 1→0 时从 ① 移出并 put 进 ②。同一份图先 ① 后 ②,不同步写。

容量

类型说明
默认按设备可用内存算 maxSize,常见为可用内存一定比例(如 0.4,低内存约 0.33);或按「屏幕像素×每像素字节数×屏数」估(如约 2 屏)
自定义AppGlideModule 的 applyOptions() 里用 GlideBuilder 的 setMemoryCache(...) / setMemorySizeCalculator(...)

skipMemoryCache(true)
本次请求加载到的图不写入 MemoryCache,也不给后续请求当缓存用,适合大图、验证码等。

小结:内存中已解码+变换的可复用图;LruCache 强引用、LRU 淘汰;先查 ① 再查此层,① 用完后移入此处。


3. 第三级:磁盘缓存 - Resource

是什么
磁盘上存「已解码、已变换」的图(如 Bitmap 序列化/压缩后的文件)。命中后直接读文件得到可显示的 Resource,无需再解码、再变换,是磁盘里最快的一层。

数据结构

  • 容器磁盘 key-value。由 DiskLruCache 或类似实现管理:一个 key 对应一个文件(目录 + 文件,按 key 读写)。
  • Key:由 EngineKey 生成的缓存 key(与 ①② 同逻辑:url、尺寸、变换等参与,同一请求同一 key)。
  • Value(文件内容)解码+变换后的 Resource 序列化/压缩成的字节。读回时由 ResourceDecoder(如 BitmapDecoder)读文件并解码成 Resource,与 MemoryCache 里存的「东西」同质,只是落在磁盘;无强/弱引用概念。

何时写入
DecodeJob 里:先有 Data → 可写 ④;解码+变换得 Resource → 再写 ③。同一请求下 ④ 先写、③ 后写;与内存 ①→② 不同步。

何时读
取缓存时先按当前请求的 EngineKey 查此层;命中则读文件并由 ResourceDecoder 解码成 Resource,放进 ActiveResources 并显示,不再走网络解码和变换。

小结:磁盘上已解码+变换的图文件,EngineKey;命中后直接用、最快;同 URL 不同尺寸/变换各存一份。


4. 第四级:磁盘缓存 - Data

是什么
磁盘上存原始数据(网络字节流或本地文件拷贝),未解码、未变换。命中后需再解码+变换才能显示,但省去网络请求或重复读源文件,省网络、省 IO。

与磁盘 Resource 的区别

对比项磁盘 Resource磁盘 Data
存的内容解码+变换后的结果原始字节
KeyEngineKey(含尺寸、变换)DataCacheKey(只与数据源有关,不含尺寸、变换)
同一 URL 多尺寸/多变换各存一份只存一份,多请求复用同一份 Data 再各自解码、变换

数据结构

  • 容器磁盘 key-value。由 DiskLruCache 或类似实现管理:一个 key 对应一个文件,存原始字节。
  • KeyDataCacheKey。只与数据源有关(如 Model/URL、Signature 等),不含宽高、变换;同一张图不同尺寸/变换共用一个 DataCacheKey,对应同一份文件。
  • Value(文件内容)原始 Data 的字节(网络或本地拿到的 InputStream 等落盘),未解码、未变换;读回后需再解码+变换才能得到 Resource。无强/弱引用概念。

何时写入
拿到原始 Data 后、解码前,若策略允许(ALL/DATA)即写 ④;之后解码+变换再写 ③。④ 是管道里最早写的一层;与内存 ①→② 不同步。

何时读
磁盘 Resource 未命中时,用 DataCacheKey 查此层;命中则读原始数据,再走解码 → 变换,得到 Resource 后写入内存缓存并显示。

小结:磁盘上原始数据,DataCacheKey;命中后需再解码+变换;同图只存一份,多尺寸/变换复用。


5. 缓存未命中时:网络 / 本地数据源(非 Glide 缓存)

前四级都未命中时,从数据源拉取:ModelLoader + DataFetcherData(常见类型:InputStream、ByteBuffer、File 等)→ Decoder 解码 → Transformation 变换 → Resource。拿到数据后的写入顺序见 7. 存缓存的顺序。数据源是「数据从哪来」的最后一环,不属于 Glide 的四级缓存。


6. 取缓存的顺序(读路径)

每一步命中则直接返回/显示未命中则进入下一步

步骤查哪一层用啥 Key命中后得到啥未命中则
1① ActiveResourcesEngineKey正在使用的 Resource,直接用于显示查 ②
2② MemoryCacheEngineKey可复用的 Resource,取出后放进 ① 再显示查 ③
3③ 磁盘 ResourceEngineKey读文件→解码得 Resource,放进 ① 再显示,用完后进 ②查 ④
4④ 磁盘 DataDataCacheKey读文件得 Data → 解码→变换得 Resource,再进 ①、②,按策略回写 ③④走 ⑤
5⑤ 数据源(非缓存)网络/本地拉取 Data → 解码→变换得 Resource,再按策略写入 ①②③④ 并显示请求失败或无数据

总结(取):①→②→③→④ 依次查,命中即用;都未命中才走数据源,拿到数据后仍会按策略回填缓存。


7. 存缓存的顺序(写路径)

存不是「四级同时写」,而是按时机策略分步写:

时机写入哪一层条件 / 说明
拿到原始 Data 后(DecodeJob 内)④ 磁盘 DataDiskCacheStrategy 允许缓存 Data(如 ALL、DATA)时写入
解码+变换得到 Resource 后(DecodeJob 内)③ 磁盘 ResourceDiskCacheStrategy 允许缓存 Resource(如 ALL、RESOURCE)时写入
Resource 交给 Target 显示时① ActiveResources必定进入,并增加 acquired;此时写 ②
Target 不再使用(acquired→0)② MemoryCache从 ① 移出后 put 进 ②,供后续相同请求命中

总结(存):磁盘在 DecodeJob 管道里按策略写 ④(有 Data 后)、③(有 Resource 后);内存先写 ①(要显示时),用完后才写 ②。


8. 磁盘缓存容量与目录

磁盘缓存(③ 磁盘 Resource + ④ 磁盘 Data)通常共用一个总容量上限同一缓存目录(如 image_manager_disk_cache),由 DiskLruCache 或同类实现统一管理:容量满时按 LRU 淘汰,③ 和 ④ 的文件一并计入总容量(具体实现因版本可能略有差异)。

项目说明
默认目录getExternalCacheDir()getCacheDir() 下子目录(如 image_manager_disk_cache)
默认容量常见约 250MB(版本可能不同),③④ 合计
自定义AppGlideModule 的 applyOptions() 里用 GlideBuilder 的 setDiskCache(DiskCache) 指定目录、容量或自定义实现

9. DiskCacheStrategy(磁盘缓存策略)

只影响磁盘(③ 和 ④),控制「原始数据(Data)」和「解码并变换后的资源(Resource)」是否读/写磁盘。不影响内存 ①②。

策略缓存 ④ Data缓存 ③ Resource典型场景
ALL同一张图可能以不同尺寸/变换多次请求;磁盘空间充足时。
DATA只存原始数据,每次读取都需解码+变换,省磁盘,适合原图大、变换多样的场景。
RESOURCE只存解码并变换后的结果,相同请求可直接用;同 URL 不同尺寸/变换会各存一份。
NONE不读写磁盘缓存,适合验证码、实时头像等必须最新的图。
AUTOMATIC由 Glide 根据数据源等决定同上默认策略;例如远程 URL 常缓存 Data+Resource,本地 File 可能不写磁盘等。

取磁盘缓存时:先按 EngineKey 查 ③(Resource),未命中再按 DataCacheKey 查 ④(Data);命中 ④ 后需再解码+变换。Transformation(如 centerCrop、圆角)在解码之后执行,把解码得到的 Resource 再处理成目标尺寸/形状。建议:列表/头像用 ALL 或 AUTOMATIC;大图多尺寸用 DATA;验证码/实时图用 NONE + skipMemoryCache(true) + URL 带时间戳。


10. 如何让某张图不走缓存(如验证码)

与后文第七题内容一致,此处仅概括:

层级做法
内存skipMemoryCache(true)
磁盘diskCacheStrategy(DiskCacheStrategy.NONE)
URL带时间戳、token 等变化,避免网络/请求合并返回旧图

11. 典型命中场景举例

场景可能命中的层说明
列表里同一张头像出现两次(同一 URL、同一尺寸、同一变换)① ActiveResources第一次加载后进 ①,第二次请求同一 Key,直接复用
从列表点进详情,详情页又用同一张图(同一 URL、同一尺寸)② MemoryCache列表滑走后图从 ① 移入 ②,详情页请求同一 Key 命中 ②,取出后先放进 ① 再显示
第二次打开同一列表页,图片上次已缓存到磁盘③ 或 ④若之前写过 ③,用 EngineKey 命中 ③ 直接读文件得 Resource;若只写过 ④,用 DataCacheKey 命中 ④ 再解码+变换
首次加载某 URL、且从未加载过⑤ 数据源①②③④ 都未命中,走网络/本地,拿到 Data 后按策略写 ④→③,Resource 进 ①,用完后进 ②

12. 面试可答小结(背完能答「几层、分别是什么」)

要点内容
几层① ActiveResources(正在显示)→ ② MemoryCache(LRU)→ ③ 磁盘 Resource → ④ 磁盘 Data
①→②→③→④ 依次查,①②③ 用 EngineKey、④ 用 DataCacheKey;未命中走数据源
内存:显示时进 ①,用完后进 ②;磁盘:DecodeJob 里先写 ④、再写 ③,由 DiskCacheStrategy 控制
KeyEngineKey(请求身份,含尺寸/变换)→ ①②③;DataCacheKey(数据源身份)→ ④
容器① Map(弱引用)、② LruCache(强引用)、③④ DiskLruCache 等

二、为什么 Glide 要绑定 Activity/Fragment 的生命周期?

答:

  • 绑定后可在页面销毁时自动取消未完成请求、清理 Target,避免在已销毁的 View 上回调、避免持有已销毁的 Activity/Fragment 导致泄漏或崩溃;
  • 在页面不可见时还可暂停加载,省 CPU 和电量。

绑定的是什么、怎么绑定

  • Glide.with(activity) / Glide.with(fragment) 会拿到一个与该 Activity 或 Fragment 对应RequestManager
  • RequestManager 会监听传入的 Activity/Fragment 的生命周期(通过 Lifecycle 或 FragmentManager 等),在适当时机做「恢复请求」或「取消并清理」。若传入的是普通 Context(如 getApplicationContext()),Glide 按无生命周期处理,得到应用级 RequestManager,不会随页面销毁而取消。

生命周期里 Glide 做了什么

生命周期Glide 行为(典型实现)
onStart / onResumeresumeRequests:恢复该 RequestManager 下的请求,开始加载
onStop / onPausepauseRequests:暂停新请求(已发出的可能继续),页面不可见时少占 CPU
onDestroy取消所有未完成请求clear 该 RequestManager 下所有 Target;不再回调到已销毁的 View,释放对 Activity/Fragment 的引用,避免泄漏

不绑定会有什么问题

问题说明
内存泄漏若用 Application 的 Context,RequestManager 不会随页面销毁而清理,可能长期持有 Activity/Fragment 或 View 的引用
崩溃或异常请求在后台完成时,若回调到已 destroy 的 View 或已回收的 ImageView,可能 NPE 或界面错乱
列表错图RecyclerView 复用时,若请求未随页面销毁取消,可能把「旧请求」的结果设到已复用到新 position 的 ImageView 上,出现串图

with(Activity) / with(Fragment) / with(Application) 对比

传入RequestManager 绑定谁页面销毁时适用场景
Activity / Fragment该 Activity / Fragment自动取消请求、清理 Target,释放引用推荐。页面内加载、列表、详情等
Application Context应用级,无页面生命周期不会因页面销毁而取消;需自己管理与页面无关的加载(如后台预加载、通知栏图标),慎用

列表 / RecyclerView 场景

  • 在 Adapter 里用 Glide.with(holder.itemView.context) 时,若 context 是 Activity,则请求绑定到该 Activity;页面销毁时(如用户返回),该页所有未完成请求会被取消,不会在已销毁的列表上继续回调,也不会持有已回收的 View。
  • 若传的是 Application 的 Context,请求不会随页面销毁取消,容易泄漏、串图或异常。

pauseRequests / resumeRequests 的典型用法

  • RecyclerViewOnScrollListener 里:滑动中调用 Glide.with(context).pauseRequests()停止滑动(IDLE)时调用 resumeRequests(),减少滑动过程中的解码和绑定,提升流畅度。
  • 若 RequestManager 绑定了 Activity/Fragment,onPause 时 Glide 可自动 pause、onResume 时自动 resume,页面不可见时不加载,可见时再加载。

with(Fragment) 与 with(Activity):两者都会绑定生命周期;若在 Fragment 内加载,用 with(fragment) 可让请求随该 Fragment 销毁而取消,比用 Activity 更细粒度(例如 ViewPager 中某页不可见时,该页 Fragment 可能已 onDestroyView,用 with(fragment) 可及时清理)。

取消单次请求:对已经发起的某次加载,可调用 Glide.with(context).clear(imageView) 或对拿到的 Target 调用 clear(),取消该请求、移除对 Target 的引用,不再回调;适用于「用户离开前取消」「列表 item 复用时取消旧请求」等场景。

小结
绑定 Activity/Fragment = 请求和 Target 的生存期与页面一致,销毁时自动清理,避免泄漏、崩溃和错图;配合 pause/resume 还能在不可见时少加载,省资源。


三、Glide 如何防止 OOM?

答: 从以下几方面控制内存,避免单张过大、总量无限和长期占用:

  • 复用:BitmapPool、多级缓存;
  • 少解码:inSampleSize、override;
  • 限容量:MemoryCache 的 maxSize、LRU;
  • 早释放:ActiveResources 弱引用、生命周期绑定后页面销毁时取消;
  • 省格式:默认 RGB_565;
  • 业务配合:override、skipMemoryCache 等。
手段做法作用
BitmapPool解码后的 Bitmap 用完后放入池,下次解码优先从池取尺寸≥需求的 Bitmap 复用内存再解码减少大块分配和 GC,降低 OOM 与卡顿
智能下采样按 ImageView 宽高或 override(w,h) 与原图宽高算 inSampleSize(2 的幂次),只解码到接近目标尺寸例如 4000×3000 在 200×200 的 View 上不会按原图解码,大幅减内存
内存缓存上限MemoryCache 用 LruCache,maxSize 限制(如可用内存比例),超出 LRU 淘汰避免缓存无限增长导致 OOM
弱引用 + 生命周期ActiveResources 用弱引用;请求绑定 Activity/Fragment,页面销毁时取消并清理避免长期持有大图或 Context,及时释放
默认格式不要求透明时默认 RGB_565(2 字节/像素),需透明时 ARGB_8888(4 字节)同尺寸图省约一半内存
override业务对已知显示尺寸使用 override(width, height) 限制解码宽高进一步减小单张图占用

与缓存、BitmapPool 的关系:防 OOM 的「复用」依赖 BitmapPool(见第十三题)和多级缓存(见第一章);限容量、早释放依赖 MemoryCache 的 maxSizeActiveResources 弱引用 + 生命周期绑定(见第二题)。


四、列表快速滑动时如何优化?

答:

  • 在 RecyclerView 的 OnScrollListener 里按滑动状态 pause / resume 该页的 RequestManager;
  • 滑动中 pause、停止滑动(IDLE) 时 resume,with 用 Activity/Fragment;
  • 第二题「pauseRequests / resumeRequests 的典型用法」一致。
滑动状态调用效果
SCROLLING / DRAGGINGGlide.with(context).pauseRequests()暂停该 RequestManager 下的请求(已发出的可能继续),减少滑动中解码和绑定
IDLEGlide.with(context).resumeRequests()恢复加载,当前可见 item 的图片开始加载

注意with(context) 建议传 Activity 或 Fragment,这样只影响该页的 RequestManager;传 Application 会暂停/恢复全局请求,影响范围大。

小结:滑动时 pause、停止时 resume,with 用 Activity/Fragment;已发出的请求可能继续,只暂停请求。


五、Glide 和 Picasso 的区别?

答: 从功能、内存、生命周期、包体积几方面对比;选型结论见下表及下方「如何选」。

对比项GlidePicasso
GIF / 视频帧支持不支持
生命周期与 Activity/Fragment 绑定完善,自动 cancel/清理需自己配合,无内置绑定
内存默认 RGB_565、BitmapPool、多级缓存(含 ActiveResources)相对简单
缩略图 / 变换thumbnail()、多种 Transform功能较少
API / 包体积API 丰富,包体积相对大更简洁、更轻

GIF 原理简述:Glide 加载 GIF 时,通过 GifDecoder 将 GIF 解码为多帧 Bitmap,再按帧率循环显示形成动画;与静态图共用同一套四级缓存与生命周期,仅解码阶段识别为 Gif 类型并走 Gif 解码器。

如何选:列表/详情/头像、要 GIF 或生命周期防泄漏 → Glide;只加载静态图、追求包体积小 → Picasso;大图/长图、需 Native 堆 → Fresco。


六、如何加载大图避免 OOM?

答: 限制解码尺寸、先小图后大图、少进内存缓存,必要时自己做采样或分块解码再交给 Glide。

  • 第三题讲 Glide 内置防 OOM 机制,本题侧重业务侧可调用的 API 与策略。
手段做法
限制解码尺寸override(width, height) 限制解码宽高,不按原图整张解码
先小后大大图详情页用 thumbnail() 先加载小图占位,再加载原图或大图
少进内存缓存对大图 skipMemoryCache(true),不写入 MemoryCache,降低内存峰值
采样 / 分块仍紧张时用 BitmapFactory.Options.inSampleSize 或分块/区域解码,再通过自定义 Decoder 或先解码再交给 Glide 显示

组合用法:大图详情页常用 override(w,h) + thumbnail(0.1f) 先缩略图占位,再加载原图;若仍占内存可再加 skipMemoryCache(true)。可用 placeholder()error() 设置占位图与加载失败图,避免空白或闪屏;preload(w,h) 只加载不进 ImageView,用于预加载到缓存。


七、如何让某张图不走缓存(如验证码)?

答:

  • 内存:不写(skipMemoryCache(true));
  • 磁盘:不读写(diskCacheStrategy(NONE));
  • URL:带时间戳、token 等变化。
    三者配合才能保证「每次最新」。(与第一章 第 10 小节一致,此处单独成题便于记忆。)
层级做法说明
内存skipMemoryCache(true)本次不写入 MemoryCache,也不作为后续请求的缓存命中
磁盘diskCacheStrategy(DiskCacheStrategy.NONE)不读、不写磁盘 ③④
URLURL 或参数带时间戳、token 等变化否则请求合并或网络层缓存仍可能返回旧图

小结:内存、磁盘、URL 三者都做才能保证每次拿到最新图;缺一可能仍命中缓存或网络缓存。


八、简述 Glide 加载一张图片的完整流程(Engine 层)

答:

  • 构建 Request(with、load、into 触发);
  • Engine 按 Key 查缓存 ①→②→③→④;
  • 未命中则拉取 Data、解码、变换得 Resource;
  • 写入缓存并回调主线程显示(①→②,磁盘按策略写 ④→③);
  • 释放时从 ① 移出、回填 ② MemoryCache。
阶段步骤说明
构建Glide.with().load(url).into(imageView)得到 RequestManager(绑定生命周期)、RequestBuilder(配置),发起 Request
查缓存Engine 按 EngineKey(①②③)或 DataCacheKey(④)依次查①→②→③→④;①② 命中得 Resource 直接可用,③ 读文件得 Resource,④ 读文件得 Data 再解码+变换得 Resource
拉取与解码都未命中EngineJob 管线程与回调,DecodeJob 跑具体流程:ModelLoader + DataFetcher 拉取 Data(网络/本地)→ Decoder 解码(期间会 BitmapPool.get)→ Transformation 变换 → 得到 Resource
写入与显示得到 Resource 后ActiveResources,回调主线程 into(ImageView);按 DiskCacheStrategy 写 ④(Data)、③(Resource)
释放Target 不再持有(detach/clear)acquired→0,从 ActiveResources 移出,put 回 MemoryCache

线程模型:拉取 Data、解码、变换在子线程(DecodeJob 所在线程池)执行;得到 Resource 后回调主线程,再执行 into(ImageView) 等 UI 操作,避免在主线程做 IO 和解码。

一句话:取 ①→②→③→④→网络;存 显示时进 ①、用完后进 ②,磁盘在 DecodeJob 里写 ④→③。

与 BitmapPool 的关系:在「拉取与解码」阶段,Decoder 会先向 BitmapPool.get() 取可复用 Bitmap,再解码;解码完成后若 Bitmap 来自池或可复用,释放时会 put 回池(见第十三题)。


九、为什么选择 Glide 而不是其他图片库?

答:

  • 需要生命周期防泄漏、GIF、省内存、列表不串图时选 Glide
  • 只加载静态图、追求极简、包体积小Picasso
  • 大图/长图、需 Native 堆可考虑 Fresco
    (与第五题对比表互补,本题给出选型结论。)
特点适用
Glide生命周期绑定、GIF/视频帧、RGB_565+BitmapPool+多级缓存、RecyclerView 自动取消旧请求防串图多数业务列表/详情/头像
Picasso极简、轻量、不支持 GIF、无内置生命周期只加载静态图、追求包体积小
FrescoNative 堆、大图友好、依赖与接入较重大图/长图/对 Native 内存有需求

十、Glide 如何根据 ImageView 大小计算采样率(inSampleSize)?

答:

  • 在解码阶段用目标宽高(ImageView 或 override)和原图宽高inSampleSize(多为 2 的幂);
  • 只解码到接近目标尺寸,避免整张原图解码,省内存。
输入来源说明
目标宽高ImageView 的宽高 或 override(width, height)希望得到的显示/解码尺寸
原图宽高图片文件头或解码时获取原始像素尺寸
输出说明
inSampleSize一般为 2 的幂(1、2、4、8…),解码时原图宽高各除以 inSampleSize 得到采样后宽高,使解码结果接近目标尺寸

流程:Downsampler 等根据「目标宽高 vs 原图宽高」算 inSampleSize,再交给解码器,这样列表小头像不会按原图大小解码,省内存。目标宽高来自 ImageView 或 override(width, height)(见第三题、第六题),inSampleSize 是实现「只解码到该尺寸」的手段之一。

为何常用 2 的幂inSampleSize 取 2 的幂(1、2、4、8…)时,解码器可只读部分像素(如每隔 n 行/列采样),减少 IO 与计算;非 2 的幂时部分实现会退化为整张解码再缩放。


十一、RequestManager、RequestBuilder、Engine 分别负责什么?

答:

  • RequestManager 管「和谁绑、什么时候能加载、什么时候全部取消」;
  • RequestBuilder 管「这次加载什么、怎么显示、最终 into 到哪」,并在 into() 时把请求交给 Engine;
  • Engine 管「按 Key 查缓存、未命中则拉取解码、写回缓存并回调到 Target」。

RequestManager(请求管理器)

项目说明
从哪来Glide.with(activity/fragment/context) 的返回值;一个 Activity/Fragment 或 Application 对应一个 RequestManager(同作用域复用)
管什么和传入的 Context 对应的生命周期 绑定:页面 onPause/onStop 时可 pauseRequests(暂停新请求),onResume/onStart 时 resumeRequests;页面 onDestroy取消该作用域下所有未完成请求,并 clear 所有已绑定的 Target(如已 into 的 ImageView),不再回调、释放引用
不管什么不管「加载哪张图、用什么变换」——那是 RequestBuilder 的事;不管「查缓存、解码」——那是 Engine 的事

RequestBuilder(请求构建器)

项目说明
从哪来RequestManager.load(model)(如 load(url))的返回值;链式调用的主体,可继续点 placeholder、error、transform、override、into
管什么把「加载什么 Model、占位图、失败图、变换、尺寸、最终展示给谁」拼成一次 Request只有调用了 into(target)(如 into(imageView))时才真正发起这次请求,把 Request 交给 Engine 执行
不管什么不管生命周期和暂停/恢复——那是 RequestManager;不管查缓存、拉数据、解码——那是 Engine

Engine(引擎,内部调度)

项目说明
从哪来Glide 内部单例持有,不对外暴露;RequestBuilder 在 into() 时把 Request 交给 Engine
管什么收到 Request 后:① 按 EngineKey 依次查 ① ActiveResources → ② MemoryCache → ③ 磁盘 Resource → ④ 磁盘 Data;命中则用该层数据回调到 Target;都未命中则起 EngineJob + DecodeJob(拉取 Data → 解码 → 变换),得到 Resource 后写入缓存(①→② 及按策略写 ③④),并回调到 Target
不管什么不管「和谁的生命周期绑、何时 pause/resume」——那是 RequestManager;不管「用户写了 load 还是 transform」——RequestBuilder 已拼好 Request

Target:请求的展示目标,如 into(imageView) 时的 ImageViewTarget;RequestManager.clear(imageView) 或 Target.clear() 会取消该请求并移除引用。

调用链(一句话)with() → RequestManager → load().into() 由 RequestBuilder 拼 Request 并交给 Engine → Engine 查缓存或拉取解码,最后回调到 Target(见第八题)。


十二、ModelLoader 和 DataFetcher 是做什么的?

答:

  • ModelLoader 负责「你的数据类型(Model)→ 该用哪个 DataFetcher 来取数据」;不干 IO,只做「匹配」。
  • DataFetcher 负责「真正做 IO(网络请求、读文件、读 ContentResolver 等),把数据取出来,以 Data(如 InputStream、ByteBuffer)形式返回」;不关心 Model 从哪来、只关心怎么把 Data 拿到。

ModelLoader(模型加载器)

项目说明
从哪来通过 Registry 注册到 Glide;根据 Model 类型(如 String、File、Uri)找到对应的 ModelLoader 实现类(如 StreamUrlLoader、FileLoader)
管什么给定一个 Model(如一个 URL 字符串),返回能提供 DataDataFetcher;即「这种类型的数据,该用谁去拉」——只做匹配,不自己拉数据
不管什么不负责实际 IO(不发起网络请求、不读文件),不负责解码——拉数据是 DataFetcher,解码是 Decoder
典型例子StreamUrlLoader:Model = String(URL) → 返回的 DataFetcher 会发网络请求,得到 InputStream;FileLoader:Model = File → 返回的 DataFetcher 会读文件,得到 InputStream 或 ByteBuffer

DataFetcher(数据获取器)

项目说明
从哪来ModelLoader.buildLoadData() 返回(DecodeJob 向 ModelLoader 要「能拉这份 Model 的 DataFetcher」时拿到)
管什么实现 loadData():真正执行 IO(如 OkHttp 请求 URL、读本地 File、ContentResolver.openInputStream),把结果封装成 Data(InputStream、ByteBuffer、File 等)交给回调;一个 ModelLoader 可对应多种 DataFetcher,按 Model 具体值选(如 URL 用 HttpUrlFetcher,本地路径用别种)
不管什么不关心「Model 是 URL 还是 File」——调用方已经通过 ModelLoader 选好了它;不负责解码——拿到 Data 后交给 Decoder 解码成 Resource

二者关系与调用顺序

  1. DecodeJob 未命中缓存,需要拉数据时,根据 Model 类型Registry 里取到对应的 ModelLoader
  2. 调用 ModelLoader.buildLoadData(model),得到 DataFetcher(以及可选的 Data 的 Class、SourceGenerator 等)。
  3. 调用 DataFetcher.loadData(),在子线程执行 IO,得到 Data(如 InputStream)。
  4. 把 Data 交给 Decoder 解码成 Resource,后续变换、写缓存、回调 Target。

Registry:Glide 的组件注册表,按 Model 类型、Data 类型等查找注册好的 ModelLoader、Decoder 等;DecodeJob 通过 Registry 拿到「该用哪个 ModelLoader、哪个 Decoder」。


十三、BitmapPool 是什么?有什么作用?

答: BitmapPool 是 Glide 用来复用 Bitmap 内存对象池。解码前优先从池里取「尺寸≥需求」的 Bitmap,在其内存上直接解码,用完后不立刻 recycle() 而是放回池中,从而减少大块分配和 GC,降低 OOM 与卡顿。


是什么

  • 本质:一块可复用 Bitmap 的缓存区,存的是「已经分配好像素内存、但当前没人用的 Bitmap」。
  • 与 MemoryCache 区别:MemoryCache 存的是「解码+变换后的整张图」(按请求 Key 命中);BitmapPool 存的是「裸的 Bitmap 对象」,不关心图内容,只按尺寸+格式复用,供解码阶段填入新图。

数据结构

项目说明
实现类默认 LruBitmapPool,按 LRU 淘汰;可通过 GlideBuilder 替换为自定义 Pool
底层存储尺寸(宽×高)+ 配置(Config,如 ARGB_8888/RGB_565) 分组存放;同一尺寸+格式的 Bitmap 归为一类,取时从对应组里拿
容量有总容量上限(如与 MemoryCache 共用一套内存预算或单独计算),超过则按 LRU 淘汰最久未用的 Bitmap 并真正 recycle()
Key逻辑上以「宽、高、Config」为维度,不是请求的 EngineKey;池内不区分图来自哪次请求,只区分「多大、什么格式」

取用逻辑(get)

  • 时机:解码新图时(如 Downsampler 等),在向系统申请新 Bitmap 之前,先调 BitmapPool.get(width, height, config)
  • 规则:从池中取一块 尺寸≥请求宽高(通常取「大于等于且最接近」的)且 Config 匹配的 Bitmap;若没有合适块则 get() 返回 null,解码逻辑再走「新建 Bitmap」。
  • 复用方式:取到的 Bitmap 可能比请求大(如 512×512 用于 200×200),解码时写入该 Bitmap 的像素缓冲区(可能只用到部分区域),再经裁剪/缩放得到目标尺寸;关键是复用其已分配的内存,避免再次 Bitmap.createBitmap()

放入逻辑(put)

  • 时机:某 Bitmap 不再被任何 Resource/缓存/ImageView 引用时(如从 MemoryCache 淘汰、或 Resource 释放时),不直接 bitmap.recycle(),而是 BitmapPool.put(bitmap) 放入池中。
  • 条件:一般要求 Bitmap 未被回收(!bitmap.isRecycled()),且 API 19+ 上需 bitmap.isMutable() == true 才能复用其像素缓冲区写入新数据;不满足则直接 recycle,不放入池。
  • 结果:放入后该 Bitmap 被池持有,后续某次解码若尺寸+格式匹配,就会通过 get() 被取走并再次写入新图。

与解码流程的关系

阶段与 BitmapPool 的关系
解码前pool.get(w, h, config),有则用池中 Bitmap 作为解码目标
解码后得到的新图若来自池,用完后(无人引用时)再 pool.put(bitmap) 还回池;若非来自池且可 mutable,也可在释放时 put
淘汰/释放MemoryCache 淘汰的 Resource 里若有 Bitmap,会 put 回 BitmapPool 而非直接 recycle

容量与配置

  • 默认容量常与 Glide 的内存预算挂钩(如 MemorySizeCalculator 中为 MemoryCache 和 BitmapPool 分配一定比例);池满时再 put 会触发 LRU 淘汰,被淘汰的 Bitmap 会真正 recycle()
  • 可通过 AppGlideModule.applyOptions()GlideBuilder.setBitmapPool() 自定义实现或调整容量。

小结

维度要点
是什么Bitmap 对象池,按尺寸+格式复用「空壳」Bitmap,不存图内容
数据结构默认 LruBitmapPool,按宽高+Config 分组,有容量上限、LRU 淘汰
解码前 get(宽, 高, config),取尺寸≥需求且格式匹配的块
Bitmap 释放/淘汰时不 recycle,改为 put 回池,供后续解码复用
作用减少大块分配与 GC,降低 OOM 和卡顿,列表滑动时效果明显

十四、Glide 默认使用 RGB_565 还是 ARGB_8888?

答:

  • 不要求透明时默认 RGB_565(2 字节/像素),同尺寸比 ARGB_8888 省一半内存;
  • 要透明(alpha)时用 ARGB_8888(4 字节/像素);可配置修改。
格式字节/像素何时用
RGB_5652默认;无透明通道时,同尺寸比 ARGB_8888 省一半内存
ARGB_88884需要透明(alpha)时使用

可通过 DecodeFormat / BitmapFormat 等配置(不同版本 API 可能略有差异)。DecodeFormat 是 Glide 的配置(如 PREFER_RGB_565),Bitmap.Config 是 Android 的;指定方式示例:RequestOptions.formatOf(DecodeFormat.PREFER_RGB_565)asBitmap().format(Bitmap.Config.RGB_565)(具体 API 以当前版本为准)。


十五、RecyclerView 滑动时图片错乱(串图)怎么避免?

答:

  • 数据对应正确:每个 position 用该 position 对应 item 的 url;
  • with 用 Activity/Fragment:页面销毁时取消请求,不在已复用 View 上回调旧结果;
  • 一次 bind 一次 load:onBindViewHolder 里只做一次 load(url).into(imageView);
  • 必要时 override 避免尺寸为 0。
    第二题讲为何绑定生命周期,本题讲具体怎么做。)
原因对应做法
数据与 position 错位每个 position 用该 position 对应 item 的 url,不要用错列表数据;Glide 会把最后一次 load 的结果设到 ImageView,若 url 与 position 不对应就会串图
请求未随页面销毁取消with()Activity/Fragment,页面销毁时取消请求,不会在已复用的 View 上回调旧结果
一次 bind 多次 load 或 url 混乱一次 onBindViewHolder 只做一次 load(item.url).into(holder.imageView),避免对同一 ImageView 先后 load 不同 url 却不清理
布局未测量导致尺寸异常item 有固定尺寸时用 override(width, height),减少解码量并避免尺寸为 0 等异常

RecyclerView 复用与「最后一次」:item 复用时若 url 变了,会再次执行 load(newUrl).into(imageView);Glide 会为新 url 发起新请求,旧请求会被取消或忽略,最终把新请求的结果设到 ImageView,因此只要每次 bind 时用当前 item 的 url,就不会串图。


十六、如何扩展 Glide(自定义 Module)?

答:

  • AppGlideModule(或 LibraryGlideModule),加 @GlideModule
  • applyOptions() 里配默认项(RequestOptions、缓存等),registerComponents() 里注册 ModelLoader、Decoder、Encoder 等;
  • 编译后生成 GlideApp,用 GlideApp.with() 使用扩展能力。
步骤做什么
定义 Module实现 AppGlideModule(应用内只能一个)或 LibraryGlideModule(库可多个),加 @GlideModule 注解
applyOptions()通过 GlideBuilder 设置默认 RequestOptions、MemoryCache、DiskCache 等
registerComponents()通过 Registry 注册自定义 ModelLoaderResourceDecoderEncoder 等,支持新数据源或编解码
编译配合 compilerksp 注解处理器,生成 GlideApp
使用GlideApp.with() 替代 Glide.with(),即带扩展能力

依赖:需引入 Glide 注解处理器(如 com.github.bumptech.glide:compiler),编译时生成 GlideApp。registerComponents 里可注册自定义 ModelLoader(见第十二题)、ResourceDecoderEncoder 等,扩展数据源与编解码。ResourceDecoder 负责把 Data 解码成 Resource;Encoder 负责把 Resource 编码成字节(如写入磁盘缓存)。


十七、如何保证在子线程或后台加载图片并拿到 Bitmap?

答:

  • 子线程拿 Bitmapsubmit(w,h)FutureTarget,子线程 get() 阻塞直到拿到;
  • 主线程拿addListener() 在 onResourceReady 里拿 Resource;
  • 用完后 clear(target)with() 建议用 Application 或与任务同生命周期的 Context。
方式API说明
子线程阻塞拿submit(w, h)asBitmap().submit(),返回 FutureTarget(泛型 Bitmap),子线程 get()阻塞直到拿到 Bitmap
主线程回调addListener() 在 onResourceReady 里拿 Resource不阻塞,在主线程回调;用 into() 时需在 into() 前调用 addListener,用 submit() 时在 submit() 前调用
注意说明
clearFutureTarget 也是 Target,拿到 Bitmap 后应在合适时机 Glide.with(context).clear(target),避免泄漏
with(context)建议 Application 或与任务同生命周期的 Context,否则 Activity 销毁会取消请求,子线程 get() 可能拿不到或异常

典型用法:后台预加载、生成缩略图、保存到文件等场景,用 Glide.with(getApplicationContext()).asBitmap().load(url).submit(w,h) 得到 FutureTarget,在子线程或线程池中 get() 取 Bitmap,用完后在主线程或合适时机 clear(target)


十八、Glide 单例是怎么获取的?和 Application 的关系?

答:

  • Glide.get(context) 内部用 getApplicationContext() 作 key 获取或创建单例,进程内唯一
  • 首次调用会做完整初始化(注册组件、创建 MemoryCache、DiskCache、BitmapPool 等)。
项目说明
获取Glide.get(context);内部用 getApplicationContext() 做 key,故与传入的 Context 无关,单例唯一
与 Application 关系单例以 Application 的 Context 为 key,因此任意 Context(含 Activity)调 Glide.with(context) 都会走到同一套 Glide 实例
首次初始化注册 ModelLoader、Decoder 等组件,创建 MemoryCache、DiskCache、BitmapPool 等
触发时机一般通过 Glide.with(context) 间接触发 get()with(View) 时 Glide 会尝试从 View 拿到所属 Activity/Fragment 再绑定其生命周期

小结:单例以 Application Context 为 key,进程内唯一;任意 Context 调 with() 都会内部触发 Glide.get(context) 拿到同一实例,再根据传入的 Context 类型(Activity/Fragment/Application)返回对应的 RequestManager(见第十一题)。