第 4 周写 ImageView 时,我只把 Glide 放在"够用级接入"的位置:能加载网络图、能显示 placeholder / error / fallback、能用 override() 控制尺寸、能通过 DataSource 看加载来源。
这样处理是对的,因为第 4 周主线还是 ImageView。但如果一直只停在这几行代码:
csharp
Glide.with(this)
.load(url)
.into(imageView)
那就会漏掉真正值得学的部分:图片框架到底帮我们接管了什么?为什么它不只是"网络请求 + 设置 Bitmap"?为什么同样是图片加载,Glide、Coil、Fresco 的使用方式和适用场景会不一样?
所以这篇专题提前补上。它不是为了把三套框架都接进项目里,也不是为了制造"谁更强"的结论,而是把图片加载框架背后的几条主线拆清楚:请求链路、生命周期、缓存、尺寸控制、动图策略、选型边界。
Demo 实际运行的是 Glide。Coil 和 Fresco 暂时不强行引入依赖。
相关资料
- Android 官方《高效加载大型位图》:官方建议使用 Glide、Picasso、Coil、Fresco 等成熟图片加载库;也确认大图需要先读尺寸、再按目标尺寸解码。
- Glide Getting Started:
with → load → into基础链路、生命周期绑定、clear()、RecyclerView 复用场景。 - Glide Caching:四级缓存、缓存 Key、
signature()、DiskCacheStrategy.AUTOMATIC、skipMemoryCache()的官方语义。 - Coil 官方首页和 Getting Started: Kotlin-first、Coroutine Image Loader,支持 Android、Compose Multiplatform、
AsyncImage、ImageView.load()和单例ImageLoader配置。 - Fresco Getting Started:Fresco 需要全局初始化,使用
SimpleDraweeView,支持ImagePipelineConfig,GIF/WebP 需要额外模块,也存在 Native / Java-only 的取舍。
为什么需要图片加载框架
Android 官方文档其实已经把原因说得很清楚:图片通常比界面实际需要的大得多。一个 ImageView 只显示 128×96 的缩略图,就不应该把 1024×768 甚至 4000×3000 的原图完整解码进内存。
如果完全手写图片加载,至少要处理这些问题:
- 网络请求
- 磁盘缓存
- 内存缓存
- Bitmap 下采样
- 图片尺寸和目标 View 尺寸匹配
- 页面销毁时取消请求
- RecyclerView 复用时避免图片错位
- GIF / WebP 等格式支持
- 加载中、失败、为空的不同 UI 状态
- 大图 OOM 预防
这已经不是一个 ImageView.setImageBitmap() 能解决的范围了。
成熟图片框架的价值,就是把这条链路里的大部分复杂度接管掉。开发者仍然要理解关键点,但不应该每个页面从零造轮子。
Glide:传统 View 体系里的稳定老兵
Glide 最典型的写法是:
csharp
Glide.with(this)
.load(url)
.into(imageView)
这三步可以拆成:
with(...):拿到和Activity/Fragment生命周期绑定的RequestManagerload(...):指定数据源,可以是 URL、File、Uri、资源 IDinto(...):指定目标 View,并真正启动请求
专题 Demo 里实际运行的 Glide 代码更接近真实业务:
kotlin
Glide.with(this)
.load(IMAGE_URL)
.placeholder(R.drawable.bg_week4_placeholder)
.error(R.drawable.vd_week4_landscape)
.override(640, 360)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.signature(ObjectKey(signature))
.listener(object : RequestListener<Drawable> {
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>?,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
binding.tvGlideState.text = "加载成功:DataSource=$dataSource"
return false
}
})
.into(binding.ivGlidePreview)
这段代码里值得看的不是"怎么写链式调用",而是几个工程判断。
with(this) 为什么重要
Glide.with(activity) 或 Glide.with(fragment) 不是随便传个上下文。Glide 官方文档说明,当你传入 Activity 或 Fragment 时,Glide 能跟随生命周期自动取消请求、释放资源。
这意味着页面销毁后,图片请求不会继续拿着旧页面乱回调。
如果是在 RecyclerView 里,复用也要注意。官方文档里提到,对于复用的 View,如果当前位置不需要图片,要么重新发起新的加载,要么显式 clear():
csharp
Glide.with(this).clear(binding.ivGlidePreview)
专题 Demo 里也保留了一个 clear 请求 按钮,就是为了让这个动作可见。它不是为了"清空图片"这么简单,而是在提醒:请求和资源是需要生命周期收尾的。
Glide 缓存:不是一层,也不只看 URL
Glide 官方缓存文档里把缓存查找顺序拆成四层:
Active Resources
Memory Cache
Resource Disk Cache
Data Disk Cache
Original Source
可以这样理解:
- Active Resources:图片现在是不是正在别的 View 上显示。
- Memory Cache:图片最近是不是加载过,还在内存里。
- Resource Disk Cache:处理后的结果图有没有落盘,比如 resize、centerCrop、Transformation 后的图。
- Data Disk Cache:原始数据有没有落盘,比如网络下载下来的原始文件。
如果四层都没有,才回到原始数据源。
专题 Demo 里用 DataSource 把命中来源打出来:
bash
binding.tvGlideState.text = "加载成功:DataSource=$dataSource,signature=$signature。"

这里还有一个很容易被忽略的点:Glide 的缓存 Key 不只是 URL。官方文档说明,缓存 Key 还会受这些因素影响:
- Model,例如 URL / File / Uri
- Signature
- 宽高
- Transformation
- Options
- 请求的数据类型,比如 Bitmap / GIF
所以同一个 URL,用不同尺寸、不同裁剪方式加载,可能对应不同缓存结果。
signature():图片内容变了,别第一反应就关缓存
很多人遇到"图片不更新",第一反应是:
scss
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
这通常不是好选择。Glide 官方文档也建议,不要轻易跳过缓存,因为从缓存加载比重新获取、重新解码、重新转换快得多。
如果图片 URL 不变,但内容变了,更合理的方式是改缓存 Key,比如用 signature():
less
.signature(ObjectKey("avatar-version-2"))
专题 Demo 里有两个按钮:signature v1 和 signature v2。它们加载的是同一个 URL,但 signature 不同:
ini
binding.btnSignatureA.setOnClickListener {
signatureVersion = 1
loadGlideImage(signature = "framework-v1")
}
binding.btnSignatureB.setOnClickListener {
signatureVersion = 2
loadGlideImage(signature = "framework-v2")
}
这个 Demo 的目的不是看图片变化,而是理解:signature 是缓存版本的一部分。
真实业务里,用户头像 URL 不变但图片内容更新,可以把头像版本号、更新时间、文件 lastModified 等混进 signature。
GIF:Glide 能播,但你要管策略
Glide 支持 GIF:
scss
Glide.with(this)
.asGif()
.load(GIF_URL)
.override(360, 240)
.diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.into(binding.ivGlidePreview)
专题 Demo 里还做了一件事:加载成功后限制循环次数。
scss
resource.setLoopCount(3)
这个小动作背后的判断是:GIF 能播放,不代表应该一直播放。聊天表情、评论区动图、Feed 动图如果一屏同时动,很容易造成卡顿和内存压力。

成熟项目一般会考虑:
- 首帧静止
- 点击后播放
- 滑出屏幕后暂停或清理
- 服务端转 WebP / AVIF 动图
- 列表里限制同时播放数量
Glide 源码:with → load → into 背后到底发生了什么
如果只看使用方式,Glide 像是三步:with()、load()、into()。但源码里不是三步,而是一条很完整的请求链。
1. RequestManager:请求入口和生命周期门面
Glide.with(activity) 最终拿到的是 RequestManager。它不是单纯的工具类,而是同时实现了生命周期监听和内存回调:
markdown
RequestManager
= 请求入口
+ 生命周期管理
+ Target 管理
+ RequestTracker 调度
+ ConnectivityMonitor 网络恢复重启
RequestManager 的关键点有三个:
scss
onStart() → resumeRequests(),恢复请求
onStop() → pauseRequests() 或 clearRequests()
onDestroy() → clearRequests(),移除生命周期监听,反注册 RequestManager
这解释了为什么推荐传 Activity / Fragment 给 Glide.with()。它不是为了方便拿 Context,而是为了让 Glide 知道页面什么时候开始、停止、销毁。
源码链路可以简化成:
csharp
Glide.with(activity/fragment)
→ RequestManager
→ lifecycle.addListener(this)
→ onStart / onStop / onDestroy 自动管理请求
所以如果你在页面销毁后还看到图片请求回调,优先要检查是不是用了不合适的 Context,或者自定义 Target 没有正确 clear。
2. RequestBuilder:load() 只是记录 model,into() 才真正启动
RequestBuilder 负责收集请求配置。源码里 load() 的核心动作其实很简单:
ini
this.model = model
isModelSet = true
也就是说:
csharp
Glide.with(this).load(url)
此时只是告诉 Glide"我要加载什么",还没有真正启动请求。
真正启动发生在 into():
scss
RequestBuilder.into(imageView)
→ 检查是否调用过 load()
→ 根据 ImageView.scaleType 自动补 transformation
→ buildRequest()
→ obtainRequest()
→ SingleRequest.obtain(...)
→ requestManager.track(target, request)
这也是为什么 Glide 的链式调用里 into() 很关键。没有 into(),前面的 load() 只是一个配置过程。
RequestBuilder 还会处理复杂请求树:
scss
error(...) → ErrorRequestCoordinator
thumbnail(...) → ThumbnailRequestCoordinator
普通请求 → SingleRequest
所以 thumbnail() 和 error() 不是随便附加两个回调,而是会进入请求协调器,由协调器决定主请求、缩略图请求和错误请求谁能更新目标 View。
3. SingleRequest:一次请求的状态机
SingleRequest 是一次具体请求的执行者。源码里它有明确状态:
objectivec
PENDING
WAITING_FOR_SIZE
RUNNING
COMPLETE
FAILED
CLEARED
一次成功请求通常是:
scss
PENDING
→ begin()
WAITING_FOR_SIZE
→ onSizeReady(width, height)
RUNNING
→ engine.load(...)
COMPLETE
→ target.onResourceReady(...)
这里有一个细节很重要:如果没有指定 override(width, height),Glide 会等 Target.getSize() 拿到 View 尺寸后再进入 Engine.load()。
这也解释了 override() 的意义:
csharp
.override(640, 360)
它不是单纯"压缩图片",而是直接影响请求尺寸,进而影响缓存 Key、解码尺寸和内存占用。
失败时,SingleRequest 会进入 onLoadFailed(),并按优先级设置失败图:
ini
model == null → fallbackDrawable
否则 → errorDrawable
再否则 → placeholderDrawable
这和第 4 周讲的 placeholder / error / fallback 语义完全对应。
4. Engine:内存缓存和任务复用的总调度
SingleRequest.onSizeReady() 最终会调用 Engine.load()。Engine 是 Glide 加载体系里的总调度入口。
源码注释里的顺序可以概括为:
sql
1. Check active resources
2. Check memory cache
3. Check in progress loads
4. Start a new load
完整一点是:
scss
Engine.load()
→ 构建 EngineKey(model + signature + width + height + transformations + options)
→ 查 ActiveResources
→ 查 MemoryCache
→ 查 Jobs 里是否已有相同请求
→ 创建 EngineJob
→ 创建 DecodeJob
→ EngineJob.start(decodeJob)
这里有两个非常重要的概念。
第一,EngineKey 不是 URL。它包含:
arduino
model
signature
width / height
transformations
resourceClass / transcodeClass
options
所以同一个 URL,如果尺寸不同、裁剪不同、signature 不同,就可能是不同缓存结果。
第二,Jobs 不是缓存图片,而是复用正在执行的任务。两个 ImageView 同时请求同一张图时,如果 key 一样,第二个请求会挂到已有 EngineJob 上,而不是再发起一次完整加载。
5. DecodeJob:真正查磁盘、取数据、解码、变换
Engine 负责调度,DecodeJob 才真正进入磁盘缓存、源数据和解码流程。
DecodeJob 内部用 Stage 推进状态:
INITIALIZE
RESOURCE_CACHE
DATA_CACHE
SOURCE
ENCODE
FINISHED
典型顺序是:
RESOURCE_CACHE
→ DATA_CACHE
→ SOURCE
→ ENCODE / FINISHED
对应的 Generator 是:
ResourceCacheGenerator:查变换后的资源磁盘缓存
DataCacheGenerator:查原始数据磁盘缓存
SourceGenerator:从原始数据源取数据,例如网络 / 文件 / ContentProvider
拿到数据后会进入:
scss
decodeFromRetrievedData()
→ decodeFromData()
→ decodeFromFetcher()
→ runLoadPath()
→ onResourceDecoded()
→ Transformation
→ 判断是否写入磁盘缓存
→ callback.onResourceReady()
所以 Glide 的"缓存"不是一个 Map。它是一套从内存活跃资源、内存缓存、磁盘结果缓存、磁盘原始数据、源数据加载、解码、变换、回写缓存组成的完整管线。
把 Glide 源码链路压缩成一句话就是:
RequestManager 管生命周期,RequestBuilder 组装请求,SingleRequest 跑状态机,Engine 查内存和复用任务,DecodeJob 负责磁盘/源数据/解码/变换。
Coil:Kotlin / Coroutine / Compose 项目的自然选择
Coil 官方定位是 Android 和 Compose Multiplatform 图片加载库。它的名字来自 Coroutine Image Loader,也就是基于协程的图片加载器。
如果是传统 Android View,最小写法是:
lua
imageView.load("https://example.com/image.jpg")
如果是 Compose,最小写法是:
ini
AsyncImage(
model = "https://example.com/image.jpg",
contentDescription = null
)
如果需要全局配置,Coil 官方文档给了 ImageLoader 配置入口,例如在 Android 应用中实现 SingletonImageLoader.Factory:
kotlin
class CustomApplication : Application(), SingletonImageLoader.Factory {
override fun newImageLoader(context: Context): ImageLoader {
return ImageLoader.Builder(context)
.crossfade(true)
.build()
}
}
Coil 的选型关键词很清楚:
- Kotlin-first
- Coroutine
- Compose
- Compose Multiplatform
- 轻量依赖
- OkHttp / Ktor 网络模块可选
所以如果是一个新 Kotlin 项目,尤其是 Compose 项目,Coil 很自然。它不是"Glide 的替代品"这么简单,而是更贴近现代 Kotlin UI 栈。
但如果你的项目是老 View 体系,已经大量使用 Glide,并且团队对 Glide 的缓存、Transformation、列表加载已经有经验,那就不一定需要为了"现代"迁移到 Coil。
Coil 源码:ImageLoader 是门面,RealImageLoader 才是执行者
Coil 的 API 看起来比 Glide 更 Kotlin:
lua
imageView.load(url)
或者 Compose 中:
ini
AsyncImage(model = url, contentDescription = null)
但源码里它的核心不是这个扩展函数,而是 ImageLoader。
1. ImageLoader:图片加载服务接口
Coil 3 的 ImageLoader 是接口,源码注释里明确说它适合作为 App 级单例共享。它暴露的核心能力是:
kotlin
fun enqueue(request: ImageRequest): Disposable
suspend fun execute(request: ImageRequest): ImageResult
两者区别很清楚:
| 方法 | 适合场景 | 返回 |
|---|---|---|
enqueue() |
UI 图片加载,不阻塞调用方 | Disposable |
execute() |
协程里等待图片结果、预加载、测试 | ImageResult |
ImageLoader 还暴露:
defaults
components
memoryCache
diskCache
这说明 Coil 的图片加载不是一个孤立请求,而是由默认请求参数、组件注册表、内存缓存、磁盘缓存一起构成的服务。
2. ImageRequest:不可变请求对象
Coil 的 ImageRequest 是不可变 value object,通过 Builder 构建。
核心字段包括:
vbnet
data:请求数据源
target:接收图片显示结果
listener:监听请求生命周期
memoryCacheKey / diskCacheKey:缓存 key
placeholder / error / fallback:三种状态图
sizeResolver:请求尺寸
scale / precision:缩放与精度
fetcherFactory / decoderFactory:自定义获取与解码
memoryCachePolicy / diskCachePolicy / networkCachePolicy:缓存策略
Builder.build() 做的事情是:
ini
用户显式设置的值优先
未设置的值用 defaults 补齐
data == null 时转成 NullRequestData
extras / memoryCacheKeyExtras 转成不可变对象
生成 Defined 记录哪些字段是用户显式设置的
这就是 Coil 的风格:请求对象本身尽量不可变,执行阶段再由 ImageLoader 接管。
3. RealImageLoader:协程、生命周期、拦截器链
ImageLoader.Builder.build() 最终返回的是 RealImageLoader。
RealImageLoader 的关键成员有:
sql
scope:内部 CoroutineScope,使用 SupervisorJob
requestService:请求生命周期和默认参数处理
memoryCache / diskCache:lazy 初始化
components:Fetcher / Decoder / Interceptor 注册表
EngineInterceptor:进入底层加载引擎的核心 interceptor
enqueue() 的链路可以简化成:
scss
ImageLoader.enqueue(request)
→ scope.async(mainCoroutineContext)
→ execute(request, REQUEST_TYPE_ENQUEUE)
→ requestDelegate(findLifecycle = true)
→ updateRequest(initialRequest)
→ target.onStart(placeholder)
→ sizeResolver.size()
→ RealInterceptorChain.proceed()
→ SuccessResult / ErrorResult
→ target.onSuccess / target.onError
execute() 的区别是它是挂起函数,并且不一定主动查找生命周期:
scss
ImageLoader.execute(request)
→ needsExecuteOnMainDispatcher(request)?
→ execute(request, REQUEST_TYPE_EXECUTE)
→ RealInterceptorChain.proceed()
→ ImageResult
Coil 的底层加载是通过 RealInterceptorChain 进入 components.interceptors。其中 EngineInterceptor 是核心节点,后面才会进入 memory cache、disk cache、fetch、decode 等阶段。
所以 Coil 和 Glide 最大的差异之一是:
python
Glide 是 Request / Engine / DecodeJob 这一套传统 View 请求体系;
Coil 是 ImageRequest / RealImageLoader / InterceptorChain 这一套 Kotlin + Coroutine 体系。
4. Coil 为什么适合 Compose
Compose 是声明式 UI,天然更适合用状态和协程组织异步结果。Coil 的 AsyncImage、ImageRequest、ImageLoader、协程执行模型,和 Compose 的心智模型更接近。
但这不代表传统 View 项目一定要迁移到 Coil。迁移不是换一行 API,而是换请求模型、缓存配置、监控方式和团队经验。
Fresco:更像一套独立图片管线
Fresco 和 Glide / Coil 的感觉不太一样。Fresco 官方入门要求先初始化:
scala
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Fresco.initialize(this);
}
}
它使用自己的 View:SimpleDraweeView。
ini
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/my_image_view"
android:layout_width="130dp"
android:layout_height="130dp"
fresco:placeholderImage="@drawable/my_drawable" />
加载图片:
ini
Uri uri = Uri.parse("https://example.com/image.jpg");
SimpleDraweeView draweeView = findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);
官方文档里明确提到,Fresco 会负责下载、缓存、显示,并在 View 离屏时从内存清理。
Fresco 还涉及 ImagePipelineConfig、Native / Java-only、GIF/WebP 模块等配置。比如 Java-only 需要排除 native 相关依赖,并通过 ImagePipelineConfig 禁用 native code。
这说明 Fresco 更像一套独立图片管线,而不是只给标准 ImageView 加一个扩展函数。
它的优势和代价都在这里:
- 优势:管线能力强,配置维度多,适合重图片场景评估。
- 代价:接入成本更高,可能要替换 View,涉及 Native / Java-only、包体、ABI、初始化等问题。
所以 Fresco 不适合作为"随手换一个图片库"的选择。它更适合在项目确实有重图片管线诉求时评估。
Fresco 源码:SimpleDraweeView 背后是 Controller 和 DataSource
Fresco 的源码链路和 Glide/Coil 都不太一样。它不是围绕标准 ImageView 扩展,而是有自己的 Drawee 体系。
1. SimpleDraweeView:输入 URI,构建 Controller
SimpleDraweeView 继承自 GenericDraweeView。源码注释里写得很直接:这个 View 接收 URI,内部构建并设置 controller。
它的核心加载方法是:
less
public void setImageURI(@Nullable Uri uri, @Nullable Object callerContext) {
DraweeController controller =
Preconditions.checkNotNull(mControllerBuilder)
.setCallerContext(callerContext)
.setUri(uri)
.setOldController(getController())
.build();
setController(controller);
}
这段源码说明了 Fresco 的 View 层链路:
scss
setImageURI(uri)
→ controllerBuilder.setUri(uri)
→ setOldController(getController())
→ build()
→ setController(controller)
→ DraweeHolder 持有并管理 controller
setOldController(getController()) 是一个重要细节:每次新请求都会把旧 controller 交给 builder,用于状态复用和生命周期衔接。
另一个坑是 setImageResource()。源码注释明确说它会绕过 Drawee 功能。如果要继续走 Fresco/Drawee 管线,要用 setActualImageResource()。
2. AbstractDraweeController:真正控制请求生命周期
AbstractDraweeController 是 Fresco Drawee 层的控制器。它负责:
arduino
submitRequest
onNewResultInternal
onFailureInternal
releaseFetch
attach / detach
retry on tap
典型生命周期是:
scss
onAttach
→ submitRequest
→ getCachedImage?
→ 命中:onNewResultInternal(final, immediate)
→ 未命中:getDataSource() + subscribe
→ DataSource 回调
→ onNewResultInternal
→ onFailureInternal
onDetach
→ scheduleDeferredRelease
→ release
→ releaseFetch
submitRequest() 里会先尝试缓存:
scss
getCachedImage()
→ 如果命中,直接 onNewResultInternal(..., progress=1.0, isFinished=true)
→ 如果未命中,创建 DataSource 并 subscribe
onNewResultInternal() 会做几件关键事:
scss
检查是否是当前 DataSource
createDrawable(image)
更新 mFetchedImage / mDrawable
hierarchy.setImage(drawable, progress, wasImmediate)
释放旧 Drawable / 旧 Image
通知 listener
这里的"检查是否是当前 DataSource"很重要。Fresco 用 isExpectedDataSource(id, dataSource) 防止旧请求回调污染当前 View,这和 RecyclerView 复用、异步回调错位有关。
失败时,onFailureInternal() 会根据状态选择:
ruby
保留旧图
显示 retry
显示 failure
通知 listener
释放时,releaseFetch() 会关闭 DataSource,释放 Drawable,释放图片对象,并清空状态。
三者怎么选
可以先用这个简单规则:
| 场景 | 更优先考虑 |
|---|---|
| 传统 View 体系、RecyclerView 列表、已有 Glide 生态 | Glide |
| Kotlin-first、Coroutine、Compose / Compose Multiplatform | Coil |
| 极重图片管线、需要独立 Drawee 体系、需要特殊格式/Native 管线评估 | Fresco |
但这不是绝对结论。选图片库不能只看"谁新""谁快""谁大厂用过",而要看项目条件:
- UI 是 View 还是 Compose
- 是否已有历史图片库
- 是否有大量列表图片
- 是否需要 GIF / WebP / AVIF / SVG
- 是否需要自定义缓存策略
- 是否能接受 Native 依赖
- 团队熟悉哪个框架
- 迁移成本有多大
如果是当前这个学习项目,我会这样安排:
- 第 4 周
ImageView:用 Glide 做够用级接入。 - 本专题:深入 Glide 链路,同时理解 Coil/Fresco 的选型边界。
- 后续 Compose 阶段:再真正接入 Coil。
- 后续性能/源码阶段:再拆 Glide 源码链路和缓存实现。
这篇要记住什么
图片框架不是三行代码的语法差异,而是对图片加载复杂度的不同封装方式。
Glide 的核心优势是传统 View 体系成熟稳定,生命周期、缓存、列表加载经验丰富。Coil 的核心优势是 Kotlin、Coroutine、Compose 友好,适合现代 Kotlin 项目。Fresco 的核心优势是独立 Image Pipeline,但接入和维护成本也更高。
所以最后的判断不是"哪个最好",而是:
你的项目是什么 UI 栈?图片复杂到什么程度?团队能承担多少迁移和维护成本?
这三个问题回答清楚,图片框架选型才有意义。