Flutter 长图列表不卡顿的三个取舍:缓存、预加载与内存上限
做图片浏览类应用时,最容易被低估的一点是:列表滑起来卡,往往不是某一帧写得慢,而是内存、解码和网络请求在同一时间撞在了一起。用户快速滑动时,屏幕上要显示新图、后台要下载下一批图、旧图还要占着内存,任何一环没有上限,都会变成卡顿甚至闪退。
我后来处理这类问题的方法,是先把目标定下来:滑动时不掉帧、内存有明确上限、弱网下也不至于一片空白。在这个目标下,缓存、预加载和内存上限要一起设计,而不是各做各的。
一、缓存要分层,不要只留一层
很多人只用内存缓存,觉得够快就行。但内存缓存的问题是容量有限,一旦被系统回收,重新解码和重新下载的代价都很大。更稳的做法是分三层:内存缓存管当前可见区域和临近区域,磁盘缓存管最近看过的内容,网络只作为最后的兜底。
分层的关键是每一层都有自己的淘汰规则。内存层按张数和字节数双重限制,磁盘层按总大小和保存时间限制,超过就按最近最少使用淘汰。没有淘汰规则的缓存,本质上就是内存泄漏的另一种写法。还要注意,缓存的键要稳定,不能把临时地址、带签名的链接直接当作键,否则同一张图会被反复存成多份。
二、预加载不是越多越好
预加载很容易走向两个极端:不预加载,翻页就等图;预加载太多,后台请求把当前页的带宽和解码资源都抢走了。我的经验是,预加载只做“下一步最可能用到的”,而不是“后面所有可能用到的”。
具体来说,正在看第 N 页时,预加载 N+1 页就够了,最多再带上 N+2 页的低优先级请求。滑动方向变了,旧的预加载任务要能取消,而不是继续占着队列。还要区分网络环境:在 Wi-Fi 下可以适当放宽,在移动网络下要收紧,甚至只预加载缩略图,等用户真正翻到那一页再拉原图。
三、给内存设一个硬上限
Flutter 自带的图片缓存默认并不小,如果不主动设上限,长列表很容易在低端机上先把内存吃满。可以主动设置图片缓存的张数和字节上限,例如:
final imageCache = PaintingBinding.instance.imageCache;
imageCache.maximumSize = 100;
imageCache.maximumSizeBytes = 100 << 20;
数字本身没有标准答案,要按机型和图片尺寸调。更重要的是,上限要和业务行为联动:离开阅读页时清理不在视野内的图片,收到系统内存警告时先降级而不是硬扛。很多闪退不是因为某一刻内存特别高,而是因为峰值没有被管住。
四、解码尺寸要和显示尺寸匹配
另一个常见浪费,是用原图尺寸去解码,再缩放到屏幕上显示。一张几千像素的图片,解码后的内存占用远比文件大小大得多。如果显示区域只有几百像素,就应该按显示尺寸请求或解码对应规格的图片。
在列表里还要避免重复解码。滚动时同一张图如果反复进出视野,缓存命中率比解码速度更重要。先保证缓存键、尺寸规格一致,再去谈解码线程和并发数,否则优化很容易做在错误的地方。
五、失败时要有降级,而不是空白
网络请求一定会失败,缓存也一定会有 miss。好的体验不是“永远不失败”,而是失败时有明确的降级路径:先显示占位图或低清图,失败后允许用户点一下重试,连续失败时不要无限重试。
降级也要分层:单张图失败只影响那一张,不要让整个列表停住;整批请求失败时,给出可重试的入口,而不是静默地停在空白页。这些细节写起来不难,却是用户感知最明显的地方。
结尾:先定上限,再谈优化
总结一下我的检查清单:内存和磁盘缓存有没有淘汰规则;预加载是不是只覆盖下一步、且能取消;图片缓存有没有硬上限;解码尺寸是不是按显示尺寸来;失败时有没有单张重试和低清降级。
把这五条先做到,多数长图列表的卡顿都会明显缓解。剩下的,才是值得花时间去 profiling 的个别瓶颈,而不是一上来就到处加缓存、加并发。
- 点赞
- 收藏
- 关注作者
评论(0)