
先说明一点在绝大多数 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。没有任何缓存同一张图反复请求流量和耗时都被白白浪费。频繁创建 BitmapGC 不断触发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 RequestListenerDrawable() { Override public boolean onLoadFailed(Nullable GlideException e, Object model, TargetDrawable target, boolean isFirstResource) { // 上报失败原因 return false; } Override public boolean onResourceReady(Drawable resource, Object model, TargetDrawable 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 尺寸overrideRGB_565配合服务端裁剪页面销毁后仍回调with 传了 ApplicationContext、使用了自定义 Target改用页面上下文在发起方增加有效性判断图片错乱或旧图闪现item 复用、加载顺序不定、占位图透明设置不透明占位使用默认 into必要时自定义 tag 校验更新了图片但界面还是旧图缓存 key 未变化、服务端未改 URL给请求加 signature让 URL 带版本参数图片加载特别慢图片过大、网络层未复用连接池、服务端机器问题用 RequestListener 统计数据替换网络层服务端按尺寸裁图做图片加载优化这么久我个人的体会是Glide 学习成本不高真正难的是把它放进整个业务体系里统一设计。每个页面都独立调 Glide 参数短期看着灵活长期只会让维护成本越来越高。最值得做的投入是在项目里做一层轻量封装把 URL、尺寸、圆角、缓存策略、加载监听都收敛到统一入口这样后续无论是调优还是排查线上问题都有据可查。图片加载不是一个点而是一条链路把生命周期、内存、缓存和网络这几件事想明白了Glide 才能真正发挥出它应有的价值。