返回上一页,图片为什么又闪了一下

返回上一页,图片为什么又闪了一下

一、先看现象

从列表点进详情,停一会儿再返回。卡片外壳还在,图片的位置先变成占位色块,再重新出现,闪一下。

第一反应通常是缓存没命中。但去查磁盘,文件一直在;去查网络,也没有重新请求。丢的其实是已经解码好的那一帧。

二、丢的是哪一帧

Flutter 的图片链路里有两样东西:缓存里的文件,和已经解码到内存里的那一帧。磁盘缓存只保证文件还在;屏幕上画的是解码后的 ui.Image。

详情页盖住列表以后,被挡住的这棵路由子树会冻结:TickerMode 被关掉。Flutter 的 Image 默认在 TickerMode 关闭后不再监听解码流,这一帧就从全局 ImageCache 的 live 集合里离开。详情页继续解码新图,很快把 LRU 挤掉。返回时如果组件先画占位、再等重新解码,就会闪一次空图。

返回时要画的是已经解码的帧,不能指望它还在全局缓存里。

三、让画图的人自己持有最后一帧

我们把“保帧”收进共享的图片入口,页面不再自己拼。

FurCachedImage 和 furCachedImageProvider 负责公开远程图:底层是 Image,打开 gaplessPlayback: true,再挂一个 frameBuilder。gaplessPlayback 让 TickerMode 关闭时继续 keepAlive 解码流;frameBuilder 里如果新帧还没到,但子节点 RawImage 里还有像素,就继续画子节点,不退回占位图。这段判断在 furBuildPersistentCachedImageFrame 里,测试也直接盯着它。

并不是所有图都从 URL 来。鉴权图、异步拿到的本机字节走 FurAsyncMemoryImage,按 cacheKey 留住字节,再交给开了 gaplessPlayback 的 Image.memory;表单和选择器的本地预览也统一带上 gaplessPlayback。头像在刷新 DTO 短暂缺 URL 时会自己留住上一张。需要描边的图在 State 里 clone 最后一帧 ui.Image,不另开一次页面级解码。

全局 ImageCache 只设容量上限:360 张、128MB。它的职责是别把内存撑爆,不负责保证某张图返回时还在。

平板双栏也是同一套机制:非活动窗格被 TickerMode 关掉,切回来同样不该重新解码,共享入口已经把帧留住了。

四、光写文档拦不住新代码

文档写在 IMAGE_DISPLAY_STANDARD.md,但人来人往,总有新页面直接写 Image。

所以规则也做成了架构测试:lib 里 Image.network、NetworkImage、CachedNetworkImage、CachedNetworkImageProvider 只允许出现在 fur_cached_image.dart;任何 Image、Image.memory、Image.file、Image.asset 调用都必须带 gaplessPlayback: true。行为测试则检查两件事:新帧没到但 RawImage 还有像素时保留子节点;清掉图片内存以后,正在展示的 live 图不掉。

五、这套修到什么为止

保帧不替代缓存。文件缓存、预热和驱逐照旧;下载和解码并发有上限;响应头最多等 20 秒,正文 12 秒没有新字节就结束这次传输并重试。信息流封面在展示档还没落到磁盘时,先画小一档。

它也不处理列表虚拟化。列表项滑出 cacheExtent 被卸载,滑回来重新解码,是另一条路径,和返回时的空图闪一下不是同一个问题。把它俩混成一个问题,容易越修越乱。

还有内存。被挡住的页面各自持有一帧解码后的位图,页面开得越多,这笔开销越大。保帧解决的是返回瞬间的画面连续性,最终还是要有页面生命周期和列表回收来确保内存占用可控。

六、收尾

图片缓存已经命中,不等于画面已经就绪。文件在磁盘上、字节在内存里、解码帧在屏幕上,是三件事。返回时出现空图闪一下,需要修的是第三件。

再遇到类似问题,可以先问一句:返回的那一刻,谁来画那张已经解码的图?

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享