有一个问题挺有意思:Glide 5.0 的 release note 写的是什么?"contains no major changes from Glide 4.16 except that we now compile against Java 8 and Kotlin 1.8"。
也就是说,Glide 的大版本升级,本质上只是编译工具链的对齐,内核没动。而在同一时期,Coil 从 2.x 升到 3.x,几乎是一次重写------API 全面 Kotlin-first,架构拥抱协程,还顺带支持了 Kotlin Multiplatform。
这就很有意思了。两个主流框架,走了截然不同的路线。一个在守,一个在攻。那在实际项目里,2026 年的今天,该怎么选?
结论先说:新项目用 Coil 3.4.0 没问题,Coil 的内存管理已经足够好;复杂场景、存量大型项目,Glide 5.0 依然是更稳健的选择,根本原因是它那套更成熟的 BitmapPool 实现。下面逐一拆解。
一、内存管理:这才是核心战场
图片加载框架最难的地方不是网络请求,是内存。一个 1080p 的 ARGB_8888 图片解码后大概 8MB,一个列表里滑动 50 张,你的内存很快就会感受到压力。
Glide 的 BitmapPool:复用优先
Glide 的杀手锏是 LruBitmapPool。核心思路是:Bitmap 对象本身很贵,申请一块 native 内存代价高,GC 时压力大。Glide 的做法是,当一个 Bitmap 不再被引用时,不直接释放,而是扔进 BitmapPool,按 size + Config 做 LRU 索引。下次需要相同规格的 Bitmap 时,直接从池子里取,不触发新的内存分配。
这个机制在快速滑动列表里效果非常明显:GC 次数减少,帧率更稳定。在图片规格比较统一的场景(比如固定大小缩略图列表),BitmapPool 命中率极高,内存抖动几乎消失。
配置示例:
kotlin
// 自定义 BitmapPool 大小(默认按设备内存等级自动调节)
val bitmapPool = LruBitmapPool(
maxSize = 50 * 1024 * 1024L // 50MB
)
// 注入自定义内存组件
@GlideModule
class MyGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
val memoryCacheSizeBytes = 1024 * 1024 * 20 // 20MB
builder.setMemoryCache(LruResourceCache(memoryCacheSizeBytes.toLong()))
builder.setBitmapPool(LruBitmapPool(memoryCacheSizeBytes.toLong()))
}
}
Coil 3.x 的 MemoryCache:更现代的思路
Coil 3.x 的内存管理走的是另一条路。它的 MemoryCache 缓存的是解码后的 ImageBitmap(或底层 Bitmap),加上 ImageLoader 实例本身支持复用。
Coil 3.x 没有单独的 BitmapPool 机制,但它在协程调度上做了优化:图片解码在后台线程池进行,主线程不阻塞,内存的申请时机更可控。同时,Coil 的 MemoryCache 支持弱引用层(WeakMemoryCache),当内存紧张时会主动释放,不会像 BitmapPool 那样持有"死"内存。
scss
// Coil 3.x ImageLoader 配置
val imageLoader = ImageLoader.Builder(context)
.memoryCache {
MemoryCache.Builder()
.maxSizePercent(context, 0.25) // 使用 25% 可用内存
.build()
}
.diskCache {
DiskCache.Builder()
.directory(context.cacheDir.resolve("image_cache"))
.maxSizeBytes(100 * 1024 * 1024) // 100MB
.build()
}
.build()
// 全局单例,App 内复用同一个 ImageLoader(重要!)
SingletonImageLoader.setSafe { context -> imageLoader }
判断:在图片规格多样、尺寸不固定的场景,Coil 的弱引用 + 主动释放策略反而内存表现更好。但在高频滑动、图片尺寸统一的场景,Glide 的 BitmapPool 命中率更高,GC 压力更小。存量项目迁移前,做一次基准测试是必要的,不要凭感觉。
二、API 设计:Kotlin-first vs Java-friendly
API 设计上,两者哲学差异非常明显。
Glide:Builder 链式,稳定可预期
scss
// Glide 5.x 典型用法
Glide.with(context)
.load(imageUrl)
.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
.centerCrop()
.diskCacheStrategy(DiskCacheStrategy.ALL)
.override(300, 300)
.into(imageView)
// 在协程中异步获取 Bitmap
val bitmap = withContext(Dispatchers.IO) {
Glide.with(context)
.asBitmap()
.load(imageUrl)
.submit()
.get() // 阻塞式,注意需在 IO 线程
}
Glide 的 RequestBuilder 模式在 Java 时代设计,但在 Kotlin 项目里依然能用,没有割裂感。链式调用清晰,每个选项都有明确的 API。
Coil 3.x:协程原生,Flow 集成
scss
// Coil 3.x 协程方式异步加载,返回 ImageResult
lifecycleScope.launch {
val request = ImageRequest.Builder(context)
.data(imageUrl)
.size(300, 300)
.crossfade(true)
.target(imageView)
.build()
val result = imageLoader.execute(request)
when (result) {
is SuccessResult -> { /* 成功 */ }
is ErrorResult -> { /* 失败,result.throwable */ }
}
}
// 用 AsyncImage 扩展(非 Compose)
imageView.load(imageUrl) {
placeholder(R.drawable.placeholder)
error(R.drawable.error)
transformations(CircleCropTransformation())
}
// 直接在挂起函数中使用
suspend fun loadAsBitmap(url: String): Bitmap? {
val request = ImageRequest.Builder(context)
.data(url)
.allowHardware(false) // 需要操作像素时关闭 hardware bitmap
.build()
return (imageLoader.execute(request) as? SuccessResult)
?.image?.toBitmap()
}
Coil 3.x 的 API 更贴合现代 Kotlin 开发范式。execute() 返回密封类 ImageResult,配合 when 处理结果非常自然。如果团队已经全面 Kotlin + 协程,Coil 的 API 体验确实更顺手。
三、Compose 集成:成熟度有差距
Compose 项目选图片框架,这个维度很重要。
Coil Compose:稳定,推荐
scss
// build.gradle.kts
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
// 使用 AsyncImage(最常用)
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("https://example.com/image.jpg")
.crossfade(true)
.build(),
contentDescription = "图片描述",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.clip(RoundedCornerShape(12.dp)),
placeholder = painterResource(R.drawable.placeholder),
error = painterResource(R.drawable.error)
)
// 需要更细粒度控制,用 SubcomposeAsyncImage
SubcomposeAsyncImage(
model = imageUrl,
contentDescription = null
) {
val state = painter.state
when {
state is AsyncImagePainter.State.Loading -> CircularProgressIndicator()
state is AsyncImagePainter.State.Error -> ErrorView()
else -> SubcomposeAsyncImageContent()
}
}
Glide Compose:仍是 beta,用前要想清楚
ini
// build.gradle.kts
implementation("com.github.bumptech.glide:compose:1.0.0-beta01")
// GlideImage 用法
GlideImage(
model = imageUrl,
contentDescription = "图片",
contentScale = ContentScale.Crop,
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
) {
it.placeholder(R.drawable.placeholder)
.error(R.drawable.error)
}
Glide 的 Compose 扩展在 2024 年底还是 beta01,API 不稳定。如果你的 Compose 项目对稳定性要求高,直接用 Coil 避免踩坑。Glide 的 View-based 集成依然是它的强项。
四、Kotlin Multiplatform:Coil 的独家优势
Coil 3.x 是目前主流图片加载框架里唯一真正支持 KMP 的。这意味着在 KMP 项目里,iOS 和 Desktop 可以复用同一套图片加载逻辑。
kotlin
// KMP 项目 build.gradle.kts(commonMain)
kotlin {
sourceSets {
commonMain.dependencies {
// coil 3.x 核心支持 KMP
implementation("io.coil-kt.coil3:coil-core:3.4.0")
implementation("io.coil-kt.coil3:coil-network-ktor3:3.4.0")
}
androidMain.dependencies {
implementation("io.coil-kt.coil3:coil-compose:3.4.0")
}
}
}
// commonMain 中定义的图片加载逻辑,Android/iOS/Desktop 共享
fun createImageLoader(context: PlatformContext): ImageLoader {
return ImageLoader.Builder(context)
.components {
add(KtorNetworkFetcherFactory())
}
.build()
}
Glide 仅支持 Android,这是它在架构层面的硬限制。如果团队正在考虑 KMP 迁移路径,Coil 3.x 是唯一合理选择。
五、磁盘缓存策略对比
两者在磁盘缓存上的策略有明显差异:
| 维度 | Glide 5.x | Coil 3.x |
|---|---|---|
| 缓存键策略 | 基于 URL + 请求参数(签名) | 基于 URL + 变换参数哈希 |
| 缓存粒度 | DATA(原图)/ RESOURCE(变换后)/ ALL / NONE | READ / WRITE / ENABLED / DISABLED |
| 缓存实现 | DiskLruCache(Jake Wharton 版本) | 自研 DiskCache,基于 okio |
| 默认大小 | 250MB | 可配置,默认 100MB |
Glide 的 DiskCacheStrategy.RESOURCE 缓存变换后的图片非常实用------比如固定 300x300 的缩略图,磁盘里直接存 300x300 的,下次命中直接用,不需要重新解码和变换。Coil 3.x 类似逻辑需要通过配置缓存 key 来实现。
六、图片变换生态
Glide 内置了常用变换(CenterCrop、CircleCrop、RoundedCorners 等),同时有 glide-transformations 三方库提供丰富扩展。
Coil 3.x 内置变换更精简,但支持自定义 Transformation 接口,扩展成本低:
kotlin
// Coil 3.x 自定义 Transformation
class BlurTransformation(
private val radius: Float = 10f,
private val sampling: Float = 1f
) : Transformation() {
override val cacheKey = "BlurTransformation(radius=$radius,sampling=$sampling)"
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
// 使用 RenderScript 或 Toolkit 做模糊
return Toolkit.blur(input, radius.toInt())
}
}
// 使用
imageView.load(url) {
transformations(BlurTransformation(radius = 20f))
}
Glide 的 MultiTransformation 支持链式叠加多个变换,生态更成熟。这个维度 Glide 胜出,但差距不大。
七、大图与超长图处理
这个话题经常被忽视,但在电商、地图、医疗影像等场景非常关键。
结论:Glide 和 Coil 都不是处理超长图的合适工具。两者都是 "加载 + 展示" 框架,对于超长图(比如长截图、地图切片),正确姿势是 SubsamplingScaleImageView(SSIV)。
SSIV 的核心思路是:按当前视口区域和缩放比例,只解码需要显示的那一部分图片数据,实现真正的按需加载。Glide/Coil 加载的是完整图片,遇到 8000x20000 的超长图直接 OOM。
less
// SubsamplingScaleImageView:大图处理的正确方案
implementation("com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0")
// 布局中使用
//
// 代码中加载
subsamplingView.setImage(ImageSource.uri(imageUri))
// 或者加载 asset 文件
subsamplingView.setImage(ImageSource.asset("large_image.jpg"))
普通图片用 Glide/Coil,超长图/超大图用 SSIV,这是正确的分工,两者不冲突。
八、迁移成本:Coil 2.x → 3.x 破坏性变更
如果项目用的是 Coil 2.x,升到 3.x 有一些不可忽视的破坏性变更:
• 包名变更 :从 io.coil-kt:coil 改为 io.coil-kt.coil3:coil(末尾加了 3),可以和 2.x 共存,但会引入两套依赖。
• ImageLoader 初始化方式变更 :2.x 用 Coil.setImageLoader(),3.x 改为 SingletonImageLoader.setSafe()。
• 网络组件解耦 :3.x 移除了内置的 OkHttp 依赖,需要手动添加 coil-network-okhttp 或 coil-network-ktor3。
• Bitmap 获取方式变更 :BitmapDrawable 不再直接返回,需要通过 .image?.toBitmap() 获取。
• Transformation API 变更 :transform() 方法签名有调整,自定义 Transformation 需要更新。
kotlin
// Coil 2.x 初始化方式(旧)
class App : Application() {
override fun onCreate() {
super.onCreate()
Coil.setImageLoader(
ImageLoader.Builder(this)
.okHttpClient { OkHttpClient() }
.build()
)
}
}
// Coil 3.x 初始化方式(新)
class App : Application(), SingletonImageLoader.Factory {
override fun newImageLoader(context: PlatformContext): ImageLoader {
return ImageLoader.Builder(context)
.components {
add(OkHttpNetworkFetcherFactory(
callFactory = { OkHttpClient() }
))
}
.build()
}
}
如果是存量大型项目,迁移成本不低。建议先在新模块试水,用基准测试(Macrobenchmark)对比内存和帧率数据后再决定是否全量迁移。
九、Fresco 和 Picasso:边缘化了吗?
简单说一下另外两个选手的现状。
Fresco
Facebook 出品,最大特点是图片数据存在 Ashmem(匿名共享内存),完全绕开 JVM 堆,对 OOM 有天然免疫。在低内存设备和超大图场景有一定优势。但代价是:侵入性极强,必须用 SimpleDraweeView 替换所有 ImageView;API 相对复杂;Compose 支持基本没有官方维护。2024 年开始,Meta 内部也在逐步迁移到其他方案。除非你的场景是极端低内存设备或 Facebook 风格的 DraweeView 架构,否则不推荐新项目采用。
Picasso
Square 出品的老牌框架,API 简洁优雅,曾经是最流行的选择。但 Picasso 已经很长时间没有重大更新,最新版本 2.8 对 Kotlin 协程、Compose 都没有原生支持。团队维护力度也在下降。新项目不推荐,存量项目如果运行稳定也没必要强行迁移。
十、横向对比总表
| 维度 | Coil 3.4.0 | Glide 5.0.5 | Fresco | Picasso |
|---|---|---|---|---|
| 内存管理 | MemoryCache + 弱引用 | BitmapPool LRU(更成熟) | Ashmem(极端场景) | 基础 LRU |
| Kotlin 支持 | 原生 first-class | 良好(Java 设计) | 一般 | 一般 |
| Compose 集成 | 稳定(推荐) | beta(慎用) | 无官方支持 | 无官方支持 |
| KMP 支持 | 已支持 | 不支持 | 不支持 | 不支持 |
| API 复杂度 | 简洁 | 适中 | 复杂 | 简洁 |
| 高频滑动性能 | 良好 | 优秀(BitmapPool) | 优秀(Ashmem) | 一般 |
| 维护活跃度 | 高 | 高 | 降低 | 低 |
| 新项目推荐 | 推荐(Compose/KMP) | 推荐(复杂/大型) | 特殊场景 | 不推荐 |
结论:怎么选
说完这么多,给个明确的判断:
选 Coil 3.4.0,如果:
• 新项目,技术栈是 Kotlin + Compose,想要 API 顺手
• 项目有 KMP 规划,需要跨端共享图片加载逻辑
• 图片场景以普通列表和详情页为主,规格多样
• 团队 Kotlin 功底扎实,愿意拥抱协程优先的架构
选 Glide 5.0,如果:
• 存量大型项目,不想承担大规模迁移风险
• 应用场景是高频滑动列表 + 规格统一的图片(BitmapPool 命中率高)
• 团队有 Glide 深度定制经验(自定义 ModelLoader、GlideModule 等)
• 需要稳定的 View-based 集成,Compose 占比不高
最后一个务实建议:如果真的拿不准,写一个 MicroBenchmark,在你们自己的机器上跑一下内存峰值和 GC 次数对比。通用结论是别人的,你项目的数据才是你的。
Glide 守住了自己的护城河,Coil 建立了自己的新领地。这场比赛没有输家,只有适合和不适合。