先说明一点:在绝大多数 Android 项目里,图片加载卡顿和 OOM 并不是 Glide 这个库不行,而是我们常常只把它当成“一行代码显示图片”的工具,没有真正理解它每一层配置背后的意义。Glide 如今几乎是 Android 生态里的标配图片加载方案,它把 Bitmap 解析、缓存、生命周期绑定、加载优先级、图片变换这些能力都集中到了一套 API 里。这篇稿子不打算逐行分析源码,而是从“图片加载为什么会慢”“Glide 原理上到底做了什么”“日常编码该怎么配置”“线上问题怎么排查”这条完整链路展开,适合刚接手图片模块的开发者,也适合团队做性能优化复盘时对照使用。
1. 图片加载为什么会成为应用性能的短板
1.1 从点击一张图到屏幕显示,中间到底发生了什么
用户看到一张图片,背后并不是“文件下载完,直接显示”这么简单。一次完整的图片加载大概要经过这样几步:网络下载或缓存读取、输入流解析、图片宽高读取、根据目标尺寸计算采样率、解码产出 Bitmap、按 ImageView 尺寸做裁剪或缩放、最后上传 GPU 或交由 Canvas 绘制。这中间任何一个环节失控,都可能让界面掉帧。
最容易误导人的一点是:Bitmap 占用的内存等于宽乘高乘每像素字节数,和文件大小不是一回事。一张 1080×1920 的图,使用 ARGB_8888 格式解码,内存占用大约是 1080×1920×4 字节,约 8MB。服务端返回的 JPEG 文件可能只有几百 KB,一到客户端解码就变成几 MB 甚至几十 MB。如果列表一屏有十来个头像,每个都按原图规格去解码,内存和 GC 压力会非常明显。
图片优化本质上就是对这个链路做“按需供给”:只加载你需要的尺寸,只解码你需要的精度,只缓存你应该复用的内容。而不是笼统地“把图片变小一点”。
1.2 不做优化会产生哪些直接后果
我见过不少项目把网络图片加载写成“开线程下载流,然后 BitmapFactory.decodeFile,最后 runOnUiThread setImageBitmap”。这种写法在小 Demo 里没问题,一到线上什么问题都会冒出来:
- 图片解码放在子线程,但没有做采样,超大图被完整解码到内存,OOM 只差时间。
- 回调回来直接操作 View,完全不判断页面是否还在,Activity 销毁后依然更新 UI。
- 没有任何缓存,同一张图反复请求,流量和耗时都被白白浪费。
- 频繁创建 Bitmap,GC 不断触发,RecyclerView 滑动时帧率极不稳定。
- 图片 URL 不变但内容变了,客户端永远显示旧图,只能清应用数据。
这些问题不是靠一两个工具类能解决的,它需要一套完整的图片加载体系。Glide 的存在价值,就是把这些通用问题收敛到一个统一框架里处理。我们使用它,不等于把全部问题甩给框架,还得搞清楚它内部替你做了什么、没做什么。
2. Glide 核心原理拆解:为什么它能做到又快又稳
2.1 生命周期绑定:请求随页面一起暂停和取消
Glide 的 with 方法接受 Activity、Fragment、View 或 Context。传入 Activity 或 Fragment 时,它会在当前页面挂一个无 UI 的 Fragment,用来监听生命周期回调。宿主的 onStart 时恢复请求,onStop 时暂停请求,onDestroy 时取消请求并释放资源。
这个机制的意义非常明显。图片加载是异步的,如果页面已经退出,网络请求还在发,ImageLoading 回调还试图更新 View,轻则白费带宽,重则内存泄漏或崩溃。Glide 把“请求是否应该继续”和页面生命周期绑定在一起,是它能稳定运行在大量列表场景的基础。
但要特别注意:如果 with 传入 ApplicationContext,就失去了页面生命周期绑定能力。请求不会随页面销毁而取消。这种写法适合真正全局性的加载场景,比如通知栏图片、桌面小组件之类。普通页面加载图片,都应该优先传 Activity 或 Fragment。
2.2 三级缓存与请求去重,省的不只是流量
Glide 的缓存体系可以粗略分成三层:活动资源、内存缓存、磁盘缓存。活动资源是正在被使用的 Bitmap,内存缓存基于 LruCache 实现,按最少使用策略淘汰;磁盘缓存负责把处理后的图片或原始数据持久化到本地。
有一个关键点是缓存 key 的生成。同一个 URL 不一定命中同一条缓存,因为 key 会结合目标宽高、图片变换、签名、缓存策略等因素。同一张图,一个场景按 200×200 centerCrop 加载,另一个场景按 800×800 fitCenter 加载,两个请求的缓存 key 不一样,内存里会存在两张 Bitmap。这在逻辑上是正确的,但如果业务上没做统一,很容易出现同图多尺寸重复缓存,白白占用磁盘空间。
请求去重则是列表场景的福音。RecyclerView 快速滑动时,如果多个 item 加载同一张头像,Glide 会把相同 key 的请求合并,只发一次网络请求,等结果返回后同时分发给所有目标。这能极大减少重复请求和重复解码,比手动做内存缓存要可靠得多。
2.3 采样解码和 Bitmap 复用,内存优化的真正功臣
Glide 并不是把整张原图解码出来再按比例缩小,而是在解码阶段就参考 ImageView 尺寸计算采样率,使用 inSampleSize 读取部分像素。也就是说,加载一个 200×200 的头像时,它不会把服务端 1080×1920 的图完整解出来再缩小,而是直接按采样率跳过一部分像素,解码结果本身就更小。这种“先压缩再解码”的方案,能显著降低内存峰值。
同时,Glide 内部维护了一个 BitmapPool。当一个 Bitmap 不再被引用时,它的内存不会立刻还给系统,而是放进池子里等待复用。这样后面加载新图时可以直接使用旧内存块,减少系统分配和 GC 次数。
我给团队做优化时经常强调一点:自定义 Transformation 时不要自己 new Bitmap。如果你在 transform 方法里手动创建新图,等于绕过了 BitmapPool,内存和稳定性都会变差。要尽量通过 Glide 提供的接口申请复用内存,或者明确释放不复用的对象。
2.4 线程模型与优先级调度
Glide 的网络请求和解码任务都发生在后台线程池,主线程只保留最终 UI 更新和必要的生命周期回调。它还支持 IMMEDIATE、HIGH、NORMAL、LOW 四档优先级。列表滚动时,当前可见位置的图片请求会被安排得更靠前,用户会感觉首屏加载更快。
这里要澄清一个误区:priority 不是绝对抢占。它改变的是同一批等待任务里的调度顺序,不等于你先调 IMMEDIATE 就能立刻无视其他任务。它更适合用来做视觉上的主次区分,比如详情页第一张主图给 HIGH,下面普通配图给 NORMAL,而不是拿它当天马行空的同步工具。
3. 日常编码里最值得养成的 Glide 使用习惯
3.1 with() 到底该传谁,真的重要吗
很多项目会写一个全局图片加载工具类,内部用一个静态 context 持有 ApplicationContext,然后所有地方都走这个工具类。这种封装好处是把 url 和配置统一起来,坏处是 Glide 失去了页面生命周期感知能力。
我推荐的原则是:
- 在 Activity 里加载图片,用
Glide.with(this)。 - 在 Fragment 里加载图片,用
Glide.with(this)或getContext()。 - 在 RecyclerView Adapter 里,优先使用
holder.itemView.getContext()。 - ApplicationContext 只留给确确实实不关心页面是否销毁的加载任务。
还要注意一个很实际的坑:Glide.with() 要求在 Android 主线程调用。有人会在协程 IO 线程里先处理 URL,然后直接在 IO 线程执行 Glide.with,多半会在运行期抛异常。遇到这个问题,先把调用切回主线程,或者把加载动作放到 UI 层统一调度。
3.2 占位图、错误图、fallback 和过渡动画,别混成一锅
placeholder 是加载中显示的图,error 是加载失败显示的图,fallback 是 URL 为空时显示的图。三者语义完全不同。很多业务 URL 可能为空,如果只设置了 error,没有 fallback,图片为空时会被当成失败处理,界面会展示错误占位,观感很差。
设置方式很简单:
Glide.with(imageView.getContext()) .load(imageUrl) // imageUrl 可能为 null .placeholder(R.drawable.ic_placeholder) .error(R.drawable.ic_error) .fallback(R.drawable.ic_fallback) .into(imageView);过渡动画方面,我建议谨慎使用 crossFade。淡入效果确实能增加细腻感,但列表快速滑动时,如果图片从内存缓存命中,交叉淡入会造成视觉闪动,也容易掉帧。头像列表、信息流列表我一般不默认加 crossFade,只在详情大图场景单独使用。
3.3 尺寸和裁剪方式,是性能优化里最立竿见影的一步
我见过很多加载代码只写了 load 和 into,没有 override,也没有 centerCrop。如果 ImageView 没有明确宽高,Glide 只能按原始图片尺寸去解码,或者等布局完成后再触发一次加载。这会造成内存浪费,也容易出现图片忽大忽小。
更合理的写法是:明确目标尺寸,并按展示需求选择裁剪方式。centerCrop 会等比缩放并裁剪中间部分,适合头像和一屏铺满的照片;fitCenter 会完整显示整张图片,适合详情页长图和需要看全貌的图。
Glide.with(context) .load(url) .override(400, 400) .centerCrop() .placeholder(R.drawable.ic_placeholder) .error(R.drawable.ic_error) .into(imageView);如果列表里每个 item 尺寸固定,尽量把 RequestOptions 抽成静态常量,不要在 bind 方法里反复 new 对象。频繁创建请求配置虽然单个对象很小,但叠加到列表成千上万次绑定上,依然是完全可以避免的开销。
3.4 磁盘缓存策略,不是越大越好
Glide 4.x 的 DiskCacheStrategy 有 NONE、DATA、RESOURCE、ALL、AUTOMATIC 几种选择。DATA 只缓存原始网络数据,每次显示图片时都需要重新解码;RESOURCE 缓存处理后的图片,下次加载快,但缓存的是按 View 尺寸处理过的版本,不同尺寸会形成不同缓存;ALL 则两者都缓存,速度和空间消耗都最高;AUTOMATIC 会基于图片资源类型做默认选择。
实际场景我一般这样选:头像、商品图、播放页封面这类高度复用的图片,用 AUTOMATIC 或 ALL;验证码、实时行情图、一次性活动图,用 NONE,避免缓存堆积。还有就是图片内容会定期变化的 URL,不能只依赖缓存策略,一定要给请求加签名,比如把图片更新时间拼进 signature,让缓存 key 发生变化。
常见坑是服务端更新了图片但 URL 没变,客户端一直显示旧图。很多人第一反应是清应用缓存,但正确做法是用签名或文件名版本号来主动失效旧缓存。
3.5 缩略图和加载优先级,让用户心理上觉得“更快”
如果加载详情页大图,我强烈建议用 thumbnail。thumbnail 不是普通占位图,它是在同一个 URL 基础上按百分比再加载一个缩小版图片,先显示小图,原图加载完成后自动替换。这样用户进入页面时很快能看到画面轮廓,体感加载速度会好很多。
Glide.with(context) .load(url) .thumbnail(0.1f) .into(imageView);同一页面多张图片时,可以用 priority 调整加载顺序。比如详情页第一屏主图设 HIGH,下方推荐位图片设 NORMAL,这样网络和解码资源会优先给最可能先被看到的图。但再次强调,priority 不是绝对的同步锁,不能依赖它保证加载完成顺序。
4. 把 Glide 接入项目后的扩展与监控
4.1 AppGlideModule 是全局配置的唯一入口
如果项目需要对 Glide 做全局性调整,比如修改磁盘缓存大小、默认解码格式、日志级别,可以写一个 AppGlideModule。4.x 版本里,编译期注解处理器会自动生成注册代码,不需要在 Manifest 里手动声明。
一个简单示例:
@GlideModule public class MediaGlideModule extends AppGlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { builder.setDefaultRequestOptions( new RequestOptions().format(DecodeFormat.PREFER_RGB_565)); builder.setDiskCache( new DiskLruCacheFactory(context.getCacheDir(), "image_glide_cache", 200 * 1024 * 1024)); } }这里有个实际教训:全局只能有一个 AppGlideModule。如果依赖的第三方库内部也注册了 AppGlideModule,编译期会直接提示冲突。遇到这种问题需要找依赖库的源码或配置,把它排除掉,而不是硬写两个。
默认解码格式 ARGB_8888 对图片质量最友好,但内存占用也最高。RGB_565 没有 alpha 通道,能省一半内存,适合缩略图、一般照片和不带透明度的图片。如果图片里有细腻渐变色,RGB_565 可能出现色带,所以要按业务场景权衡,不要全局无脑切。
4.2 替换网络请求层,让图片请求融入项目统一体系
Glide 默认使用 HttpURLConnection 拉取网络图片。但大部分项目的网络层都基于 OkHttp,里面有 token、统一 Header、日志上报、DNS 等逻辑。如果图片网络请求不走这套体系,会带来两个问题:一是无法复用连接池,二是无法统一拦截和监控。
我们可以注册一个 LibraryGlideModule,把 Glide 默认的 HttpUrlLoader 替换成基于 OkHttp 的实现。替换之后,图片请求会自动继承 OkHttp 的连接池、超时配置和拦截器,对整体网络调优非常有帮助。
这种做法并不复杂,但要注意替换方式。LibraryGlideModule 适合库项目使用,应用项目也可以注册。替换完以后,最好先做一次线上回归,重点检查 HTTPS 图片、重定向 URL、断点续传和拦截器是否兼容。图片在弱网环境的表现通常会比默认 HttpURLConnection 更好。
4.3 通过 RequestListener 监控图片加载质量
图片优化不能只凭感觉。我习惯在关键图片请求上挂一个 RequestListener,统计 onResourceReady 时图片来源和耗时。命中内存缓存非常快,命中磁盘缓存较快,网络加载最慢。把这几类数据分开统计,能很快定位慢图到底慢在哪个环节。
Glide.with(context) .load(url) .listener(new RequestListener<Drawable>() { @Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Drawable> target, boolean isFirstResource) { // 上报失败原因 return false; } @Override public boolean onResourceReady(Drawable resource, Object model, Target<Drawable> target, DataSource dataSource, boolean isFirstResource) { // 记录 dataSource 和耗时 return false; } }) .into(imageView);这里特别要提醒:listener 返回 true 表示你已经消费了这个事件,Glide 会中断后续逻辑。如果只是想观察数据,必须返回 false。不少人不小心返回 true,导致图片不显示,排查了很久才发现是监听器把流程拦断了。
4.4 列表页的专项优化清单
RecyclerView 是图片性能问题最集中的地方。一些稳定高效的细节,我列在下面:
- 不要在每个 bind 方法里重新创建 RequestOptions,抽成静态常量复用。
- 不要在自定义 View 的 onDraw 或 layout 回调里触发 Glide 加载,调用频率过高会产生大量无效请求。
- 固定尺寸的列表图,用 centerCrop + override,避免解码出多余像素。
- item 复用时,普通场景默认用 into(ImageView),它会自动取消上一个请求,避免新旧图错乱。
- 如果同一张图在不同位置需要不同尺寸,尽量让服务端按场景返回对应规格,而不是客户端反复做变换。
列表滚动时如果发现明显掉帧,先不要急着优化算法,可以打开 Android Studio 的 Profile 看看内存和 GC。大量 GC 停顿往往就是 Bitmap 频繁创建导致的,先用 Glide 把尺寸和缓存收敛好,再追求更复杂的性能技巧。
5. 线上常见问题与排查技巧实录
5.1 图片一直占位但不报错,可能卡在哪儿
遇到图片一直显示占位图,第一条检查 URL 是否可访问。Android 9 以后默认禁止明文 HTTP 请求,如果地址是 http 而配置没放开,Glide 请求会直接失败,表现就是占位图一直挂着,页面也不一定崩溃。处理方式是按项目规范配置网络安全策略,或者把资源切到 HTTPS。
第二个常见点是磁盘缓存异常。旧缓存文件损坏时,Glide 解码会失败。排查时可以临时把 diskCacheStrategy 改成 NONE,如果图片恢复正常,基本可以确认是缓存问题。线上不适合让用户手动清缓存,可以考虑在版本升级后对缓存目录做一次处理或清理。
还有一个隐蔽原因:RequestListener 的 onLoadFailed 返回 true。如果排查代码时发现有监听器,先把它改成返回 false,再看图片是否恢复。这个坑我踩过不止一次,低级但真实。
5.2 大图 OOM 的排查与处理路径
OOM 不一定发生在网络图加载,本地大图、长截图、相册原图也一样。排查时先确认三点:ImageView 有没有固定宽高、Glide 请求有没有 override、图片源是不是远大于显示区域。
常规降内存路径是:
- 给加载请求设置 override,限制解码尺寸不超过屏幕宽高。
- 非透明图使用 RGB_565 格式,能省一半左右内存。
- 磁盘缓存策略优先用 RESOURCE,避免每次加载都重新解码原始大图。
- 详情大图可以配合 thumbnail 先用小图撑住视觉。
- BitmapPool 不是根治 OOM 的万能药,它只是复用内存,真正问题往往是该缩放的没缩放。
如果服务端返回的图动不动就是 6000×4000,客户端做得再好也有限。更合理的方案是服务端按场景输出不同规格,客户端加载对应的图片地址。客户端和服务端一起调优,效果远远好过单端硬扛。
5.3 页面销毁后回调仍然执行
Glide 绑定生命周期后,页面销毁时会取消请求。如果你发现 onResourceReady 依然被回调,多半是 with 里传了 ApplicationContext,或者请求是在单例、后台工具类里发起的。这时 Glide 根本没有宿主的生命周期可取,自然也不会取消。
另一个情况是自定义 Target。Glide 对普通 ImageView 能通过内部机制管理请求,但对自定义 Target 只能保证它自己不主动取消,无法感知 View 是否存在。所以做后台下载图片再更新业务状态的场景,要自己判断发起者是否仍然有效,不能把状态判断完全交给 Glide。
5.4 列表复用导致图片错乱或旧图闪现
RecyclerView 高速滚动时,item 会被复用给新的位置。如果上一个位置的图片请求还没完成,新的请求已经发出,返回顺序可能和绑定顺序不一致,于是用户会看到旧图一闪或者最终显示错误图。
Glide 的 into(ImageView) 本身会给 ImageView 绑定请求 key,并在新加载时取消旧请求,所以大多数场景不会错图。但如果是手动写的 Target,或者加载速度太快、占位图是透明的,旧图残留就会在替换前被看到。
有效处理方式是给列表加载统一设置不透明的占位图,保证旧图被遮盖。更严格一点,可以在 bind 前先给 ImageView 设置空背景或 tag,再发起新的加载。大多数项目用默认 into 已经足够,不需要强行自定义。
5.5 常见问题速查表
| 症状 | 可能原因 | 排查建议 |
|---|---|---|
| 图片一直显示占位图 | URL 不可达、HTTP 明文限制、磁盘缓存损坏、listener 吞事件 | 检查 URL 和网络安全配置;临时关缓存;检查 RequestListener 返回值 |
| 大图加载 OOM | 没有 override、目标尺寸过大、原图超大、App 内存水位高 | 固定 ImageView 尺寸;override;RGB_565;配合服务端裁剪 |
| 页面销毁后仍回调 | with 传了 ApplicationContext、使用了自定义 Target | 改用页面上下文;在发起方增加有效性判断 |
| 图片错乱或旧图闪现 | item 复用、加载顺序不定、占位图透明 | 设置不透明占位;使用默认 into;必要时自定义 tag 校验 |
| 更新了图片但界面还是旧图 | 缓存 key 未变化、服务端未改 URL | 给请求加 signature;让 URL 带版本参数 |
| 图片加载特别慢 | 图片过大、网络层未复用连接池、服务端机器问题 | 用 RequestListener 统计数据;替换网络层;服务端按尺寸裁图 |
做图片加载优化这么久,我个人的体会是:Glide 学习成本不高,真正难的是把它放进整个业务体系里统一设计。每个页面都独立调 Glide 参数,短期看着灵活,长期只会让维护成本越来越高。最值得做的投入,是在项目里做一层轻量封装,把 URL、尺寸、圆角、缓存策略、加载监听都收敛到统一入口,这样后续无论是调优还是排查线上问题,都有据可查。图片加载不是一个点,而是一条链路,把生命周期、内存、缓存和网络这几件事想明白了,Glide 才能真正发挥出它应有的价值。