Android图片加载框架深度对比:Coil 3.4.0 vs Glide 5.0,该选哪个?

0 阅读9分钟

有一个问题挺有意思: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 命中率极高,内存抖动几乎消失。

配置示例:

// 自定义 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 那样持有"死"内存。

// 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 链式,稳定可预期

// 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 集成

// 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:稳定,推荐

// 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,用前要想清楚

// 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 可以复用同一套图片加载逻辑。

// 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.xCoil 3.x
缓存键策略基于 URL + 请求参数(签名)基于 URL + 变换参数哈希
缓存粒度DATA(原图)/ RESOURCE(变换后)/ ALL / NONEREAD / 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 接口,扩展成本低:

// 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。

// 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-okhttpcoil-network-ktor3

Bitmap 获取方式变更BitmapDrawable 不再直接返回,需要通过 .image?.toBitmap() 获取。

Transformation API 变更transform() 方法签名有调整,自定义 Transformation 需要更新。

// 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.0Glide 5.0.5FrescoPicasso
内存管理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 建立了自己的新领地。这场比赛没有输家,只有适合和不适合。