Flutter Beta 版本引入 ScrollCacheExtent ,并修复长久存在的 shrinkWrap NaN 问题

在最近发布的 Flutter 3.43.0-0.1.pre 这个 Beta 版本里,官方在 Framework 层面对 ScrollView / Viewport / ShrinkWrappingViewport 做了一个比较有意思的修改:

  • 引入 ScrollCacheExtent,废弃 cacheExtent + cacheExtentStyle
  • 修复 RenderShrinkWrappingViewport 在无约束下 cacheExtent 可能变成 NaN 的问题
  • 重构 Viewport cache 计算路径

这次修改涉及 rendering 层核心代码,属于 Viewport 底层重构 ,暂时看来修改的作用是正向的,应该不至于引起类似之前《Flutter 3.41 iOS 键盘负优化:一个代码洁癖引发的负优化》 的问题。

根据 #181092 的修改内容,这次修改范围主要涉及:

dart 复制代码
rendering/viewport.dart
widgets/scroll_view.dart
widgets/page_view.dart
widgets/list_view.dart
widgets/grid_view.dart

对应源码的影响有:

dart 复制代码
RenderViewportBase
RenderViewport
RenderShrinkWrappingViewport
Viewport
ShrinkWrappingViewport
ScrollView
ListView
PageView

所以,虽然看起来只是一个小 feature 和一个 bug fix,但是其实这个调整并不是 Widget 层的小改动,而是 Viewport 渲染路径修改

所以才会需要挑出来聊一聊。

首先是 ScrollCacheExtent ,在之前的实现里,Viewport cache 主要由这两个字段控制:

dart 复制代码
double cacheExtent
CacheExtentStyle cacheExtentStyle

相关逻辑为:

dart 复制代码
switch (cacheExtentStyle) {
  case CacheExtentStyle.pixel:
    calculatedCacheExtent = cacheExtent;
  case CacheExtentStyle.viewport:
    calculatedCacheExtent = mainAxisExtent * cacheExtent;
}

涉及的关键变量是:

dart 复制代码
mainAxisExtent = viewport size

而问题也就出现在这里,因为 ShrinkWrappingViewport 的特殊性,当 ScrollView 设置 shrinkWrap = true 的时候,ScrollView.buildViewport 就会会创建 ShrinkWrappingViewport

dart 复制代码
ScrollView.buildViewport
 -> ShrinkWrappingViewport
 -> RenderShrinkWrappingViewport

ShrinkWrappingViewport 的特点就是 viewport size 由子节点决定,而不是通过父约束,这就意味着mainAxisExtent 可能不是y一个有限的值 ,也就是类似以下的场景:

dart 复制代码
SingleChildScrollView
  -> ListView(shrinkWrap: true)
dart 复制代码
Column
  -> ListView(shrinkWrap: true)

这些情况下父布局在主轴方向是 unbounded ,所以 ShrinkWrappingViewport 会得到 constraints.maxExtent = infinity 的情况,也就是最终:

dart 复制代码
mainAxisExtent = infinity

这乍一看没什么问题,但 cacheExtent 逻辑没有考虑这个情况,因为在旧逻辑里:

dart 复制代码
viewport cache mode
= cacheExtentStyle.viewport

也就是

dart 复制代码
calculatedCacheExtent = mainAxisExtent * cacheExtent

如果这时候 mainAxisExtent = infinity ,那就会 infinity * 0.5 = infinity ,以至于在后续布局计算里paintExtent \ layoutOffset \ scrollOffset 都可能出现 infinity - infinity ,也就是结果为 NaN ,比如:

dart 复制代码
SingleChildScrollView(
  child: ListView.builder(
    shrinkWrap: true,
    cacheExtent: 0.5,
    cacheExtentStyle: CacheExtentStyle.viewport,
    itemBuilder: ...
  ),
)

而在新 API 下,cacheExtentcacheExtentStyle 现在变成 ScrollCacheExtent ,并且内部做了适配,所以这种情况现在不会再报错了:

dart 复制代码
SingleChildScrollView(
  child: ListView.builder(
    shrinkWrap: true,
    scrollCacheExtent: ScrollCacheExtent.viewport(0.5),
  ),
)

所以这里的 ScrollCacheExtent 不是简单的把两个参数编程一个,而是内部做了重构,首先是在 viewport.dart 内部提供了:

dart 复制代码
ScrollCacheExtent.pixels()
ScrollCacheExtent.viewport()

对应内部实现了新的 Viewport 计算逻辑:

dart 复制代码
_calculateCacheOffset(mainAxisExtent)
​
_calculatedCacheExtent =
  _scrollCacheExtent._calculateCacheOffset(mainAxisExtent)

这个情况下 cache 集中计算,并且避免 style + value 分离 ,其中「NaN 修复」的关键在于 RenderShrinkWrappingViewport ,对应核心修改为:

dart 复制代码
if (!mainAxisExtent.isFinite)
  cacheExtent = 0

因为对于 infinite viewport 来说,实际上 already builds all children ,所以根本不需要 Cache ,而这个修改也会涉及 PageView \ ListView \ GridView \ CustomScrollView 等常用控件。

所以这也是一个相对昂贵的性能配置选项。

所以这个 ScrollCacheExtent 的修改,本质上是:

  • 重构 Viewport cache API
  • 修复 ShrinkWrappingViewport 在无约束下 cacheExtent 计算 NaN 的问题
  • 统一 ScrollView / Viewport / RenderViewport 的缓存逻辑

虽然逻辑改动看起来好像改的不多,但是涉及的文件和地方还是挺多的,从长远来看,这个修改还是比较有意义的,至少之前经常遇到的 NaN 问题终于不要自己处理了。

链接

github.com/flutter/flu...

相关推荐
Liu.7742 小时前
vscode前端实用插件
前端·vscode
黄林晴2 小时前
你写过多少个重复的 @Preview?Compose 终于要解决这个问题了
android
REDcker2 小时前
Android MediaCodec 架构与实现解析
android·架构
常利兵2 小时前
Android Compose 指南:Column与LazyColumn的抉择
android
黄林晴2 小时前
Kotlin 2.3.20 发布!解构声明不怕写反了
android·kotlin
阿拉斯攀登2 小时前
第 16 篇 摄像头驱动适配,V4L2 子系统详解
android·驱动开发·rk3568·瑞芯微·rk安卓驱动
HWL56792 小时前
使用CSS实现,带有动态浮动高亮效果的导航菜单
前端·css
GISer_Jing2 小时前
AI Agent技能Skills设计
前端·人工智能·aigc·状态模式
大漠_w3cpluscom2 小时前
使用 sibling-index() 和 if() 实现动态的 :nth-child()
前端