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"套路能把缓存/解码/网络解耦并复用?
相关推荐
_周游2 小时前
Java8 API文档搜索引擎_7.项目优化之权重合并
java·开发语言·前端·搜索引擎·intellij-idea
专注VB编程开发20年2 小时前
c#.NET异步同小,ASYNC,AWAIT,PushFrame ,DOEVENTS
开发语言·.net
Asmewill2 小时前
Kotlin高阶函数
android
电商API_180079052472 小时前
淘宝商品详情数据获取全方案分享
开发语言·前端·javascript
maplewen.3 小时前
C++11 返回值优化
开发语言·c++·面试
我命由我123453 小时前
Android Studio - 在 Android Studio 中直观查看 Git 代码的更改
android·java·开发语言·git·java-ee·android studio·android jetpack
hewence13 小时前
Kotlin协程启动方式详解
android·开发语言·kotlin
城东米粉儿3 小时前
Android EventHub的Epoll原理 笔记
android
gihigo19983 小时前
MATLAB运动估计基本算法详解
开发语言·算法·matlab