Skip to content

图片加载方式总览与原理 ​

回到总览:图片加载 / Image Loading上层封装:Landscapist:Compose 图片加载封装(本页讲的引擎即它背后的三个后端) 相关模块:Bitmap 内存与 GC(inBitmap 复用 / 降采样)· LRU 与淘汰策略(内存/磁盘缓存的淘汰基础)· Compose 状态模型(remember 与重组语义)

一句话定义 ​

「图片加载」在工程上 = 取字节(网络/磁盘/资源)→ 解码成 Bitmap/Drawable(含降采样)→ 缓存(内存/磁盘)→ 绑定到视图生命周期并绘制 四个阶段的组合。不同库的区别在于:请求如何建模、缓存分几级、bitmap 如何复用、生命周期如何绑定。本页按 Glide / Coil / Fresco / Compose 原生 / 手动解码 五类逐一拆解其内部机制。

代码索引 ​

主题Lab 说明源码
图片加载方式总览与原理暂无可运行 lab(需 Android / Compose 运行环境;labs/kotlin/host 为 JVM,不适用)—

1. 实现原理 ​

1.1 Glide:Request 链 + 四级查找 + BitmapPool ​

请求建模:Glide.with(context) 返回 RequestManager(绑定生命周期);.load(model) 返回 RequestBuilder;.into(target) 构建并启动 Request。

加载流程(内存四级 → 磁盘 → 源) :

into() ──► ActiveResources(弱引用,正在使用的资源)
            └─ miss ─► MemoryCache(LruResourceCache,LRU)
                        └─ miss ─► DiskCache
                                      ├─ ResourceCache(已变换/解码的 bitmap)
                                      └─ DataCache(原始字节,变换前)
                                            └─ miss ─► Source(网络/文件解码)
  • ActiveResources:Map<Key, ResourceWeakReference>,持有「正在显示」的资源弱引用,避免被 MemoryCache 的 LRU 淘汰掉正在用的图。
  • MemoryCache / DiskCache:见 00 的 LRU 与 DiskLruCache。
  • 解码:Downsampler 用 BitmapFactory.Options.inSampleSize + inBitmap 做降采样与复用(复用机制见 02 的 inBitmap 段);复用池是 LruBitmapPool。
  • 线程:磁盘读取/解码在 DiskCacheExecutor / SourceExecutor(后台),结果 post 回主线程绘制。
  • 生命周期绑定:RequestManagerRetriever 向 Activity/Fragment 注入一个不可见的 SupportRequestManagerFragment,监听 onDestroy 以暂停/取消请求。

1.2 Coil:ImageLoader 单例 + Interceptor 责任链 ​

请求建模:AsyncImage(model, …)(Compose)或 imageLoader.enqueue(ImageRequest) 构建 ImageRequest,交给全局 ImageLoader(由 ImageLoader.Builder 配置 MemoryCache / DiskCache / BitmapPool / Components)。

加载流程(Interceptor 链) :

enqueue ──► MemoryCacheInterceptor(强引用 + 弱引用二级)
             └─ miss ─► DiskCacheInterceptor(DiskLruCache)
                            └─ miss ─► FetchInterceptor(OkHttp/文件/Content 取字节)
                                           └─► DecodeInterceptor(Decoder 解码,降采样)
                                                  └─► EngineInterceptor(写回 MemoryCache/DiskCache)
  • MemoryCache:StrongMemoryCache(LRU,value 强引用)+ WeakMemoryCache(value 弱引用,被 GC 前仍可命中),带引用计数。
  • DiskCache:DiskLruCache(Jake Wharton 版,journal 日志)。
  • 协程驱动:整条链跑在 Dispatchers.IO;AsyncImagePainter 把结果映射成状态机:Empty → Loading → Success / Error,Compose 据此切占位/成功/失败。
  • 生命周期:rememberAsyncImagePainter / AsyncImage 把请求绑定到 Composition,离开组合取消。
  • Coil 3 / KMP:ImageLoader 平台无关;网络栈可换 Ktor/OkHttp,同一 AsyncImage 在 Android / iOS / JVM / Wasm 复用。

1.3 Fresco:Drawee(UI)+ ImagePipeline(producers 链) ​

两层分离:

  • Drawee(视图层) :DraweeController + DraweeHierarchy(多层 drawable:placeholder / 实际图 / progress / retry / failure)。SimpleDraweeView 是包裹 DraweeHolder 的 View。
  • ImagePipeline(非 UI 层) :一条 Producer 责任链,每个节点只做一步并交给下游:
NetworkFetchProducer ─► DiskCacheProducer(编码字节,磁盘)
   └─► EncodedMemoryCacheProducer(编码字节,内存)
          └─► DecodeProducer(解码,PlatformDecoder:BitmapFactory/ImageDecoder)
                 └─► BitmapMemoryCacheProducer(解码后 bitmap,LRU)
                        └─► PostprocessorProducer ─► CloseableReference 交付 Drawee
  • CloseableReference<T>:引用计数管理的 native/Java 内存句柄,使用方必须 close() 释放。早期 Android 上 bitmap 像素放 native 内存(Ashmem / pinned),绕开 Dalvik 堆上限——这是 Fresco 相对 Glide/Coil 的差异化点(现代 Android 上 bitmap 本身已进 native,该优势减弱)。
  • 生命周期:DraweeController 监听 View 的 onAttach/onDetach;detach 时关闭 CloseableReference 释放图片。

1.4 Compose 原生加载 ​

  • Image(painter = painterResource(R.drawable.x), …):同步解码本地资源(vector / drawable / mipmap),无网络、无缓存、无异步;适用于图标、本地图。
  • AsyncImage(model, …)(Coil) :见 §1.2;声明式,placeholder / error / loading 都是 Composable,状态来自 AsyncImagePainter。
  • SubcomposeAsyncImage:在 loading/success/error 各状态可子组合出不同 UI(如加载完成后用主色做背景),比 AsyncImage 更灵活。
  • rememberAsyncImagePainter(model):返回 Painter,自行用 Image(painter) 绘制,适合自定义绘制逻辑。
  • 手动解码:ImageDecoder.decodeDrawable(API 28+) 或 BitmapFactory.decodeStream + inSampleSize/inBitmap,得到 Bitmap 后 Image(bitmap.asImageBitmap());用于需要精确控制解码线程/采样、或加载非标准来源(加密文件、自定义协议)的场景。

1.4.1 Android 本地资源解码与密度缩放 ​

在使用 BitmapFactory.decodeResource(resources, id) 或 painterResource 加载打包在 APK 中的位图资源(PNG/JPG)时,Android 系统会在解码期隐式应用密度缩放:

  • 缩放计算:系统将图片所在资源目录的密度设为 inDensity,设备的真实密度设为 inTargetDensity,解码输出尺寸公式为:解码宽度=原图宽度×inTargetDensityinDensity。
  • 关联策略:为避免在低密度桶放置图片导致在现代高分屏设备上被二次放大、引发内存平方级飙升,本地小图标推荐使用矢量图(VectorDrawable)或放置于高密度桶(如 xxhdpi),大背景图则放置于 drawable-nodpi 配合 inSampleSize 降采样(详细物理内存计算与密度桶选型策略,见 12/01 dp/dpi 与图片资源放置 §2-4)。

1.5 五类方式的共同骨架 ​

无论哪类,内部都绕不开这四步,区别只在「谁负责」与「缓存分几级」:

取字节 → 解码(降采样)→ 写缓存(内存/磁盘)→ 绑定生命周期并绘制

2. 特点与优势(与其他方式的区别) ​

  • Glide vs Coil:Glide 生态成熟、BitmapPool 复用激进、生命周期自动、Java 时代积累多;代价是 API 偏旧(命令式 into)、包体大、无 KMP。Coil Kotlin 优先、协程、体积小、Compose 原生 AsyncImage、v3 起 KMP;代价是生态与极端内存手段(native 内存)弱于 Glide/Fresco。
  • Glide/Coil vs Fresco:Fresco 的 CloseableReference + native 内存对超大图/低端机(尤其是 API<21)更稳,producers 链可插拔、适合复杂管线;代价是包体最重、学习曲线陡、不 Compose 优先(需 AndroidView 桥 SimpleDraweeView)。现代 Android(bitmap 已 native)上 Fresco 的相对优势明显缩小。
  • 库加载 vs Compose 原生 painterResource:本地资源用 painterResource 最简、零依赖、同步可控;网络/大图必须走带缓存与生命周期管理的库(Coil AsyncImage 或其封装),否则要自己重写缓存与取消逻辑。
  • 库加载 vs 手动 BitmapFactory/ImageDecoder:手动解码给你解码线程与采样点的完全控制,但缓存、生命周期、占位/重试全要自己写;除非来源特殊(加密/私有格式),否则用库。
  • 与 Landscapist 的关系:Landscapist 是 §1.1–§1.3 三个引擎的 Compose 封装层,自身不实现取字节/解码/缓存(见 01 的「缓存来自引擎」),本页才是被封装引擎的内部原理。

3. 用法 ​

3.1 Glide ​

kotlin
Glide.with(imageView)
    .load(url)
    .placeholder(R.drawable.placeholder)
    .error(R.drawable.error)
    .apply(RequestOptions().override(400, 400).centerCrop())
    .into(imageView)
// 取消:Glide.with(imageView).clear(imageView)(视图销毁自动触发)

3.2 Coil(Compose) ​

kotlin
AsyncImage(
    model = url,
    contentDescription = "photo",
    modifier = Modifier.size(200.dp),
    placeholder = painterResource(R.drawable.placeholder),
    error = painterResource(R.drawable.error),
)
// 或更低层:
val painter = rememberAsyncImagePainter(url)
Image(painter = painter, contentDescription = null)

3.3 Fresco ​

kotlin
// Application 初始化一次:Fresco.initialize(context)
val controller = Fresco.newDraweeControllerBuilder()
    .setUri(uri)
    .setOldController(simpleDraweeView.controller)
    .build()
simpleDraweeView.controller = controller
// 内存由 CloseableReference 管理,View detach 自动释放

3.4 Compose 原生(本地资源 / 手动解码) ​

kotlin
// 本地资源:同步、无缓存
Image(painter = painterResource(R.drawable.icon), contentDescription = "icon")

// 手动解码(API 28+),自行控制采样与线程
val source = ImageDecoder.createSource(context.contentResolver, uri)
val drawable = ImageDecoder.decodeDrawable(source) { decoder, _, _ ->
    decoder.allocator = ImageDecoder.ALLOCATOR_SOFTWARE
    decoder.setTargetSize(400, 400) // 降采样到目标尺寸
}
Image(painter = drawable.toPainter(), contentDescription = null)

3.5 选型速查 ​

场景首选
Android + Compose 网络图,轻量Coil AsyncImage
需要多后端统一 API + 插件(blur/palette/shimmer)/ KMPLandscapist(见 01)
超大图 / 低端机 / 复杂管线Fresco
老工程、生态依赖多Glide
本地图标/资源painterResource
加密/私有格式来源手动 ImageDecoder + 库缓存

本页回答的问题(一句话答) ​

  • Glide 的加载流程分几级查找?
    • 一句话答:内存四级(ActiveResources 弱引用 → MemoryCache LRU → DiskCache 的 Resource/Data 两级 → Source),逐级 miss 才向下,解码后逐级回写。
  • Coil 为什么是「责任链」而不是「一步步调用」?
    • 一句话答:ImageLoader 把 MemoryCache/DiskCache/Fetch/Decode 做成 Interceptor 链,每个节点只负责一级(命中即短路返回),便于插入自定义拦截器与统一回写缓存。
  • Fresco 的 CloseableReference 解决什么?
    • 一句话答:用引用计数管理(含 native)内存,使用方 close() 才释放,避免 bitmap 长时间占 Dalvik 堆;现代 Android 上该优势已减弱。
  • Compose 里加载本地图和加载网络图为什么用不同 API?
    • 一句话答:本地 painterResource 同步解码、无缓存需求;网络/大图需缓存 + 生命周期 + 异步,交给 Coil AsyncImage 或 Landscapist。
  • 手动 BitmapFactory/ImageDecoder 何时才该用?
    • 一句话答:仅当来源特殊(加密/私有协议)或需精确控制解码线程/采样;否则用库,避免重写缓存与取消逻辑。

对应实验 ​

暂无对应 lab:图片加载库需要 Android / Compose 运行环境,而 labs/kotlin/host 是 JVM 工程,无法运行 Glide/Coil/Fresco 或 Compose AsyncImage;如后续建立 Android Host lab,可补一个最小 AsyncImage / GlideImage 验证工程。

复习检查题 ​

  1. Glide 的「ActiveResources」和「MemoryCache」分别解决什么?为什么需要两级内存? 答:ActiveResources 用弱引用持有「正在显示」的资源,防止它们被 MemoryCache 的 LRU 在仍在使用期间淘汰;MemoryCache 是常规 LRU 缓存。两级配合:正在用的不被淘汰,不用的按 LRU 回收。
  2. Coil 的 MemoryCache 为什么有「强引用 + 弱引用」两部分? 答:强引用部分(LRU)保证近期访问一定命中;弱引用部分在内存压力下可被 GC 回收,被回收前若再次访问仍能命中,兼顾命中率与内存弹性。
  3. Fresco 的 ImagePipeline 和 Drawee 为什么要分开? 答:Drawee 只管 UI 层(层级 drawable + 生命周期),ImagePipeline 只管取字节/解码/缓存的生产链(producers),两者解耦后管线可被非 UI 场景复用,且 Drawee 切换图时不阻塞生产。
  4. 为什么现代 Android 上 Fresco 相对 Glide/Coil 的优势变小了? 答:API 21+ 起 Bitmap 像素本身已分配在 native 内存,Fresco 用 CloseableReference 绕开 Dalvik 堆的核心卖点被系统吸收;而 Glide/Coil 更轻、Compose 更友好。
  5. 什么情况下应该「不用库、手动 ImageDecoder 解码」? 答:来源不是标准 URL/文件(加密文件、私有协议、需自定义采样/色彩空间),或要在特定线程精确控制解码;此时仍需自己接缓存与生命周期。

速记 ​

  • 五类方式:Glide / Coil / Fresco / Compose 原生(painterResource、AsyncImage)/ 手动(ImageDecoder、BitmapFactory)。
  • 共同骨架:取字节 → 解码(降采样)→ 写缓存 → 绑定生命周期绘制。
  • Glide:四级查找 + BitmapPool(inBitmap) + 隐藏 Fragment 管生命周期。
  • Coil:ImageLoader 单例 + Interceptor 链(内存→磁盘→取→解码→回写)+ 协程 + AsyncImagePainter 状态机。
  • Fresco:Drawee(UI) + ImagePipeline(producers 链) + CloseableReference(native 内存,引用计数)。
  • 库 vs 手动:除非来源特殊,否则用库,别自己重写缓存/取消。

站点构建时间:2026/8/24 23:43:17