专题:Glide / Coil / Fresco,不是三种写法,而是三套图片加载思路

第 4 周写 ImageView 时,我只把 Glide 放在"够用级接入"的位置:能加载网络图、能显示 placeholder / error / fallback、能用 override() 控制尺寸、能通过 DataSource 看加载来源。

这样处理是对的,因为第 4 周主线还是 ImageView。但如果一直只停在这几行代码:

csharp 复制代码
Glide.with(this)
    .load(url)
    .into(imageView)

那就会漏掉真正值得学的部分:图片框架到底帮我们接管了什么?为什么它不只是"网络请求 + 设置 Bitmap"?为什么同样是图片加载,GlideCoilFresco 的使用方式和适用场景会不一样?

所以这篇专题提前补上。它不是为了把三套框架都接进项目里,也不是为了制造"谁更强"的结论,而是把图片加载框架背后的几条主线拆清楚:请求链路、生命周期、缓存、尺寸控制、动图策略、选型边界。

Demo 实际运行的是 GlideCoilFresco 暂时不强行引入依赖。

相关资料

  • Android 官方《高效加载大型位图》:官方建议使用 Glide、Picasso、Coil、Fresco 等成熟图片加载库;也确认大图需要先读尺寸、再按目标尺寸解码。
  • Glide Getting Started: with → load → into 基础链路、生命周期绑定、clear()、RecyclerView 复用场景。
  • Glide Caching:四级缓存、缓存 Key、signature()DiskCacheStrategy.AUTOMATICskipMemoryCache() 的官方语义。
  • Coil 官方首页和 Getting Started: Kotlin-first、Coroutine Image Loader,支持 Android、Compose Multiplatform、AsyncImageImageView.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 生命周期绑定的 RequestManager
  • load(...):指定数据源,可以是 URL、File、Uri、资源 ID
  • into(...):指定目标 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 官方文档说明,当你传入 ActivityFragment 时,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

可以这样理解:

  1. Active Resources:图片现在是不是正在别的 View 上显示。
  2. Memory Cache:图片最近是不是加载过,还在内存里。
  3. Resource Disk Cache:处理后的结果图有没有落盘,比如 resize、centerCrop、Transformation 后的图。
  4. 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 v1signature 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 / FragmentGlide.with()。它不是为了方便拿 Context,而是为了让 Glide 知道页面什么时候开始、停止、销毁。

源码链路可以简化成:

csharp 复制代码
Glide.with(activity/fragment)
  → RequestManager
  → lifecycle.addListener(this)
  → onStart / onStop / onDestroy 自动管理请求

所以如果你在页面销毁后还看到图片请求回调,优先要检查是不是用了不合适的 Context,或者自定义 Target 没有正确 clear。

2. RequestBuilderload() 只是记录 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 的 AsyncImageImageRequestImageLoader、协程执行模型,和 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 栈?图片复杂到什么程度?团队能承担多少迁移和维护成本?

这三个问题回答清楚,图片框架选型才有意义。

相关推荐
只可远观2 小时前
Android 自动埋点(页面打开 / 关闭 + 点击事件)完整方案
android·kotlin
私人珍藏库3 小时前
【Android】小小最新AI--千变万化扮演任何角色--沉浸式互动
android·app·工具·软件·多功能
zh_xuan3 小时前
Android MVI架构
android·mvi
测试开发-学习笔记4 小时前
Airtest+Poco快速上手
android·其他
李斯维4 小时前
Android Jetpack 简介:由来和演进
android·android studio·android jetpack
阿巴斯甜4 小时前
ARouter 的使用:
android
沐言人生4 小时前
ReactNative 源码分析9——Native View初始化
android·react native
程序员陆业聪5 小时前
当AI学会了混淆代码:LLM辅助混淆 vs R8,Android安全的下一个十字路口
android
yubin12855709235 小时前
mysql正则函数REGEXP
android·数据库·mysql