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. 先抽象一下:图片加载本质在干什么
不管用哪套库,图片加载都可以抽象成:
- 设置图片地址(URL / Uri)
- 请求网络获取图片字节
- 将字节解析成可渲染的图像对象(例如 Android 的 Bitmap 或类似封装)
- 交给 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、并主动把结果灌回DraweeHierarchy;ImagePipeline提供数据获取能力。 - 之所以不是典型 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 的
Uri(setImageURI(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);
}
当有结果时,核心是两步:
createDrawable(image):把图片对象转成DrawablemSettableDraweeHierarchy.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),把它转换成最终能塞进DraweeHierarchy的Drawable。
接下来这段代码容易让人困惑:为什么有 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,本质上还是一个ImageViewDraweeHierarchy: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());
}
因此 Controller 在 onNewResultInternal 里调用 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"套路能把缓存/解码/网络解耦并复用?