Flutter 鸿蒙性能优化实战:图片列表中的 cacheWidth 与 precacheImage
Flutter 鸿蒙性能优化实战:图片列表中的 cacheWidth 与 precacheImage
图片列表的卡顿经常被归因于“图片太多”,但真正的变量通常是解码尺寸、同时解码数量和缓存策略。本文项目用一张本地示例图生成 40 个列表卡片,提供“限制解码宽度”开关,比较同一图片在展示尺寸附近解码和按原始尺寸解码的差异。
项目地址:flutter_ohos_image_cache_demo。核心列表项实现见实测环境后的代码片段。
一、实测环境和测量方式
| 项目 | 实测版本或条件 |
|---|---|
| Flutter OH | 3.44.9+ohos-0.0.1-canary1 |
| Dart | 3.12.2 |
| DevEco Studio | 26.0.0 Release |
| HarmonyOS | 7.0.0.105 (API 26) |
| 构建模式 | profile |
| 数据量 | 40 张本地示例图,缩略图 128 x 88 逻辑像素 |
图片尺寸、DPR 和滚动时长必须在两组对照中保持一致,否则无法把差异归因到解码尺寸。
Image.asset(
'assets/hero.jpeg',
width: 128,
height: 88,
fit: BoxFit.cover,
cacheWidth: boundedDecode ? 256 : null,
filterQuality: FilterQuality.low,
)
cacheWidth 影响解码缓存尺寸,不会改变布局尺寸。实际项目要考虑设备像素比,通常把目标逻辑宽度乘以 DPR 后再设置,而不是把一个固定数字复制到所有设备。展示一张 128 逻辑像素的缩略图,却解码成几千像素,会浪费内存和光栅时间。
首屏可以用 precacheImage 预热关键图片:
precacheImage(const AssetImage('assets/hero.jpeg'), context);
预热的代价是把工作提前到首屏,不能无条件对整组图片调用。我的做法是只预热首屏和用户马上会看到的资源,其余图片交给列表按需加载。网络图片还需要占位、失败重试和取消策略,本文没有把网络波动混入解码实验。
测试时建议同时记录滚动慢帧、图片缓存命中、进程 PSS 和 Dart/Native Heap。图片缓存命中率提高不代表内存一定更低;缓存太大反而会挤压其他资源。可以先用 DevTools Memory 找到增长来源,再用 HiDumper 看进程总量。
仓库已通过分析、Widget 测试和资源打包检查,assets/hero.jpeg 会随项目提交。本文把真机截图作为页面状态证据;要写出两种模式的性能改善幅度,还需要在同一台 HarmonyOS API 26 真机上完成多轮 profile 滚动采样,并隐藏设备编号和本机路径。
二、先看容易出问题的图片写法
1. 原图解码和展示尺寸不是一回事
图片显示宽度只有 128 逻辑像素时,如果仍然按原图几千像素解码,布局虽然没有变大,但解码、纹理上传和缓存占用都会按更大的尺寸发生。cacheWidth 只影响图片进入缓存时的解码尺寸,不会替你改变布局约束;所以我在代码里同时保留 width、height、fit,让展示尺寸和解码尺寸各自承担清晰的职责。
实际应用不能直接复制一个固定的 256 到所有设备。更稳妥的做法是根据目标展示宽度乘以设备 DPR 计算,并给上下限。低分辨率设备不需要解码到很大的像素,高密度屏幕也不能因为一个经验数字而出现明显模糊。图片源本身的压缩格式、透明通道和旋转信息也会影响最终内存。
2. 先用真机页面确认对照状态
本文 Demo 的两张真机图只用于确认页面状态:列表内容、开关状态和图片展示一致,不能单独证明解码耗时或内存下降。关闭限制解码宽度时,图片仍然使用相同资源和布局尺寸,变化点是 cacheWidth。
3. precacheImage 不是把所有图片提前加载
precacheImage 适合预热首屏确定会出现的少量资源。本 Demo 在依赖变化后预热同一张本地示例图,页面上显示完成次数,方便确认预热确实发生。它并不是“把 40 张图片都提前解码”的理由:预热会把 CPU、IO 和内存工作提前到首屏阶段,数量过大反而可能延迟首次可交互时间。
网络图片还要单独考虑加载失败、占位、取消和重试,不能把网络等待和本地解码耗时混在一个数字里。滚动时建议固定动作,先预热首屏,再记录图片缓存命中、慢帧和 PSS;否则一次刚好命中缓存、一次刚好发生解码,结果会非常不稳定。
4. 图片实验应该记录哪些证据
| 层面 | 工具 | 关注点 |
|---|---|---|
| Widget/布局 | Flutter DevTools | 列表按需创建和滚动中的构建 |
| 图片缓存 | DevTools Memory | 图片对象是否持续增长 |
| 进程总量 | HiDumper | PSS、Native Heap、图形缓冲 |
| 用户体验 | FrameTiming | 滚动慢帧和 Raster 时间 |
四项证据互相补充,不能只看缓存命中率,也不能用 Dart Heap 代替系统进程内存。
三、换成接近展示尺寸的解码方式
1. 把逻辑像素换算成解码像素
展示尺寸和解码尺寸不是同一个单位。假设缩略图宽度是 128 逻辑像素,设备 DPR 为 2,那么目标解码宽度可以从 128 * 2 开始,再根据资源清晰度设置上下限:
final targetWidth = (128 * MediaQuery.devicePixelRatioOf(context))
.round()
.clamp(128, 512)
.toInt();
Image.asset(
'assets/hero.jpeg',
width: 128,
height: 88,
cacheWidth: targetWidth,
fit: BoxFit.cover,
)
这里的上下限只是示例,不能替代设计稿和设备矩阵。过小会导致放大后模糊,过大又会把解码内存重新推高。若同一张图在多个尺寸使用,建议按用途建立明确的缩略图规格,而不是让每个页面随意设置一个数字。
2. 缓存命中不是唯一目标
图片缓存命中率高,说明复用成功,但不代表进程内存一定健康。缓存内容还受图片宽高、颜色通道和同时保留的数量影响。滚动列表中,最常见的错误是把所有图片都预热到缓存,结果首屏更慢、PSS 更高,且用户很可能永远不会看到列表末尾。
本文的对照只改变 cacheWidth,并保持图片源、卡片数量、滚动距离和 filterQuality 不变。正式项目还要把以下情况单独记录:切换账号后缓存是否需要清理,内存警告时是否主动缩减缓存,网络图片失败后是否重复解码,以及页面退出后是否仍有图片监听。
四、缓存、预热和生命周期边界
1. 建议的验收记录
| 阶段 | 必须固定的变量 | 输出证据 |
|---|---|---|
| 首屏 | 图片数量、DPR、预热数量 | 首帧时间、首屏截图 |
| 滚动 | 距离、持续时间、速度 | 慢帧、Raster 峰值 |
| 稳定后 | 等待时间、后台负载 | PSS、图片对象数量 |
只有三阶段都记录,才能判断优化是减少了解码成本,还是单纯把工作延后了。
图片缓存的验收还要包含清理路径:切换账号、退出页面、收到系统内存压力时,确认旧资源不会继续被业务引用。缓存策略有效的标志不是缓存数量越多,而是在目标设备和真实滚动动作下,清晰度、首帧、慢帧和内存都在可接受范围内。
2. 列表图片的生命周期
图片条目进入 viewport 时可能经历请求、解码、上传纹理和绘制四个阶段。快速滚动时,旧条目离开 viewport 并不等于缓存中的图片立刻释放;如果图片仍被其他组件引用,内存会继续保留。反过来,如果缓存太小,用户来回滚动会重复解码,同样会造成 Raster 峰值。
因此图片列表要同时考虑三个窗口:列表的 cacheExtent、图片的内存缓存和网络层的请求缓存。三个窗口都设得很大时,首帧和 PSS 会同时变重;三个窗口都设得很小时,滚动会出现重复加载。建议先根据卡片尺寸确定 cacheWidth,再根据设备内存和滚动速度调缓存数量,最后才考虑网络预取。
3. 首帧预热的边界
precacheImage 适合首屏确定会显示的少量图片,不适合拿来代替列表懒加载。首帧之前预热会增加首帧耗时,首帧之后预热会与首次交互竞争 CPU 和 IO。可以让首屏先显示低分辨率占位,页面稳定后在空闲时预热下一屏,并在页面退出时忽略已经无效的 Future。
对于网络图片,还要给失败和取消留出路径。用户快速滑过一张图片时,如果请求仍然继续并在返回后更新已回收的条目,既浪费带宽,也可能触发无效重建。图片 provider、列表 key 和请求标识要保持一致,避免同一资源被多个条目重复下载。
4. 设备矩阵和视觉验收
图片优化不能只在一台高端设备上确认。至少应覆盖一个低内存设备、一个高 DPR 设备和一个较高刷新率设备,记录相同图片在不同 DPR 下的目标解码宽度。视觉验收还要检查文字边缘、圆角裁剪、透明通道和深色模式;FilterQuality.low 带来的 Raster 收益不能以明显锯齿为代价。
五、真机结果如何记录
1. 结果记录模板
每组测试保存原图宽高、展示宽高、DPR、cacheWidth、预热数量、首帧耗时、首次解码耗时、滚动慢帧和稳定 PSS。若只完成了页面截图,就把结论限定为“功能状态已复现”;只有完成 profile 多次采样,才能写图片解码对性能的改善幅度。
六、复现命令和测试范围
flutter pub get
flutter analyze
flutter test
flutter build hap --debug --no-codesign
flutter run --profile -d <ohos-device-id>
进入页面后分别打开、关闭“限制解码宽度”,执行相同滚动动作并保存截图、日志和内存采样。文章中的截图证明页面状态,性能结论仍应来自重复采样。
七、这次实验的实际结论
cacheWidth 解决的是解码尺寸和图片缓存占用问题,precacheImage 解决的是确定资源的提前准备问题,二者不能互相替代。当前项目的真机截图证明了两种页面状态可以复现,但没有提供足够的 profile 多轮数据,因此不把截图写成“所有设备都更快”。
迁移到业务时,先按展示尺寸确定解码像素,再决定是否预热;同时固定设备 DPR、图片源和滚动动作,记录首次解码、慢帧、Raster 和 PSS。只有在同一设备、同一输入的重复采样中观察到稳定趋势,才适合写出性能改善幅度。
欢迎加入CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter
更多推荐

所有评论(0)