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 的修改内容这次修改范围主要涉及rendering/viewport.dart widgets/scroll_view.dart widgets/page_view.dart widgets/list_view.dart widgets/grid_view.dart对应源码的影响有RenderViewportBaseRenderViewportRenderShrinkWrappingViewportViewportShrinkWrappingViewportScrollViewListViewPageView所以虽然看起来只是一个小 feature 和一个 bug fix但是其实这个调整并不是 Widget 层的小改动而是Viewport 渲染路径修改。所以才会需要挑出来聊一聊。首先是ScrollCacheExtent在之前的实现里Viewport cache 主要由这两个字段控制double cacheExtentCacheExtentStylecacheExtentStyle相关逻辑为switch(cacheExtentStyle){caseCacheExtentStyle.pixel:calculatedCacheExtentcacheExtent;caseCacheExtentStyle.viewport:calculatedCacheExtentmainAxisExtent*cacheExtent;}涉及的关键变量是mainAxisExtentviewport size而问题也就出现在这里因为ShrinkWrappingViewport的特殊性当ScrollView设置shrinkWrap true的时候ScrollView.buildViewport就会会创建ShrinkWrappingViewportScrollView.buildViewport-ShrinkWrappingViewport-RenderShrinkWrappingViewport而ShrinkWrappingViewport的特点就是 viewport size 由子节点决定而不是通过父约束这就意味着mainAxisExtent可能不是y一个有限的值 也就是类似以下的场景SingleChildScrollView-ListView(shrinkWrap:true)Column-ListView(shrinkWrap:true)这些情况下父布局在主轴方向是 unbounded 所以ShrinkWrappingViewport会得到constraints.maxExtent infinity的情况也就是最终mainAxisExtentinfinity这乍一看没什么问题但cacheExtent逻辑没有考虑这个情况因为在旧逻辑里viewport cache modecacheExtentStyle.viewport也就是calculatedCacheExtentmainAxisExtent*cacheExtent如果这时候mainAxisExtent infinity那就会infinity * 0.5 infinity以至于在后续布局计算里paintExtent\layoutOffset\scrollOffset都可能出现infinity - infinity也就是结果为 NaN 比如SingleChildScrollView(child:ListView.builder(shrinkWrap:true,cacheExtent:0.5,cacheExtentStyle:CacheExtentStyle.viewport,itemBuilder:...),)而在新 API 下cacheExtent和cacheExtentStyle现在变成ScrollCacheExtent并且内部做了适配所以这种情况现在不会再报错了SingleChildScrollView(child:ListView.builder(shrinkWrap:true,scrollCacheExtent:ScrollCacheExtent.viewport(0.5),),)所以这里的ScrollCacheExtent不是简单的把两个参数编程一个而是内部做了重构首先是在 viewport.dart 内部提供了ScrollCacheExtent.pixels()ScrollCacheExtent.viewport()对应内部实现了新的 Viewport 计算逻辑_calculateCacheOffset(mainAxisExtent)_calculatedCacheExtent_scrollCacheExtent._calculateCacheOffset(mainAxisExtent)这个情况下 cache 集中计算并且避免 style value 分离其中「NaN 修复」的关键在于RenderShrinkWrappingViewport对应核心修改为if(!mainAxisExtent.isFinite)cacheExtent0因为对于 infinite viewport 来说实际上 already builds all children 所以根本不需要 Cache 而这个修改也会涉及PageView\ListView\GridView\CustomScrollView等常用控件。所以这也是一个相对昂贵的性能配置选项。所以这个ScrollCacheExtent的修改本质上是重构 Viewport cache API修复ShrinkWrappingViewport在无约束下cacheExtent计算 NaN 的问题统一ScrollView/Viewport/RenderViewport的缓存逻辑虽然逻辑改动看起来好像改的不多但是涉及的文件和地方还是挺多的从长远来看这个修改还是比较有意义的至少之前经常遇到的 NaN 问题终于不要自己处理了。链接https://github.com/flutter/flutter/pull/181092