上一篇,我们分享了Flutter在两轮的应用推广,本次分享的主题是Flutter在两轮的升级之路,主要分为两部分。一是我们在Flutter落地之后,由于业务的发展,导致我们需要对Flutter进行升级。二是升级之后我们遇到了一些问题,这里列举了一个比较典型的案例——FlutterEngine的自定义。

背景介绍

升级原因

两轮从2019年来开始使用flutter,算是应用比较早的团队。随后flutter升级了2.x版本,这个版本有个比较重要的更新就是空安全(nullsafety),因为在1.x的版本,如果一个对象为null之后,再去调用它的任何方法,都会抛异常,在页面上的表现就是页面假死,也不会再刷新了。所以就需要研发人员关注下自己使用变量时是否已经为空了,2.0会强加这个检查,在编译阶段让我们确定好次变量是否可空以及是否懒加载,这样在我们使用变量时,就会有系统提醒我们,相当于帮我们做了这个工作,减少出错率。其实很多其他语言也都有这样的检测机制。再加上一些性能的提优,于是有了升级的诉求。

Flutter版本敲定

图片

这里对比了一下1.x版本和2.x版本的主要区别。2.x版本优化了应用启动的延迟,调用Dart VM的GC策略也做了一些改进,低端Android设备的初始帧出线间隔时间最多减少了约300ms。

此外,空对象消息是我们在开发中最常见的exception,所以空安全是很有必要的。pub.dev已经发布了超过1000个空安全package,包括来自Dart、Flutter、Firebase和Material 团队发布的数百个package。

升级实施

基础设施

基础设施升级主要涉及到两方面,打包机sdk升级和个人研发侧版本管理工具。由于SDK升级在2.8.1版本删除了一些被弃用的Api,会造成我们编译报错。可参考以下修改:

  • 切换本地环境为2.8.1版本
  • 运行工程查看报错
  • 修改对应报错库的api
  • 本地运行成功后,移动端CI/CD系统发库提测
  • 测试同学全量回归,灰度

升级节奏

整体的升级节奏包括:基础升级启动、各个业务线研发启动、业务线null_safety改造1-n次、全量回归、灰度发布和异常观测。

本次2.0升级工作量基本都在null_safety语法改造上,由于工作量大涉及范围广所以无法在短期内完成语法改造。所以本次改造将依照业务功能划分,尽量收拢每次语法改造带来的影响范围,从而引起测试侧多次全量测试的人力浪费。

Flutter空安全

健全的空安全已在 Dart 2.12 和 Flutter 2.0 及更高版本中可用。Dart 3 及以后的版本将只支持健全的空安全。

有了空安全,下面代码中所有的变量都是非空的:
image.png

依赖项检查

使用官方工具进行类型推断,最好确保依赖的所有包都已经升级为null safe版本。所以在改造时应该从底层向上逐步升级。

在控制台执行如下命令:
dart pub outdated —mode=null-safety
可以查看到当前项目工程依赖包是否是null safe。

对于其中不符合要求的内部库,需要手动升级后打包,生成null safe版本,建议版本号比之前最新版增加一个大版本;对于三方库可以在三方库官方站点或者中文站点查找符合null safe的最新版本号,并修改项目工程的yaml文件。

全部依赖修改完成后,执行命令拉取最新依赖:

flutter pub get
flutter pub upgrade

操作完成之后可以使用第一步的命令检查是否依赖都校正完了。

如果提示所有依赖都声明支持null safe,或者期望跳过依赖检查,就可以进行后续操作。

执行成功之后,会生成url,点击可在浏览器中使用迁移工具:

左侧是需要迁移的文件目录,官方工具会对选中文件进行类型推断并自动添加如下标记:

  • ! (类型不可为空)
  • ? (类型可为空)
  • late (初始化延时)
  • late final (一次性初始化延时)
  • required (参数标记)

面板中间可以看到详细改动,右侧可以看到当前文件改动的原因。

对于不需要迁移或者大型文件需要分步迁移的场景,可以只勾选需要迁移的文件目录,官方迁移工具会自动为不需要迁移的文件顶层添加null unsafe Dart的版本注释。

引入支持null safe的第三方库

在更改本地代码时,一般都需要先升级依赖的第三方库,但是在官网第三方库下载地址https://pub.flutter-io.cn或者中文站点https://pub.flutter-io.cn中查询到依赖库的最新版本。
image.png

常见错误及修改方式

  • 错误1:The non-nullable variable 'preferences' must be initialized.

原因:非空的静态变量或者顶层变量在声明时没有初始化,诊断器就会报此错误。因为flutter对于没有初始化的变量会自动赋值为null,而该变量并没有声明可空。

解决方法:
图片

  • 错误2:The method '*' can't be unconditionally invoked because the receiver can be 'null'.

原因:可能为空的变量直接使用'.'调用方法,诊断器就会报此错误。

解决方法:
图片

  • 错误3:Map<int, list> 复杂类型修改

解决方法:
图片

  • 错误4:The default 'List' constructor isn't available when null safety is enabled.

解决方法:
image.png

FlutterEngine的Crash问题

在上线之后,平稳运行一段时间,我们也持续观察监控数据,发现在native有大量新类型crash。

[FlutterEngine destroyContext]相关的crash

崩溃原因是在用户关闭应用的时候会通知到flutter引擎进行重置操作(调用方法看【FlutterEngine destroyContext】)。重置的时候由于官方问题导致 engine被多次重置销毁。在第二次的重置的时候(OnPlatformViewSetSemanticsEnabled这个方法里边获取引擎指针的时候崩溃了)

发现问题后,我们去翻看了官方issue,发现官方已经修复该问题,不过是在后续版本上修复的,结合我们自身情况(此crash发生是在后台)我们内部研讨了几种解决方案:

  • 解决方案 1

官方已经修复了该问题,并已经合并代码到master。

修复的pull request:https://github.com/flutter/engine/pull/30835/commits

bufix的描述:
https://github.com/flutter/flutter/issues/95844

合并到引擎commit记录:https://github.com/flutter/engine/commit/fb3ee7f2b5e6e537a2a83c9fe2cf733cd9c6ec06

在高版本,2.10.2(2.10.3+)以上此类问题已修复。

2.8.1Cherrypicks:
https://github.com/flutter/engine/pull/30355

但目前无法直接升级到flutter版本到2.10.2 以上,暂时的解决办法是本地cherry pick 代码,编译打包flutter engine,替换2.8.1的flutter engine。

风险最小但是需要评估改pull request的cherry pick成本,需要基础技术支持打包方案文档,需要探索引擎打包融合到flutter修改打包,再到flux打flutter包流程融合。

  • 解决方案 2

flutter引擎从2.8.1升级到2.10.4

  • 解决方案 3

可以hook destroyContext 在判断应用状态能否调用destroyContext,但这个有一定风险可能会影响其他的逻辑。

  • 解决方案 4

梳理引擎切换的场景,通过业务上避免减小crash发生。

最终我们选择了保守的方案一定制FlutterEngine。
image.png

环境准备

git
Python
Xcode
depot_tools创建目录 /Users/xx/Desktop/flutter-engine
depot_tools安装(需要全局VPN否则很慢)

cd /Users/xx/Desktop/flutter-engine
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

配置环境 (~/.zshrc或者~/.bashrc)

export PATH=/Users/xx/Desktop/flutter-engine/depot_tools:$PATH

图片

这里有个配置文件需要注意下:
gclient
配置在/Users/xxx/Desktop/flutter-engine目录engine文件夹创建.gclient文件

同步依赖

cd /Users/xxx/Desktop/flutter-engine/engine
gclient sync

编译产物

cd /Users/xxx/Desktop/flutter-engine/engine/src
# --simulator就是模拟器
# --ios-cpu=arm就是armv7,不指定默认就是arm64
flutter/tools/gn --ios —unoptimized

在src/out/ios_debug_unopt目录下,会生成一个 Xcode 项目,用于debug时候使用引擎源代码。

src/out/host_debug_unopt,我在debug引擎时候发现需要里面的dart-sdk,所以这个也需要编译一下。如果没有修改dart层代码就不需要执行这一步。
图片

执行ninja,生成framework产物。

ninja -C out/ios_debug_unopt && ninja -C out/host_debug_unopt

编译过程很漫长,可以在ninja后面加上参数-j 2,避免编译占用过多的电脑资源,影响你开发。

编译完成之后,就要创建最后的Flutter.xcframework。

官方也有提供python脚本工具,src/flutter/sky/tools目录下的:create_ios_framework.py和create_macos_gen_snapshots.py

创建Flutter.xcframework:release版本加--dsym参数会生成dysm符号表

cd  ~/Desktop/flutter-engine/engine/src/flutter/sky/tools

# debug版本Flutter.xcframework
python create_ios_framework.py --dst  ~/Desktop/flutter-engine/engine/src/out/vd/ios --arm64-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_debug --armv7-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_debug_arm --simulator-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_debug_sim

# profile版本Flutter.xcframework
python create_ios_framework.py --dst  ~/Desktop/flutter-engine/engine/src/out/vd/ios-profile --arm64-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_profile --armv7-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_profile_arm --simulator-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_debug_sim

# release版本Flutter.xcframework
python create_ios_framework.py --dst  ~/Desktop/flutter-engine/engine/src/out/vd/ios-release --arm64-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_release --armv7-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_release_arm --simulator-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_debug_sim --dsym

# 每个版本都加上了模拟器的部分,便于使用方模拟器调试

创建gen_snapshots

cd  ~/Desktop/flutter-engine/engine/src/flutter/sky/tools

# debug版本gen_snapshot_arm64和gen_snapshot_armv7
python create_macos_gen_snapshots.py --dst  ~/Desktop/flutter-engine/engine/src/out/vd/ios --arm64-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_debug --armv7-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_debug_arm

# profile版本gen_snapshot_arm64和gen_snapshot_armv7
python create_macos_gen_snapshots.py --dst  ~/Desktop/flutter-engine/engine/src/out/vd/ios-profile --arm64-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_profile --armv7-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_profile_arm

# release版本gen_snapshot_arm64和gen_snapshot_armv7
python create_macos_gen_snapshots.py --dst  ~/Desktop/flutter-engine/engine/src/out/vd/ios-release --arm64-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_release --armv7-out-dir  ~/Desktop/flutter-engine/engine/src/out/ios_release_arm

# 模拟器是JIT的,不需要gen_snapshot

图片

图片

未来展望

俗话说,工欲善其事必先利其器。目前两轮Flutter升级后平稳运行,对业务迭代快速发展保驾护航。未来,两轮在大前端甚至可以继续探索flutter的新特效,如flutter-web,为大前端在小程序,taro,h5,app的大融合夯实基础。

(本文作者:叶旸)

图片


哈啰技术
89 声望51 粉丝

哈啰官方技术号,不定期分享哈啰的相关技术产出。