FSA全栈行动 FSA全栈行动
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr

公众号:FSA全栈行动

记录学习过程中的知识
首页
  • 移动端文章

    • Android
    • iOS
    • Flutter
  • 学习笔记

    • 《Kotlin快速入门进阶》笔记
    • 《Flutter从入门到实战》笔记
    • 《Flutter复习》笔记
前端
后端
  • 学习笔记

    • 《深入浅出设计模式Java版》笔记
  • 逆向
  • 分类
  • 标签
  • 归档
  • LinXunFeng
  • GitLqr
  • AndroidUI

  • Android第三方SDK

  • Android混淆

  • Android仓库

  • Android新闻

  • Android系统开发

  • Android源码

  • Android注解AOP

  • Android脚本

  • AndroidTv开发

  • AndroidNDK

  • Android音视频

  • Android热修复

  • Android性能优化

  • Android云游戏

  • Android插件化

  • iOSUI

  • iOS工具

  • iOS底层原理与应用

  • iOS组件化

  • iOS音视频

  • iOS疑难杂症

  • iOS之Swift

  • iOS之RxSwift

  • iOS开源项目

  • iOS逆向

  • Flutter开发

    • Dart - 抽象类的实例化
    • Flutter - 打印好用的Debug日志
    • Flutter - 混合开发
    • Flutter - 解决混合开发iOS脚本打包遇到的问题
    • Flutter - 低版本在iOS14上遇到的问题与解决方案
    • Flutter - 解决原生弹窗的触摸事件被Flutter响应的问题
    • Flutter - 实现列表上下拉切换header
    • Flutter - 获取ListView当前正在显示的Widget信息
    • Flutter - 列表滚动定位超强辅助库,墙裂推荐!🔥
    • Flutter - 快速实现聊天会话列表的效果,完美💯
    • Flutter - 聊天输入框更新文本时的必备优化点🔖
    • Flutter - 我给官方提PR,解决run命令卡住问题 😃
    • Flutter - 探索run命令到底做了什么 🤔
    • Flutter - 引擎调试(iOS篇)🛠
    • Flutter - 引擎调试bug到提交PR实战 🐞
    • Flutter - 船新升级😱支持观察第三方构建的滚动视图💪
    • Flutter - 瀑布流交替播放视频 🎞
    • Flutter - IM保持消息位置大升级(支持ChatGPT生成式消息) 🤖
    • Flutter - 滚动视图中的表单防遮挡 🗒
    • Flutter - 秒杀1/2曝光统计 📊
    • 一天内加入 Flutter 和 FlutterCandies 两大组织是什么体验 🧐
    • Flutter - 如何快速搓一个微信通讯录列表(azlist) 📓
    • Flutter - 混编项目集成Shorebird热更新🐦(安卓篇)
    • Flutter - 混编项目集成Shorebird热更新🐦(iOS篇)
    • Flutter - 解决返回原生页面时dispose方法未被触发的问题 🐞
    • Flutter - 升级3.19之后页面多次rebuild?🤨
      • 一、背景
      • 二、踩坑
      • 三、探索
      • 四、解决方案
        • 方案一:调整 ModalRoute.of
        • 方案二:魔改源码
      • 五、最后
    • Flutter - 热更新 Shorebird 1.0 正式版来了 🐦
    • Flutter - 使用Pigeon实现视频缓存插件 🐌
    • Flutter - 轻松搞定屏幕旋转功能 😎
    • Flutter - 解决Connection closed before full header was received
    • Flutter - 实现聊天键盘与功能面板的丝滑切换 🍻
    • Flutter - 支持观察NestedScrollView,兼容性更强 😈
    • Flutter - 聊天键盘与面板丝滑切换的强势升级 🍻
    • Flutter - 升级到3.24后页面还会多次rebuild吗?🧐
    • Flutter - 轻松实现PageView卡片偏移效果
    • Flutter - 轻松搞定炫酷视差(Parallax)效果
    • Flutter - 危!3.24版本苹果审核被拒!
    • Flutter - 子部件任意位置观察滚动数据
    • Flutter - iOS编译加速
    • Flutter - Xcode16 还原编译速度
  • 移动端
  • Flutter开发
LinXunFeng
2024-03-23
目录

Flutter - 升级3.19之后页面多次rebuild?🤨

欢迎关注微信公众号:[FSA全栈行动 👋]

# 一、背景

上周一尝试从 3.16.9 升级 3.19.3,主要有两个原因:

一是安卓端有一个疑似造成崩溃率上涨的 bug 在 Flutter 3.16 上出现,相关issue: #138947 (opens new window), 该 bug 在 3.13 不会出现,在 3.17.pre 上得到修复,而 3.16 之后的下个正式版本是 3.19。

二是苹果的隐私清单审核政策。

在苹果发布的【关于 App Store 提交的隐私更新 (opens new window)】新闻中指出

自 3 月 13 日起: 如果你上传新 App 或更新 App 到 App Store Connect,且该 App 使用了需要声明批准原因的 API,但你未在 App 的隐私清单中提供批准原因,我们会通过电子邮件告知你。这是对 App Store Connect 中现有通知的补充。

自 5 月 1 日起: 你需要就你的 App 代码使用的所列 API 提供批准原因,才能将新 App 或更新 App 上传到 App Store Connect。如果你没有合理的原因使用某个 API,请寻找替代的方案。如果你添加了常用第三方 SDK 列表中所列的新版第三方 SDK,那么这些 API、隐私清单和签名要求将应用于该 SDK。请务必使用包含其隐私清单的 SDK 版本,并注意在将该 SDK 添加为二进制依赖项时也需要提供签名。

在苹果的【即将发布的第三方SDK要求 (opens new window)】一文中,列举出需要隐私清单和签名的 SDK,其中就包含了 Flutter。为了符合该审核要求,Flutter 从 3.19 开始包含了 PrivacyInfo.xcprivacy 这个隐私清单文件。

文件位于: https://github.com/flutter/engine/blob/3.19.0/shell/platform/darwin/ios/framework/PrivacyInfo.xcprivacy (opens new window)

# 二、踩坑

升到到 3.19.3 后发现,从 页面A 跳转到 页面B 和返回 页面A 时,页面A 的 build 方法都会被执行,降回 3.16.9 则不会,这就很奇怪。后来发现是因为 页面A 间接使用了 ModalRoute.of。

以下是可复现问题的代码

class PageA extends StatefulWidget {
  @override
  State<PageA> createState() => _PageAState();
}

class _PageAState extends State<PageA> {
  @override
  Widget build(BuildContext context) {

    // ==== 这里 ====
+    final arguments = ModalRoute.of(context)?.settings.arguments;
+    print("PageA arguments:$arguments");

    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('PageA'),
      ),
      body: const SizedBox.shrink(),
    );
  }
}

# 三、探索

在经过一番摸索后,发现 ModalRoute 在 3.19 上面有一个小修改~

相关 issue 是: #112567 (opens new window) 。

该 issue 主要是涉及在 Web 端上按 Tab 键切换焦点的问题,后续有个 PR: #130841 (opens new window) 解决了该问题。

该 PR 因内部测试原因进行了回滚,后再重新登陆,现PR: #134554 (opens new window)

而在该 PR 中就对 ModalRoute 加了如下代码:

// packages/flutter/lib/src/widgets/routes.dart

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
  ...
+  @override
+  void didChangeNext(Route<dynamic>? nextRoute) {
+    super.didChangeNext(nextRoute);
+    changedInternalState();
+  }
+
+  @override
+  void didPopNext(Route<dynamic> nextRoute) {
+    super.didPopNext(nextRoute);
+    changedInternalState();
+  }
+
  @override
  void changedInternalState() {
    super.changedInternalState();
-    setState(() { /* internal state already changed */ });
-    _modalBarrier.markNeedsBuild();
+    // No need to mark dirty if this method is called during build phase.
+    if (SchedulerBinding.instance.schedulerPhase != SchedulerPhase.persistentCallbacks) {
+      setState(() { /* internal state already changed */ });
+      _modalBarrier.markNeedsBuild();
+    }
    _modalScope.maintainState = maintainState;
  }
...
}

didChangeNext 和 didPopNext 这两个方法对应的就是页面的 push 和 pop,现在在该 PR 中重写并调用了 changedInternalState 方法,在 changedInternalState 方法中调用了 setState。

下面将以高亮的方式标出重点代码(不是新增代码)。

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
  ...
  @protected
  void setState(VoidCallback fn) {
    if (_scopeKey.currentState != null) {
+      _scopeKey.currentState!._routeSetState(fn);
    } else {
      // The route isn't currently visible, so we don't have to call its setState
      // method, but we do still need to call the fn callback, otherwise the state
      // in the route won't be updated!
      fn();
    }
  }
...
}

这个 ModalRoute 内的 setState 会使 _ModalScopeStatus 的 _routeSetState 被调用,然后触发 _ModalScopeState 的 setState,接着其 child: _ModalScopeStatus 就开始 rebuild 了。

class _ModalScopeState<T> extends State<_ModalScope<T>> {
  ...
  void _routeSetState(VoidCallback fn) {
    if (widget.route.isCurrent && !_shouldIgnoreFocusRequest && _shouldRequestFocus) {
      widget.route.navigator!.focusNode.enclosingScope?.setFirstFocus(focusScopeNode);
    }
+    setState(fn);
  }
  
  
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      ...
+      child: _ModalScopeStatus(
        ...
      ),
    );
  }
  ...
}

如下代码所示,_ModalScopeStatus 是一个 InheritedWidget,在经过一系列的处理后最终会走到其 InheritedElement 的 update 方法,在 update 方法中通过调用 updateShouldNotify 来判断数据是否发生变化,进而决定是否通知相关依赖。

+ class _ModalScopeStatus extends InheritedWidget {
  ...

  @override
+  bool updateShouldNotify(_ModalScopeStatus old) {
+    return isCurrent != old.isCurrent ||
           canPop != old.canPop ||
           impliesAppBarDismissal != old.impliesAppBarDismissal ||
           route != old.route;
  }
  ...
}
class InheritedElement extends ProxyElement {
  ...
  @override
  void updated(InheritedWidget oldWidget) {
+    if ((widget as InheritedWidget).updateShouldNotify(oldWidget)) {
      super.updated(oldWidget);
    }
  }
  ...
}

_ModalScopeStatus 的 isCurrent 表示当前页面是否处于最上层,所以在打开和关闭下一个页面时,其值必定切换,也就是 updateShouldNotify 必定返回 true,既而通知依赖(实际上就是找出一个个依赖进行标脏,然后等待 build 方法的重新调用)。

而我们在使用 ModalRoute.of 的时候,内部就是将当前页的 BuildContext 添加到依赖中,所以他这个改动就会影响到使用 ModalRoute.of 的 Widget,使其多次 rebuild。

@optionalTypeArgs
static ModalRoute<T>? of<T extends Object?>(BuildContext context) {
  final _ModalScopeStatus? widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
  return widget?.route as ModalRoute<T>?;
}

# 四、解决方案

# 方案一:调整 ModalRoute.of

在当前版本中,of 的用意就是找到相应的 ModalRoute 并且创建依赖关系,当数据改变时会重新 build ,这是符合它期望用意的。

但是有些场景下我们并不希望有这个 “特性”,比如,我打开新页面后,通过 ModalRoute.of(context)?.settings.arguments 取路由参数,当前页面的取参,与跳转和关闭下个页面是没有任何关系的,所以这种场景下触发 rebuild 将毫无意义。

所以我提了个 PR: #145389 (opens new window), 给 ModalRoute.of 添加了 createDependency 参数,为开发者提供了是否创建依赖的选择。目前还在审核中~

  static ModalRoute<T>? of<T extends Object?>(
    BuildContext context, {
    bool createDependency = true,
  }) {
    _ModalScopeStatus? widget;
    if (createDependency) {
      widget = context.dependOnInheritedWidgetOfExactType<_ModalScopeStatus>();
    } else {
      widget = context
          .getElementForInheritedWidgetOfExactType<_ModalScopeStatus>()
          ?.widget as _ModalScopeStatus?;
    }
    return widget?.route as ModalRoute<T>?;
  }

# 方案二:魔改源码

原 PR 是对 Tab 键切换焦点问题的修复,但对于移动端来说根本不算问题,因为用不上~ 😅 (当然,如果你们的用户有使用无障碍功能的,还需要自行斟酌一下)

如果这个问题到时还未解决(原 PR 的作者还在休假),那我们也可以先注释掉相关代码对 changedInternalState 的调用来应对

// packages/flutter/lib/src/widgets/routes.dart

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
  ...
  @override
  void didChangeNext(Route<dynamic>? nextRoute) {
    super.didChangeNext(nextRoute);
+    // changedInternalState();
  }

  @override
  void didPopNext(Route<dynamic> nextRoute) {
    super.didPopNext(nextRoute);
+    // changedInternalState();
  }
}

提供一个补丁

# 进入你的 flutter 目录,比如我用的是 fvm 下载的 3.19.3
# 记得将 cd 后面的路径换成你自己电脑上的~
cd /Users/lxf/fvm/versions/3.19.3

# 下载补丁
curl -O https://raw.githubusercontent.com/LinXunFeng/flutter_assets/main/patch/01_rollbak_3_19_routes_change/0001-Roll-back-changes-to-routes.dart.patch

# 应用补丁
git apply 0001-Roll-back-changes-to-routes.dart.patch

# 五、最后

总而言之,距离5月1日(苹果强制要求添加隐私清单文件的期限)还有一个月,我们现在大可保持在 3.13 版本先用着,免得折腾,同时也祈祷快点修复该问题,然后顺利升级上去~

#Dart#Flutter
Flutter - 解决返回原生页面时dispose方法未被触发的问题 🐞
Flutter - 热更新 Shorebird 1.0 正式版来了 🐦

← Flutter - 解决返回原生页面时dispose方法未被触发的问题 🐞 Flutter - 热更新 Shorebird 1.0 正式版来了 🐦→

最近更新
01
Flutter - Xcode16 还原编译速度
04-05
02
AI - 免费的 Cursor 平替方案
03-30
03
Android - 2025年安卓真的闭源了吗
03-28
更多文章>
Theme by Vdoing | Copyright © 2020-2025 FSA全栈行动
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×