Fresco 图片加载全链路解析:从 SimpleDraweeView 到 Producer 责任链

Fresco 图片加载全链路解析:从 SimpleDraweeView 到 Producer 责任链

Fresco 的图片加载可以拆成两条链路:

  • UI 链路:SimpleDraweeView 如何把一行 setImageURI 变成最终的 Drawable 展示
  • 数据链路:ImagePipeline 如何通过 Producer/Consumer 责任链拿到"可展示的图片数据"

适用场景:排查"为什么这张图没显示/为什么重复请求/为什么没走缓存/为什么线程不对",并能把问题定位到具体节点和数据流向。


摘要(先看结论)

  • Fresco 把"展示"和"取数"分开:SimpleDraweeView 负责展示,PipelineDraweeController 负责调度,ImagePipeline 负责产出图片数据。
  • PipelineDraweeController.submitRequest() 的核心是订阅 DataSource:数据到了就 createDrawable(image),然后塞进 DraweeHierarchy
  • ImagePipeline.fetchDecodedImage() 返回的是 DataSource<CloseableReference<CloseableImage>>:也就是"已经解码好、可用于展示"的图片对象。
  • Fresco 的图片 pipeline 用 Producer/Consumer 责任链把步骤拆开复用:内存缓存、磁盘缓存、网络、解码、变换......都变成可组合的"积木"。

快速导航(按问题定位):

你遇到的问题 优先看哪条链路 关键类 关键方法 最有效的观测点
不显示/占位图一直不变 UI 链路 PipelineDraweeController / DraweeHierarchy submitRequest() / onNewResultInternal() / setImage(...) 是否拿到 DataSource;是否回调 onNewResult;是否成功 createDrawable
总是重复请求/列表滑动抖动 数据链路 ImagePipeline / Producer fetchDecodedImage() / produceResults(...) 是否出现 Multiplex;是否命中内存/磁盘缓存;是否每次都走 NetworkFetch
明明请求成功但没走缓存 数据链路 Memory/Disk Cache Producer produceResults(...) producer 列表里是否出现 cache read/write;命中时是否短路返回
线程不对/卡顿/主线程重活 UI + 数据 ThreadHandoffProducer / Controller 回调线程 subscribe(...) Producer 是否切线程;回调是否在 UI executor;是否把解码/变换压到主线程

1. 先抽象一下:图片加载本质在干什么

不管用哪套库,图片加载都可以抽象成:

  1. 设置图片地址(URL / Uri)
  2. 请求网络获取图片字节
  3. 将字节解析成可渲染的图像对象(例如 Android 的 Bitmap 或类似封装)
  4. 交给 View/Drawable 展示

现实中的复杂度主要来自两类需求:

  • 缓存:内存/磁盘多级缓存,以及"写入缓存"的时机
  • 变换:缩放、旋转、格式转换(webp/heif 等)、渐进式图片、占位图/失败图

2. Fresco 的整体分层:用 MVP 把展示和取数解耦

Fresco 的经典抽象可以用 MVP 来理解:

  • SimpleDraweeView:只负责展示(相当于 V)
  • PipelineDraweeController:图片加载控制器,负责调度与生命周期(相当于 P)
  • ImagePipeline:负责提供图片数据(相当于 M)

好处是"取数逻辑"可以脱离 View 复用。例如预加载场景不需要 View,也能直接跑 ImagePipeline 拿到数据塞进缓存。

2.1 Fresco 更像 MVP,而不是 MVC/MVVM

先用三张图把 MVC/MVP/MVVM 的"参与者关系"快速捡起来(不同资料会有细微变体,这里取最常见的语义):

MVC:

text 复制代码
User -> Controller -> Model
             |        |
             v        v
            View <----+

MVP(被动 View):

text 复制代码
User -> View <--> Presenter <--> Model

MVVM(数据绑定):

text 复制代码
User -> View <==binding==> ViewModel <--> Model
  • 如果硬要套经典模式,它更接近 MVP 的"被动 View":SimpleDraweeView 本身不拉取数据;PipelineDraweeController 订阅数据源、拼装 Drawable、并主动把结果灌回 DraweeHierarchyImagePipeline 提供数据获取能力。
  • 之所以不是典型 MVC:MVC 往往强调 View 直接观察/读取 Model(或至少 View 与 Model 的耦合更强),Controller 更多处理输入与协调;但 Fresco 的 View 基本只负责展示,核心调度与状态机都在 Controller 内部。
  • MVVM 的"VM"通常持有可观察状态(LiveData/Flow)并通过数据绑定驱动 UI;Fresco 没有把"可观察状态 + 绑定"做成框架层能力,它用的是 DataSource 回调/订阅 + Controller 内部状态机,所以直觉上更像 MVP,而不是 MVVM。

3. UI 链路:从 setImageURI 到真正显示发生了什么

3.1 一行代码如何触发加载

典型业务代码:

kotlin 复制代码
val uri = Uri.parse(model.albumUrl)
coverView.setImageURI(uri)

补充:能不能直接传 URL 字符串?

  • Fresco 对外的核心入参是 Android 的 UrisetImageURI(Uri ...)),因为它需要统一表达 http(s) 网络图、本地文件、本地 contentUri、资源图等多种来源。
  • 部分版本或业务封装可能会提供 setImageURI(String) 之类的便捷重载;即使有,本质也只是内部做了一次 Uri.parse(urlString)。如果你当前代码里没有这个重载,那就需要自己转成 Uri

补充:URL 和 URI 的区别是什么?

  • URI(Uniform Resource Identifier)是"标识符",语义更宽;URL(Uniform Resource Locator)是 URI 的子集,强调"可定位访问的地址",最常见就是 https://...
  • 在 Android/Fresco 语境里,用 Uri 的价值是:同一套 API 能同时表达多种 scheme,例如:
    • 网络:https://example.com/a.webp
    • 本地文件:file:///sdcard/a.jpg
    • ContentProvider:content://media/external/images/media/123
    • 资源(Fresco 约定):res:///2131230890

SimpleDraweeView#setImageURI 内部会构建一个 DraweeController,然后 setController(controller)

java 复制代码
// SimpleDraweeView.java(简化)
public void setImageURI(Uri uri, @Nullable Object callerContext) {
  DraweeController controller =
      mControllerBuilder.get()
          .setCallerContext(callerContext)
          .setUri(uri)
          .setOldController(getController())
          .build();
  setController(controller);
}

这里的 DraweeController 是一个更上层的接口/抽象类型,PipelineDraweeController 是它在"走 ImagePipeline 的那条实现":

  • SimpleDraweeView 面向的是 DraweeController(接口),这样它不被某个具体实现绑死
  • PipelineDraweeControllerBuilder.build() 产出的具体对象通常就是 PipelineDraweeController(但用父类型 DraweeController 接住)
  • 所以你在代码里看到的是"声明类型"是 DraweeController,但"运行时真实类型"是 PipelineDraweeController

setController(controller) 是怎么触发真正的图片请求的?

  • setController 本身更多是"绑定 + 生命周期接管":把 controller 交给 DraweeHolder 管起来
  • 真正的 submitRequest() 通常发生在 controller 被 attach 的时刻(View attach 到窗口/变为可见的生命周期节点)
  • 粗略链路可以理解为:
text 复制代码
SimpleDraweeView.setController(controller)
  -> DraweeHolder.setController(controller)
    -> (如果当前已 attach) controller.onAttach()
      -> submitRequest() / subscribe(DataSource)

另外,创建 controller 时为什么要 setOldController(getController())

  • 这是一个典型的"复用优化"入口:把旧 controller 传进去,让 Builder 有机会复用/继承一部分状态与资源(例如层级、监听器、手势/动画相关对象等),减少频繁创建对象带来的开销与列表场景抖动
  • 即使传了 oldController,Builder 也不一定 100% 复用,更多是"能复用则复用,不行就新建",对业务侧通常是无感的

到这里可以先记住一句话:

  • SimpleDraweeView 不自己拉网络;它把"我要什么图"交给 Controller,自己只负责展示结果。

3.2 Controller 如何把数据塞回 View

PipelineDraweeController 的关键步骤是 submitRequest():它会拿到一个 DataSource,然后订阅。

java 复制代码
// AbstractDraweeController.java(片段)
protected void submitRequest() {
  mDataSource = getDataSource();
  final DataSubscriber<T> dataSubscriber = new BaseDataSubscriber<T>() {
    @Override
    public void onNewResultImpl(DataSource<T> dataSource) {
      boolean isFinished = dataSource.isFinished();
      float progress = dataSource.getProgress();
      T image = dataSource.getResult();
      onNewResultInternal(id, dataSource, image, progress, isFinished, wasImmediate, hasMultipleResults);
    }
  };
  mDataSource.subscribe(dataSubscriber, mUiThreadImmediateExecutor);
}

当有结果时,核心是两步:

  1. createDrawable(image):把图片对象转成 Drawable
  2. mSettableDraweeHierarchy.setImage(drawable, ...):把 drawable 塞进层级里,最终更新到 View
java 复制代码
private void onNewResultInternal(...) {
  Drawable drawable = createDrawable(image);
  mSettableDraweeHierarchy.setImage(drawable, 1f, wasImmediate);
}

所以从 UI 视角看,Controller 就像一个"桥":

text 复制代码
SimpleDraweeView  --(创建/绑定)-->  Controller  --(订阅)-->  DataSource
        ^                                                     |
        |                                                     v
        +--------------------(setImage Drawable)----  DraweeHierarchy

4. 数据链路:DataSource 到底从哪来

上面 UI 链路里我们已经看到:Controller 会 subscribe(DataSource),等结果到了再 createDrawable(...)。这一节要回答的就是:这个 DataSource 到底是谁创建的、又是谁在往里"产出结果"。

PipelineDraweeController 来说,getDataSource() 这一步可以按"先拿到一个可订阅的异步句柄 → 再让 pipeline 去填充它"来理解:

  • mDataSourceSupplier.get():为"这一次图片加载"创建一个新的 DataSource(可以把它当成一次请求的句柄/收件箱),避免不同请求共用同一个结果通道。
  • ImagePipeline.fetchDecodedImage(...):真正去执行"查缓存/走网络/解码/变换"等流程,并把中间进度与最终结果写回到这个 DataSource 里。

为什么方法名叫 fetchDecodedImage?encoded / decoded 的区别是什么?

  • encoded image:还在"压缩文件/字节流"形态的图片数据,例如 JPEG/PNG/WebP 的原始 bytes(Fresco 里常见对应类型是 EncodedImage)。它适合存磁盘/做网络传输,但不能直接画到屏幕
  • decoded image:把 encoded bytes 解码成"可渲染的像素数据/帧序列"之后的形态(Fresco 里最终会抽象成 CloseableImage,里面可能持有 Bitmap 或动图帧等)。这一步会占用更多内存与 CPU,但结果可以直接转成 Drawable 展示
  • 所以 fetchDecodedImage 这个名字强调的是:这条 API 承诺给你的是"已经解码好、可以用来展示"的结果;如果只需要 encoded 数据(比如纯预热磁盘缓存、或你自己要做其他解码/处理),走的通常会是另一条取 encoded 的接口与 producer 链。

4.1 如何读懂 DataSource<CloseableReference<CloseableImage>>

  • DataSource<T>:Fresco 自己的一套异步结果抽象,除了"最终结果"外,还能表达进度(getProgress())、失败(getFailureCause())、取消、以及"中间结果/多次回调"(例如渐进式图片或先低清后高清)。
  • 这里的 T 不是"两个泛型",只是 T = CloseableReference<CloseableImage>,也就是 DataSource 的结果类型本身又是一个泛型容器。
  • CloseableReference<X>:可以把它理解成"带引用计数的智能指针"。它的核心价值是管理可能占用大量 native 内存/池化内存 的对象生命周期:谁拿到引用谁负责 close(),引用计数归零时底层资源才真正释放/回收到池里。它确实能减少"忘记释放导致的内存泄露/爆内存"风险,但更准确说是强制显式管理,而不是靠 GC 碰运气。
  • CloseableImage:解码后的"可展示图片"抽象父类(实现了 Closeable),不等于 Bitmap 本身。常见实现会包装静态 Bitmap(以及旋转角度、质量信息等元数据)或动图帧序列等。Controller 在拿到 CloseableImage 后会再 createDrawable(image),把它转换成最终能塞进 DraweeHierarchyDrawable

接下来这段代码容易让人困惑:为什么有 getDataSource(),又有一个 getDataSourceForRequest(...)?它们之间不是"并列"的两个入口,而是"外层入口 + 内层实现"的关系。

java 复制代码
// PipelineDraweeController.java(片段)
@Override
public DataSource<CloseableReference<CloseableImage>> getDataSource() {
  return mDataSourceSupplier.get();
}

@Override
protected DataSource<CloseableReference<CloseableImage>> getDataSourceForRequest(...) {
  return mImagePipeline.fetchDecodedImage(
      imageRequest,
      callerContext,
      convertCacheLevelToRequestLevel(cacheLevel),
      getRequestListener(controller),
      Priority.getHigherPriority(Priority.HIGH,
          imageRequest != null ? imageRequest.getPriority() : Priority.HIGH));
}

它们的调用关系可以按下面的"套娃"来理解:

text 复制代码
submitRequest()
  -> getDataSource()                    // 对外:给我一个 DataSource(我马上就订阅它)
    -> mDataSourceSupplier.get()        // 延迟创建:这一刻才真正"创建本次请求的数据源"
      -> getDataSourceForRequest(...)   // 对内:用当前 request 参数拼出真正的 DataSource
        -> ImagePipeline.fetchDecodedImage(...)

为什么要拆成两层,而不是在 getDataSource() 里直接写死 fetchDecodedImage(...)

  • getDataSource() 的语义很稳定:Controller 只需要"拿到一个可订阅的 DataSource",并不关心里面到底是走 decoded 还是 encoded、走网络还是本地。
  • 具体"怎么拼 request 参数、怎么选 pipeline 接口"这件事留给 getDataSourceForRequest(...):它更像"把本次请求需要的参数都填齐"的地方。
  • mDataSourceSupplier 的存在让创建变成"按需":Controller attach 时才去创建 DataSource,也方便做取消/重试/复用旧 controller 等操作。

这里有两个关键词:

  • Supplier<T>:Fresco 里很多 xxxSupplier 是"抽象工厂",负责按需构建数据源/序列
  • fetchDecodedImage:直接拿"解码后的图片",也就是最终可用于展示的结果类型 CloseableImage

5. 为什么 Fresco 不把"缓存/网络/解码"写成一坨 if-else

如果你自己写一个"能工作"的图片加载,通常会这么想:

text 复制代码
内存缓存命中?
  命中 -> 直接返回
磁盘缓存命中?
  命中 -> 解码 -> 写内存 -> 返回
否则:
  拉网络 -> 写磁盘 -> 解码 -> 写内存 -> 返回

这种写法的问题是:

  • 强耦合:缓存、网络、解码交织在一起,难以替换其中一步
  • 难复用:不同场景(预加载/只要 encoded/只要 decoded/是否 resize)需要不同排列组合
  • 难并发:同一个 URL 多处请求时如何合并复用结果,代码会迅速膨胀

Fresco 的解法是把每一步拆成"积木",然后用责任链把积木串起来。


6. Producer/Consumer:Fresco 的责任链"积木系统"

6.1 Producer 接口:每一步就是一个 task

Fresco 用 Producer<T> 表达某一个步骤,处理完通过 Consumer 回调结果:

java 复制代码
public interface Producer<T> {
  void produceResults(Consumer<T> consumer, ProducerContext context);
}

它把一次图片请求拆成多个可复用步骤,例如:

  • network fetch
  • disk caching
  • memory caching
  • decoding
  • applying transformations

6.2 两种责任链思路:Interceptor 栈 vs Producer 链表

责任链(Chain of Responsibility)本质是在做同一件事:把一次"请求"拆成一串步骤,让每一步都可以决定:

  • 自己处理完就结束(短路)
  • 或者把请求交给下一个节点继续处理

常见实现大体有两种,分别对应 OkHttp 和 Fresco 的风格。

方式一:List/栈式(OkHttp Interceptor 风格)

特点:链是一个 List,调用时按顺序"套娃"执行;每个拦截器拿到同一个 Chain,要么自己返回结果,要么调用 chain.proceed(request) 交给下一个。

kotlin 复制代码
interface Interceptor {
  fun intercept(chain: Chain): Response
}

class CacheInterceptor : Interceptor {
  override fun intercept(chain: Chain): Response {
    val cached = memoryCache.get(chain.request)
    if (cached != null) return cached
    val resp = chain.proceed(chain.request)
    memoryCache.put(chain.request, resp)
    return resp
  }
}

这里的"套娃"发生在运行时:OkHttp 通常是把拦截器收集到一个 List 里,然后由 Chain 这个对象用"索引递增"的方式驱动下一个拦截器,并不是你用 A(B(C())) 的方式把它们嵌起来。

kotlin 复制代码
val client =
  OkHttpClient.Builder()
    .addInterceptor(CacheInterceptor())
    .addInterceptor(OtherInterceptor())
    .build()

可以粗暴理解为:

text 复制代码
proceed(i):
  interceptor[i].intercept(chain(i + 1))

优点:同步语义直观、调用栈清晰、实现成本低。缺点:天然更适合"单线程 + 单结果 + 同一种返回类型"的场景;要做复杂异步、进度、多次结果、跨线程切换,写起来会越来越绕。

方式二:链表式(Fresco Producer/Consumer 风格)

特点:每个 Producer 持有下游 inputProducer。上游不是"返回一个结果",而是把 consumer 传下去,让下游在异步完成后通过回调把结果一路往上游传。

kotlin 复制代码
interface Consumer<T> {
  fun onNewResult(result: T, isLast: Boolean)
  fun onFailure(t: Throwable)
}

data class ProducerContext(val key: String)

interface Producer<T> {
  fun produceResults(consumer: Consumer<T>, context: ProducerContext)
}

data class Image(val bytes: ByteArray)

class NetworkFetchProducer : Producer<ByteArray> {
  override fun produceResults(consumer: Consumer<ByteArray>, context: ProducerContext) {
    consumer.onNewResult(ByteArray(0), isLast = true)
  }
}

class DecodeProducer(
  private val inputProducer: Producer<ByteArray>,
) : Producer<Image> {
  override fun produceResults(consumer: Consumer<Image>, context: ProducerContext) {
    val wrapped = object : Consumer<ByteArray> {
      override fun onNewResult(result: ByteArray, isLast: Boolean) {
        consumer.onNewResult(Image(result), isLast)
      }

      override fun onFailure(t: Throwable) {
        consumer.onFailure(t)
      }
    }
    inputProducer.produceResults(wrapped, context)
  }
}

class ResizeProducer(
  private val inputProducer: Producer<Image>,
) : Producer<Image> {
  override fun produceResults(consumer: Consumer<Image>, context: ProducerContext) {
    inputProducer.produceResults(consumer, context)
  }
}

val chain: Producer<Image> = ResizeProducer(DecodeProducer(NetworkFetchProducer()))

chain.produceResults(consumer, ProducerContext(key = "url"))

这里写成 ResizeProducer(DecodeProducer(NetworkFetchProducer())) 只是为了把"每个节点持有下游节点"的 wiring 画出来;Fresco 真实实现里通常是由 ProducerSequenceFactory 按请求类型把一串 Producer 组装好。

优点:天然适配图片加载这种"异步 + 进度 + 可能多次结果(渐进式)+ 需要切线程 + 同一 URL 多处复用(Multiplex)"的场景;每个节点都可以独立复用/组合。缺点:控制流不再是直观的同步调用栈,调试时需要靠日志/trace/producerList 来还原路径;consumer 包装层级深时心智负担更高。

为什么都是责任链,但 Fresco 和 OkHttp 要做成两种形态?

  • OkHttp 的核心是"一次请求返回一个 Response",拦截器对同一 Request/Response 做加工,最匹配同步的 proceed() 套娃。
  • Fresco 的核心是"图片加载是一个过程":可能先进度、再中间结果、再最终结果;并且每一步可能在不同线程,还要做缓存短路与请求合并复用。用 Producer/Consumer 回调链更容易表达这些语义。

理解了链的"形态"之后,下面用一个最常见的节点(内存缓存)看看 Fresco 是怎么做"短路 + 包装 consumer"的。

6.3 经典套路:缓存 Producer 的"短路 + 包装 Consumer"

以内存缓存为例,它的套路非常典型:

  • 如果缓存命中:直接 consumer.onNewResult(...) 返回,链路短路
  • 如果缓存不命中:创建一个"包装后的 consumer",用于在下游返回结果时写缓存,然后再把结果回传给上游 consumer
  • 最后调用 mInputProducer.produceResults(wrappedConsumer, context) 驱动下一个节点
java 复制代码
// BitmapMemoryCacheProducer.java(片段)
@Override
public void produceResults(
    final Consumer<CloseableReference<CloseableImage>> consumer,
    final ProducerContext producerContext) {

  CloseableReference<CloseableImage> cachedReference = mMemoryCache.get(cacheKey);
  if (cachedReference != null) {
    consumer.onNewResult(cachedReference, BaseConsumer.simpleStatusForIsLast(isFinal));
    cachedReference.close();
    return;
  }

  Consumer<CloseableReference<CloseableImage>> wrappedConsumer =
      wrapConsumer(consumer, cacheKey, producerContext.getImageRequest().isMemoryCacheEnabled());

  mInputProducer.produceResults(wrappedConsumer, producerContext);
}

这个模式非常值得记住,因为你在看其他 Producer 时会不断遇到同一种结构。


7. fetchDecodedImage:如何拼出一条"拿到解码图片"的链

ImagePipeline.fetchDecodedImage(...) 开始:

java 复制代码
public DataSource<CloseableReference<CloseableImage>> fetchDecodedImage(...) {
  Producer<CloseableReference<CloseableImage>> producerSequence =
      mProducerSequenceFactory.getDecodedImageProducerSequence(imageRequest);
  return submitFetchRequest(producerSequence, ...);
}

ProducerSequenceFactory 会根据 Uri 类型选择不同的 sequence,例如网络、本地文件、本地 contentUri 等:

java 复制代码
public Producer<CloseableReference<CloseableImage>> getDecodedImageProducerSequence(ImageRequest imageRequest) {
  return getBasicDecodedImageSequence(imageRequest);
}

private Producer<CloseableReference<CloseableImage>> getBasicDecodedImageSequence(ImageRequest imageRequest) {
  switch (imageRequest.getSourceUriType()) {
    case SOURCE_TYPE_NETWORK:
      return getNetworkFetchSequence();
    case SOURCE_TYPE_LOCAL_IMAGE_FILE:
      return getLocalImageFileFetchSequence();
    default:
      throw new IllegalArgumentException("Unsupported uri scheme!");
  }
}

对于网络图片,Fresco 会把"从网络拿到 encoded 数据"以及"把 encoded 解码成可展示数据"两段拼起来:

  • 一段产出 EncodedImage(编码态图片数据)
  • 一段把 EncodedImage -> CloseableImage(解码态图片数据)

完整链条很长,下面给出一个典型的链表关系(按箭头表示从上游到下游):

text 复制代码
BitmapMemoryCacheGetProducer
  -> ThreadHandoffProducer
  -> BitmapMemoryCacheKeyMultiplexProducer
  -> BitmapMemoryCacheProducer
  -> DecodeProducer
  -> ResizeAndRotateProducer
  -> AddImageTransformMetaDataProducer
  -> EncodedCacheKeyMultiplexProducer
  -> EncodedMemoryCacheProducer
  -> DiskCacheReadProducer
  -> DiskCacheWriteProducer
  -> NetworkFetchProducer

如果只看类名确实很难读,最有效的方法是:先把它们按"在干什么"分组,再回头看链路顺序。

节点 一句话作用(直觉版) 常见归类
BitmapMemoryCacheGetProducer 先试着从"解码后的 bitmap 内存缓存"直接拿结果,命中就短路返回 decoded 侧:内存命中与短路
ThreadHandoffProducer 把后续工作切到合适的后台线程执行,避免在 UI 线程做重活 线程切换
BitmapMemoryCacheKeyMultiplexProducer 相同 key 的"解码图请求"合并成一次执行,多处订阅共享同一份结果 复用/去重(Multiplex)
BitmapMemoryCacheProducer 如果最终拿到了 decoded 结果,把它写回"解码后的 bitmap 内存缓存" decoded 侧:写回缓存
DecodeProducer 把 encoded bytes 解码成可渲染的图片对象(decoded) decoded 侧:解码
ResizeAndRotateProducer 按请求做缩放/旋转等基础变换(有时在解码过程中完成) decoded 侧:变换
AddImageTransformMetaDataProducer 把变换相关的元信息补齐/透传给后续步骤(例如旋转角等) decoded 侧:元信息
EncodedCacheKeyMultiplexProducer 相同 key 的"编码态请求"(磁盘/网络)合并执行,多处订阅共享下载/读盘结果 复用/去重(Multiplex)
EncodedMemoryCacheProducer 先试 encoded 内存缓存,命中就不用走磁盘/网络 encoded 侧:内存缓存
DiskCacheReadProducer 读磁盘缓存(通常读到的是 encoded bytes) encoded 侧:磁盘读
DiskCacheWriteProducer 把网络拿到的 encoded bytes 写入磁盘缓存,供下次复用 encoded 侧:磁盘写
NetworkFetchProducer 真正的网络下载,把图片 bytes 拉回来 encoded 侧:网络

读这条链最有效的方法是"按职责分段":

  • decoded 侧(离 UI 更近):Bitmap memory cache、Decode、Resize/Rotate
  • encoded 侧(离网络更近):Encoded memory cache、Disk cache read/write、Network fetch
  • 复用/线程:Multiplex(请求合并复用)、ThreadHandoff(切线程)

8. 展示链路:为什么 SimpleDraweeView 能显示 Drawable

先把关系说清楚:

  • SimpleDraweeView:最终承载在屏幕上的 View,本质上还是一个 ImageView
  • DraweeHierarchy:Fresco 给"一个图片位要怎么画"抽出来的独立对象,可以理解成一套可配置的 Drawable 层级(占位图/失败图/进度条/实际图片/overlay 等)
  • SimpleDraweeView 不是每次拿到新图就 setImageDrawable(newDrawable) 这么简单,而是把 DraweeHierarchy 的"最顶层 drawable"(TopLevelDrawable)一次性挂到 ImageView 上;后续图片状态变化只要更新 hierarchy 里的某一层即可

一张图理解它们的关系:

text 复制代码
SimpleDraweeView (ImageView)
  holds -> DraweeHolder
            holds -> DraweeHierarchy (一组层级 drawable)
                      topLevelDrawable -> set 到 ImageView 上

为什么要搞一个 DraweeHierarchy

  • 把"展示层的复杂度"从 View 里拿出来:占位图/失败图/进度条/淡入淡出/圆角/overlay 这些都是展示逻辑,不应该和"取数/调度"绑在一起
  • 让更新更稳定:View 上始终是同一个 TopLevelDrawable,真正变化发生在 hierarchy 内部(切占位/切失败/切真实图),减少 UI 抖动和重复 setDrawable 带来的问题
  • 让能力可组合:你可以通过 builder 配出一套统一的显示策略,Controller 只负责把"最终 drawable"塞进 hierarchy

对应到代码上:GenericDraweeView 初始化时构造 DraweeHierarchy,并把 hierarchy 的 TopLevelDrawable 设置给 ImageView

java 复制代码
// GenericDraweeView.java(片段)
protected void inflateHierarchy(Context context, @Nullable AttributeSet attrs) {
  GenericDraweeHierarchyBuilder builder =
      GenericDraweeHierarchyInflater.inflateBuilder(context, attrs);
  setHierarchy(builder.build());
}

// DraweeView.java(片段)
public void setHierarchy(DH hierarchy) {
  mDraweeHolder.setHierarchy(hierarchy);
  super.setImageDrawable(mDraweeHolder.getTopLevelDrawable());
}

因此 ControlleronNewResultInternal 里调用 mSettableDraweeHierarchy.setImage(drawable, ...) 时,本质是在更新这套层级 drawable,最终体现在 ImageView 上。


9. 把全链路串起来(最重要的一张图)

text 复制代码
业务代码
  |
  |  setImageURI(uri)
  v
SimpleDraweeView(展示)
  |
  |  build Controller + setController
  v
PipelineDraweeController(调度)
  |
  |  submitRequest()
  |    - getDataSource()
  |    - subscribe(DataSubscriber)
  v
DataSource(可订阅的数据源)
  |
  |  产出 decoded image
  v
ImagePipeline(取数)
  |
  |  fetchDecodedImage(imageRequest)
  v
ProducerSequence(责任链积木)
  |
  |  内存缓存 -> 磁盘缓存 -> 网络 -> 解码 -> 变换 -> ...
  v
CloseableImage(解码结果)
  |
  |  createDrawable(image)
  v
DraweeHierarchy.setImage(drawable)
  |
  v
ImageView 显示最终 Drawable

10. 实战:如何观测一张图到底走了哪些 Producer

打开调试开关后,可以在控制台看到 pipeline 的 producer 列表,例如:

bash 复制代码
consumer return success https://example.com/xxx.heif
DiskCacheWriteConsumer producerList:
  BitmapMemoryCacheGetProducer,
  ThreadHandoffProducer,
  BitmapMemoryCacheKeyMultiplexProducer,
  BitmapMemoryCacheProducer,
  DecodeProducer,
  ResizeAndRotateProducer,
  AddImageTransformMetaDataProducer,
  EncodedCacheKeyMultiplexProducer,
  EncodedMemoryCacheProducer,
  DiskCacheReadProducer,
  DiskCacheWriteProducer,
  NetworkFetchProducer

当你排查问题时,可以用这条日志回答三个关键问题:

  • 有没有走到 NetworkFetch(是否命中缓存)
  • Decode/Resize 是否执行(是否发生解码与变换)
  • Multiplex 是否出现(是否发生请求合并复用)

11. 你读懂 Fresco 的标志

当你能回答下面三个问题,就基本"懂了":

  • 为什么 Fresco 要把 View/Controller/Pipeline 分层?各自职责边界是什么?
  • 为什么 DataSource + 订阅能很好地承接"异步、多结果、进度、失败"的场景?
  • 为什么 Producer/Consumer 的"短路 + 包装 consumer"套路能把缓存/解码/网络解耦并复用?
相关推荐
雨白11 小时前
Android 快捷方式实战指南:静态、动态与固定快捷方式详解
android
hqk11 小时前
鸿蒙项目实战:手把手带你实现 WanAndroid 布局与交互
android·前端·harmonyos
LING12 小时前
RN容器启动优化实践
android·react native
恋猫de小郭14 小时前
Flutter 发布官方 Skills ,Flutter 在 AI 领域再添一助力
android·前端·flutter
Kapaseker20 小时前
一杯美式搞懂 Any、Unit、Nothing
android·kotlin
黄林晴20 小时前
你的 Android App 还没接 AI?Gemini API 接入全攻略
android
恋猫de小郭1 天前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
冬奇Lab1 天前
PowerManagerService(上):电源状态与WakeLock管理
android·源码阅读
BoomHe1 天前
Now in Android 架构模式全面分析
android·android jetpack
二流小码农2 天前
鸿蒙开发:上传一张参考图片便可实现页面功能
android·ios·harmonyos