一、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 与容器
| 层级 | 容器 | Key | Value |
|---|---|---|---|
| ① | Map(Engine 内) | EngineKey | WeakReference→EngineResource(图+acquired+回调) |
| ② | LruCache(LruResourceCache) | EngineKey | Resource(强引用) |
| ③ | 磁盘 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 的区别
| 对比 | EngineKey | DataCacheKey |
|---|---|---|
| 用于哪几层 | ① ② ③ | ④ |
| 包含什么 | Model、签名、目标宽高、变换等,即「哪张图 + 多大 + 什么效果」 | 只与数据源有关(如 Model、Signature),不含宽高、变换 |
| 同一 URL 多尺寸/多变换 | 不同尺寸或不同变换 → 不同 Key,各存一份 | 同一张图只一个 Key,只存一份,多请求复用 |
各层容量与淘汰
| 层级 | 容量 | 淘汰 |
|---|---|---|
| ① | 无固定上限(由正在用的图数量决定) | 用完后移出并 put 进 ② |
| ② | maxSize(常按可用内存比例) | LRU 淘汰最久未用 |
| ③④ | 共用总容量(如约 250MB) | LRU,③④ 一起计入 |
取/存详细顺序见下文 6. 取缓存的顺序、7. 存缓存的顺序。下面分述各级。
1. 第一级:ActiveResources(活动资源)
是什么
存的是「此刻正在被某个 ImageView(或别的 Target)显示」的图。只要这张图还在屏幕上被用着,就放在这里,不参与 MemoryCache 的 LRU 淘汰。
作用
- 保护:正在显示的图若只放在 MemoryCache,会被 LRU 按「最近最少用」淘汰,导致界面闪烁或重复解码;单独放在 ActiveResources 后,等没人用了再移回 MemoryCache,避免被误杀。
- 复用:同一张图(同一 URL、尺寸、变换)可能同时被多个 ImageView 显示(如列表里两个 item 同一头像)。第一份交给 A 后放进 ActiveResources;B 再请求时先查 ActiveResources,命中则直接复用同一份 Resource 给 B,不重复解码、不占两份内存。这里的「缓存」指当前多请求复用同一份正在用的图,不是「为以后再用」。
数据结构
-
容器:Map(如 HashMap,在 Engine 内),key 区分「是哪一张图」,value 指向「这份图在内存里的对象」。
-
Key:EngineKey。见下表生成规则;同一请求 → 同一 EngineKey。
-
Value:WeakReference 引用 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) acquired int,当前有多少个 Target 在用; into(imageView)+1,释放/clear -1ResourceListener 回调,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 淘汰。
- Key:EngineKey。与 ① ActiveResources 一致(Model、签名、宽高、变换等),同一请求同一 Key。
- Value:Resource。即解码+变换后的结果(如 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 |
|---|---|---|
| 存的内容 | 解码+变换后的结果 | 原始字节 |
| Key | EngineKey(含尺寸、变换) | DataCacheKey(只与数据源有关,不含尺寸、变换) |
| 同一 URL 多尺寸/多变换 | 各存一份 | 只存一份,多请求复用同一份 Data 再各自解码、变换 |
数据结构
- 容器:磁盘 key-value。由 DiskLruCache 或类似实现管理:一个 key 对应一个文件,存原始字节。
- Key:DataCacheKey。只与数据源有关(如 Model/URL、Signature 等),不含宽高、变换;同一张图不同尺寸/变换共用一个 DataCacheKey,对应同一份文件。
- Value(文件内容):原始 Data 的字节(网络或本地拿到的 InputStream 等落盘),未解码、未变换;读回后需再解码+变换才能得到 Resource。无强/弱引用概念。
何时写入
拿到原始 Data 后、解码前,若策略允许(ALL/DATA)即写 ④;之后解码+变换再写 ③。④ 是管道里最早写的一层;与内存 ①→② 不同步。
何时读
磁盘 Resource 未命中时,用 DataCacheKey 查此层;命中则读原始数据,再走解码 → 变换,得到 Resource 后写入内存缓存并显示。
小结:磁盘上原始数据,DataCacheKey;命中后需再解码+变换;同图只存一份,多尺寸/变换复用。
5. 缓存未命中时:网络 / 本地数据源(非 Glide 缓存)
前四级都未命中时,从数据源拉取:ModelLoader + DataFetcher 取 Data(常见类型:InputStream、ByteBuffer、File 等)→ Decoder 解码 → Transformation 变换 → Resource。拿到数据后的写入顺序见 7. 存缓存的顺序。数据源是「数据从哪来」的最后一环,不属于 Glide 的四级缓存。
6. 取缓存的顺序(读路径)
每一步命中则直接返回/显示,未命中则进入下一步:
| 步骤 | 查哪一层 | 用啥 Key | 命中后得到啥 | 未命中则 |
|---|---|---|---|---|
| 1 | ① ActiveResources | EngineKey | 正在使用的 Resource,直接用于显示 | 查 ② |
| 2 | ② MemoryCache | EngineKey | 可复用的 Resource,取出后放进 ① 再显示 | 查 ③ |
| 3 | ③ 磁盘 Resource | EngineKey | 读文件→解码得 Resource,放进 ① 再显示,用完后进 ② | 查 ④ |
| 4 | ④ 磁盘 Data | DataCacheKey | 读文件得 Data → 解码→变换得 Resource,再进 ①、②,按策略回写 ③④ | 走 ⑤ |
| 5 | ⑤ 数据源(非缓存) | — | 网络/本地拉取 Data → 解码→变换得 Resource,再按策略写入 ①②③④ 并显示 | 请求失败或无数据 |
总结(取):①→②→③→④ 依次查,命中即用;都未命中才走数据源,拿到数据后仍会按策略回填缓存。
7. 存缓存的顺序(写路径)
存不是「四级同时写」,而是按时机和策略分步写:
| 时机 | 写入哪一层 | 条件 / 说明 |
|---|---|---|
| 拿到原始 Data 后(DecodeJob 内) | ④ 磁盘 Data | DiskCacheStrategy 允许缓存 Data(如 ALL、DATA)时写入 |
| 解码+变换得到 Resource 后(DecodeJob 内) | ③ 磁盘 Resource | DiskCacheStrategy 允许缓存 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 控制 |
| Key | EngineKey(请求身份,含尺寸/变换)→ ①②③;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 / onResume | resumeRequests:恢复该 RequestManager 下的请求,开始加载 |
| onStop / onPause | pauseRequests:暂停新请求(已发出的可能继续),页面不可见时少占 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 的典型用法
- 在 RecyclerView 的
OnScrollListener里:滑动中调用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 的 maxSize 和 ActiveResources 弱引用 + 生命周期绑定(见第二题)。
四、列表快速滑动时如何优化?
答:
- 在 RecyclerView 的 OnScrollListener 里按滑动状态 pause / resume 该页的 RequestManager;
- 滑动中 pause、停止滑动(IDLE) 时 resume,with 用 Activity/Fragment;
- 与第二题「pauseRequests / resumeRequests 的典型用法」一致。
| 滑动状态 | 调用 | 效果 |
|---|---|---|
| SCROLLING / DRAGGING | Glide.with(context).pauseRequests() | 暂停该 RequestManager 下的新请求(已发出的可能继续),减少滑动中解码和绑定 |
| IDLE | Glide.with(context).resumeRequests() | 恢复加载,当前可见 item 的图片开始加载 |
注意:with(context) 建议传 Activity 或 Fragment,这样只影响该页的 RequestManager;传 Application 会暂停/恢复全局请求,影响范围大。
小结:滑动时 pause、停止时 resume,with 用 Activity/Fragment;已发出的请求可能继续,只暂停新请求。
五、Glide 和 Picasso 的区别?
答: 从功能、内存、生命周期、包体积几方面对比;选型结论见下表及下方「如何选」。
| 对比项 | Glide | Picasso |
|---|---|---|
| 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) | 不读、不写磁盘 ③④ |
| URL | URL 或参数带时间戳、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、无内置生命周期 | 只加载静态图、追求包体积小 |
| Fresco | Native 堆、大图友好、依赖与接入较重 | 大图/长图/对 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 字符串),返回能提供 Data 的 DataFetcher;即「这种类型的数据,该用谁去拉」——只做匹配,不自己拉数据 |
| 不管什么 | 不负责实际 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 |
二者关系与调用顺序
- DecodeJob 未命中缓存,需要拉数据时,根据 Model 类型 从 Registry 里取到对应的 ModelLoader。
- 调用 ModelLoader.buildLoadData(model),得到 DataFetcher(以及可选的 Data 的 Class、SourceGenerator 等)。
- 调用 DataFetcher.loadData(),在子线程执行 IO,得到 Data(如 InputStream)。
- 把 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_565 | 2 | 默认;无透明通道时,同尺寸比 ARGB_8888 省一半内存 |
| ARGB_8888 | 4 | 需要透明(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 注册自定义 ModelLoader、ResourceDecoder、Encoder 等,支持新数据源或编解码 |
| 编译 | 配合 compiler 或 ksp 注解处理器,生成 GlideApp |
| 使用 | 用 GlideApp.with() 替代 Glide.with(),即带扩展能力 |
依赖:需引入 Glide 注解处理器(如 com.github.bumptech.glide:compiler),编译时生成 GlideApp。registerComponents 里可注册自定义 ModelLoader(见第十二题)、ResourceDecoder、Encoder 等,扩展数据源与编解码。ResourceDecoder 负责把 Data 解码成 Resource;Encoder 负责把 Resource 编码成字节(如写入磁盘缓存)。
十七、如何保证在子线程或后台加载图片并拿到 Bitmap?
答:
- 子线程拿 Bitmap:submit(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() 前调用 |
| 注意 | 说明 |
|---|---|
| clear | FutureTarget 也是 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(见第十一题)。