Coil 和 Glide 的加载性能对比数据
Coil 和 Glide 的加载性能对比数据
从一个真实的 ANR 开始
去年维护的一个社交类 App 开始频繁上报 Input dispatching timed out 类型的 ANR,堆栈信息集中在图片加载环节。当时用的是 Glide 4.14.2,在低端机上浏览信息流时,快速滑动会出现主线程阻塞。排查后发现是 Bitmap 解码和变换操作占用了过多 CPU 时间,Glide 的线程池配置在当时的业务场景下显得有些僵硬。
这个契机让我开始认真评估迁移到 Coil 的可行性。Coil 在 Kotlin 社区的风评一直不错,"Kotlin-first"、"轻量"、"基于 Coroutine" 这些标签很吸引人。但性能优化不能靠信仰,我需要看到具体的数据。这篇文章记录了我用 Macrobenchmark 和 Systrace 做的一次相对系统的对比测试,以及后续在线上环境验证时遇到的一些意外。
测试环境搭建:Macrobenchmark 的权限陷阱
做图片加载性能测试,最大的难点是排除网络变量。我选择在本地搭建了一个微型 HTTP 服务器,用 NanoHTTPD 的 2.3.1 版本,预置了 200 张经过处理的图片资源。图片分三组:小图(200x200,WebP 平均 15KB)、中图(800x600,WebP 平均 120KB)、大图(1920x1080,WebP 平均 800KB)。所有图片都经过 cwebp 工具压缩,质量参数统一设为 75。
Macrobenchmark 库的版本用的是 1.2.0-beta01,这个版本在测量帧率时比 1.1.x 稳定不少。但有个坑需要提前说:Android 13 上 WRITE_EXTERNAL_STORAGE 权限的行为变化会导致基准测试数据无法写入,manifest 里必须加上 android:requestLegacyExternalStorage="true" 才能兼容测试设备的文件访问。这个在官方文档里提得不明显,我是在 StackOverflow 的一个 2022 年的帖子里找到的解决方案,原帖链接现在还能访问:https://stackoverflow.com/questions/63364476。
测试设备选了三档:Pixel 7(高端,Android 14)、小米 10(中端,骁龙 865,Android 12)、Redmi 9A(低端,Helio G25,Android 10)。每档设备跑 50 次冷启动后的滚动测试,取第 10 到 40 次的数据平均,排除前几次的 JIT 编译干扰。
帧率数据:Coil 的主线程优势
先说结论,在 Redmi 9A 这种低端设备上,Coil 2.4.0 的滚动帧率比 Glide 4.14.2 高出 8-12 个百分点,具体取决于图片尺寸。
测试场景是一个垂直的 RecyclerView,每个 item 一张图,快速滑动 100 个 item。Coil 的平均帧率(Macrobenchmark 输出的 frameDurationCpuMs 小于 16.7ms 的占比)是 78.3%,Glide 是 66.5%。差距主要来自主线程的调度方式:Coil 的 ImageRequest 完全基于 Kotlin Coroutine,解码操作默认发生在 Dispatchers.Default,而 Glide 的 EngineJob 虽然也有线程池,但部分回调(尤其是 ResourceListener 的通知链)会跳回主线程做状态同步。
用 Systrace 抓一段 10 秒的滑动过程,能看到 Glide 的主线程每隔 200-300ms 会出现一个 5-8ms 的阻塞尖峰,对应 SingleRequest.onResourceReady 里的锁竞争。Coil 的 trace 段里主线程相对干净,但有个反直觉的发现:Coil 的 RealImageLoader 在图片解码完成后,会有一次明显的 Transition.Crossfade 动画提交,如果默认开启 crossfade,这个动画在低端机上会吃掉 2-3ms 的帧预算。Coil 的文档里 crossfade 是默认启用的,这个设计对性能测试有干扰,需要显式关掉:
ImageRequest.Builder(context)
.crossfade(false)
.build()关掉 crossfade 后,Coil 在 Redmi 9A 上的帧率提升到 81.7%,和 Glide 的差距拉大到 15 个百分点。这个细节在 Coil 的 GitHub issue #1602 里有讨论, maintainer Colin White 的解释是 "crossfade is intended for aesthetic, not performance",但默认开启确实容易让新手踩坑。
内存占用:Glide 的复用池 vs Coil 的简洁
帧率只是故事的一半。用 Android Studio Profiler 的 Memory 视图持续监控 5 分钟,Glide 的 Java Heap 曲线明显比 Coil 更平缓,峰值低 10-15MB。Glide 的 LruBitmapPool 在 4.x 版本里已经相当成熟,默认按屏幕尺寸计算池大小,Bitmap 复用策略能显著减少 GC 压力。
Coil 2.4.0 没有内置 Bitmap 池。这个设计取舍在 Coil 的文档里有明确说明:"Coil does not implement a bitmap pool as it adds significant complexity and provides minimal benefit on modern Android versions"。这里的 "modern Android versions" 指的是 Android 8.0 以上的 BitmapFactory.Options.inBitmap,系统层的内存管理已经足够高效。但我实际测试下来,在 Android 10 的 Redmi 9A 上,Coil 的 GC 次数比 Glide 多 20% 左右,每次 GC 的 pause time 在 5-15ms 之间,虽然不至于掉帧,但电量消耗有细微差异。
用 Batterystats 抓了一段 30 分钟的持续滑动测试,Coil 的 CPU 时间比 Glide 多 3.7%,换算成电量大概是每小时多消耗 15-20mAh。这个差距在高端机上可以忽略,但在低端机或者需要长时间后台预加载的场景里,可能需要权衡。
Glide 的内存优势有个代价:配置复杂。GlideBuilder.setBitmapPool()、setArrayPool()、setMemoryCache() 这些 API 给了精细控制权,但学习曲线陡峭。Coil 的内存配置几乎为零,开箱即用,代价是失去了调优空间。我个人觉得这种取舍符合 Kotlin 社区的偏好——默认行为合理,高级用户可以通过自定义 ImageLoader 深入,但文档在这块讲得不够透。
解码速度:WebP 和 HEIF 的分水岭
回到具体的解码性能。我选了两张典型图片做单项测试:一张 800x600 的 WebP(有 alpha 通道),一张 1920x1080 的 HEIF(从 iPhone 导出的实况照片封面)。
WebP 解码,Coil 和 Glide 都依赖系统 BitmapFactory,速度几乎没差别,800x600 的图片在 Pixel 7 上都是 12-15ms。但 HEIF 是个分水岭:Glide 4.14.2 需要额外依赖 com.github.bumptech.glide:heif-integration,而这个集成库最后一次更新是 2021 年,对 Android 12 以上的 HEIF 动画支持有 bug,解码时会 fallback 到软件解码,耗时飙升到 200ms 以上。Coil 2.4.0 直接走系统 ImageDecoder,HEIF 解码在 Android 10+ 上是硬件加速,同一张图只要 35ms。
这个差异不是 Coil 本身多厉害,而是 Glide 的插件生态维护跟不上了。HEIF 在 iOS 端是默认格式,跨平台应用如果要做图片互通,Coil 的兼容性更省心。但如果你的业务里全是 JPEG/WebP,这个优势不存在。
网络层:OkHttp 的隐形成本
Coil 的网络请求默认走它内置的 OkHttpClient 实例,或者你可以传入自己的。Glide 的网络层更灵活,支持 Volley、OkHttp、甚至自定义 ModelLoader。但灵活性意味着配置分散,Glide 的 OkHttp3UrlLoader 需要额外依赖 com.github.bumptech.glide:okhttp3-integration,版本号经常和主库不同步。
我在测试里统一了两者的网络配置:共用同一个 OkHttpClient 实例,连接池、缓存、DNS 完全一致。即便如此,Coil 的首次加载延迟(从 ImageRequest 创建到 onSuccess 回调)比 Glide 快 30-50ms。这个差距不是网络层面的,是请求调度机制的差异。
Glide 的 Engine 有一个复杂的 EngineKey 计算过程,涉及图片 URL、尺寸、变换矩阵、签名等多个维度的哈希,这个计算发生在主线程。Coil 的 MemoryCache.Key 相对简单,默认只取 URL 和尺寸,哈希计算量小。对于信息流这种高频请求场景,Coil 的调度开销更低。
但这里有个反向的坑:Coil 的简单缓存键策略在需要"同 URL 不同显示效果"的场景下会命中错误缓存。比如同一个用户头像,在个人页是圆形裁剪,在列表页是圆角矩形,Coil 默认会认为是同一个缓存键。需要手动配置 memoryCacheKey 和 diskCacheKey 来区分,这个在 Coil 的文档里藏在 "Advanced" 章节,不够显眼。Glide 的 Signature 机制虽然啰嗦,但不容易出错。
包体积:数字不会说谎
用 R8 全量混淆 + 资源压缩后的 APK 对比,只包含图片加载功能,无其他业务代码:
Coil 2.4.0 的核心依赖(io.coil-kt:coil)编译后约 180KB 方法数,DEX 贡献 1.2MB。加上 coil-compose 和 coil-svg 后,总方法数到 240KB 左右。
Glide 4.14.2 的核心依赖(com.github.bumptech.glide:glide)编译后约 430KB 方法数,DEX 贡献 2.8MB。加上 okhttp3-integration、compiler(注解处理器,生成 GlideApp)、以及 GIF/WebP 支持,轻松突破 3.5MB。
这个差距很直观:Coil 用 Kotlin 标准库的扩展函数和 Coroutine 替代了 Glide 大量的接口和回调抽象,代码更紧凑。Glide 的注解处理器生成的代码量也不小,一个中等规模的 App 里 GlideApp 的生成类能有 50KB 以上。
包体积敏感的项目,Coil 的优势明显。但如果你的 App 已经深度集成 Glide,迁移成本不能只看包体积。Glide 的 RequestListener、Target 接口生态有大量历史代码,Coil 的 ImageRequest.Listener 虽然概念对应,但回调时机和线程环境不同,迁移时需要逐条核对。
一个线上验证的意外:Coil 的磁盘缓存失效
实验室数据漂亮,但线上环境总有意外。我们把 Coil 2.4.0 灰度到 5% 用户后,发现磁盘缓存命中率比 Glide 低 40%。排查后发现是 Coil 的默认磁盘缓存策略和 CDN 的缓存头交互有问题。
我们的图片 CDN 返回的 Cache-Control 头是 max-age=3600, s-maxage=86400,Glide 的 DiskLruCacheWrapper 会优先尊重这个头,在有效期内直接读缓存。Coil 的 CacheStrategy 默认行为更保守,如果 Cache-Control 里没有 immutable,它会向服务器发一个条件请求(带 If-Modified-Since)来验证缓存有效性。这个设计在 CDN 边缘节点不稳定时更安全,但我们的 CDN 条件请求响应慢(平均 80ms),导致大量"缓存命中"变成了网络往返。
解决方案是自定义 CacheControl:
val cacheControl = CacheControl.Builder()
.maxAge(1, TimeUnit.HOURS)
.build()
ImageRequest.Builder(context)
.diskCachePolicy(CachePolicy.ENABLED)
.networkCachePolicy(CachePolicy.ENABLED)
.build()或者更直接地,在 OkHttpClient 层面拦截替换响应头。这个行为差异在 Coil 的文档里没有明确对比 Glide 的说明,我是在源码里 CacheStrategy.compute() 的逻辑中发现的。对应的源码文件是 coil-base/src/main/java/coil/network/CacheStrategy.kt,第 87 行的 isCacheable 判断。
Glide 的缓存策略更"粗放",但符合国内 CDN 的常见配置。Coil 的设计更贴近 HTTP 规范,规范在理想环境里是好的,在复杂的国内网络环境下反而需要额外适配。
Compose 集成:这不是一个公平的比较
现在聊 Compose。Coil 的 coil-compose 2.4.0 和 Glide 的 compose-glide 1.0.0-alpha.1(截至 2024 年初的最新版本)完全不在一个成熟度上。
AsyncImage 是 Coil 为 Compose 设计的核心组件,支持 contentScale、placeholder、error、fallback 的声明式配置,状态管理直接对接 ImageRequest 的 Flow。Glide 的官方 Compose 支持还在 alpha 阶段,GlideImage 的重组优化有问题,快速滑动时会出现图片闪烁,原因是 Disposable 的清理时机和 Compose 的重组周期不同步。这个 bug 在 Glide 的 GitHub issue #4996 里有跟踪,但维护者的回复是 "we're working on it",没有明确时间表。
如果新项目用 Jetpack Compose,Coil 几乎是唯一合理的选择。但 coil-compose 也有坑:AsyncImage 的 model 参数接受任意类型,如果传入一个会频繁变化的计算属性(比如带随机查询参数的 URL),会导致不必要的重新加载。正确的做法是用 remember 稳定 ImageRequest 实例:
val request = remember(imageUrl) {
ImageRequest.Builder(context)
.data(imageUrl)
.crossfade(true)
.build()
}
AsyncImage(
model = request,
contentDescription = null
)这个 remember 的用法在 Coil 的示例代码里有,但不够强调,很多开发者直接传 model = imageUrl,在重组频繁时性能很差。
SVG 和动图:功能边界的差异
Coil 的 SVG 支持通过 coil-svg 模块,底层是 Android 的 ImageDecoder 和第三方库的组合。Glide 的 SVG 支持需要找社区方案,比如 com.github.corouteam:glide-to-vector-you,但维护状态参差不齐。
GIF 方面,Glide 的 GifDrawable 是老牌的成熟方案,支持帧级控制、循环次数设置、逐帧解码的内存优化。Coil 2.4.0 的 GIF 支持基于 ImageDecoder,在 Android 9+ 上走系统实现,简单场景够用,但要做复杂的 GIF 编辑(比如叠加文字、帧率调整)就没有 API 了。
WebP 动画也是类似情况。Glide 4.14.2 从 4.12 开始支持 WebP 动画,但需要 webdecoder 集成库,配置繁琐。Coil 直接支持,但控制粒度粗。
这些差异说明:Coil 覆盖的是"常见需求的最优解",Glide 覆盖的是"全功能集的可配置性"。如果你的产品需要深度定制动图播放行为,Glide 仍然是更稳妥的底;如果只是展示,Coil 的简洁是优势。
个人判断:什么时候选什么
经过这轮的测试和线上验证,我的判断是:
新项目、Kotlin 为主、Jetpack Compose、包体积敏感、图片格式以 WebP/JPEG/静态图为主——选 Coil 2.4.0 或更新的 2.x 版本。配置简单,性能数据在主流场景下有优势,社区活跃度(GitHub star 数、issue 响应速度)在 2023-2024 年明显超过 Glide。
历史项目、大量自定义 Transformation 和 Target、需要精细控制 Bitmap 内存池、GIF/WebP 动画有复杂需求、团队对 Glide 的源码已经熟悉——继续用 Glide 4.x,迁移成本可能高于收益。Glide 的维护虽然放缓,但核心功能稳定,不会因为不更新就突然不能用。
混合场景可以考虑渐进迁移:列表页用 Coil 提帧率,详情页的复杂图片编辑保留 Glide。但两个库共存会增加包体积,需要评估是否值得。
最后的数据附录
完整的测试代码我放在了一个私有仓库里,不能公开链接,但关键配置可以复述:
Macrobenchmark 的 BaselineProfile 生成用的是 androidx.benchmark:benchmark-macro-junit4:1.2.0-beta01,测试脚本基于 Google 的 MacrobenchmarkSample 修改,原仓库地址是 https://github.com/android/performance-samples。Systrace 用 perfetto 命令行抓取,分析用 ui.perfetto.dev 网页版,比旧版 Systrace 的 UI 流畅很多。
电量测试的环境比较简陋,Batterystats 的数据用 battery-historian 解析,https://github.com/google/battery-historian 这个项目已经几年没更新,但还能跑。更精确的电量测试应该用 Monsoon 或 Power Monitor 硬件,但成本太高,日常开发用软件估算足够定性。
关于 Coil 的磁盘缓存问题,最终我们的线上方案是自定义了一个 CacheInterceptor,在 OkHttp 层面强制覆盖 CDN 的缓存头,把条件请求关掉。这个做法有点粗暴,但在我们的 CDN 架构下是合理的。如果 CDN 支持更好的缓存验证,Coil 的默认行为反而更安全。
Glide 的 ANR 问题,最后没有根治,而是通过把 decodeFormat 从 PREFER_ARGB_8888 改为 PREFER_RGB_565 缓解,牺牲了一些图片质量换取解码速度。这个方案在 OLED 屏幕上会有明显的色带,不是理想解,但业务优先级不允许做完整迁移。
性能优化永远是权衡,数据能照亮一部分路径,但最终的决策要结合团队现状、业务约束、维护成本。Coil 和 Glide 都是优秀的库,这篇文章的数据希望能给正在做技术选型的开发者一个具体的参考锚点,而不是简单的"A比B好"。