Flutter 双端热更新新方案:基于 Dart 官方动态模块的 flutter_patcher 1.0.0

开源,无需改代码、无需改 SDK,支持自托管,iOS 和 Android 都能用,并按 App Store 与 Google Play 的政策要求设计

一、为什么还要再造一个热更新?

做 Flutter 的同学大概都经历过:线上出了个小 Bug,改动只有一行,却得重新打包、提审、等审核、等用户升级。快的要一两天,慢的得拖上一周。

热更新本来是解决这类问题的标准手段,但在 Flutter 上一直卡在三个现实问题里:

1. 社区缺少成熟的开源方案 Flutter Release 包是 AOT 编译的机器码,没法像 JS 那样换个脚本文件就生效。社区过去的探索大致分两类:一类是自己写 Dart 解释器,或者把页面转成 DSL 下发,不仅写法受限,体验也很割裂;另一类是直接替换 .so 动态库,但这招只能用于 Android,iOS 无解,而且在 Google Play 上还属于违规操作。

2. Shorebird 落地门槛高 Shorebird 是目前最知名的商业方案,技术很强,但实际引入时经常遇到阻碍:它的核心服务绑定在官方云上,按补丁安装量计费,用户量上来后成本不低;不支持私有化部署,补丁和数据都得经过第三方服务器,很多公司过不了内部合规;此外,国内网络环境下的连接稳定性也是个现实问题。

3. 应用商店的审核红线 这一点最容易被忽略,也最致命:

  • Google Play 禁止从非官方渠道下载可执行代码(如 dex、jar、so 等),但允许在虚拟机或解释器中运行、且不直接调用受限 Android API 的代码。

  • App Store 同样禁止下发原生可执行代码,但开发者协议允许加载由内置解释器执行的代码,前提是不改变 App 的主要用途、不用来绕过审核。

因此,一套真正能在生产环境落地的 Flutter 热更新,补丁必须以"解释执行"的方式运行,同时尽量不打扰开发者原有的编码习惯。

基于这个思路,我开发了 flutter_patcher。

二、flutter_patcher 是什么

flutter_patcher 是一套开源、可自托管、符合双端商店政策的 Flutter 热更新方案:

  • 双端可用:Android 和 iOS 使用同一套构建与下发流程。

  • 无需改代码、无需改 SDK:业务代码照常写,接入只需几行初始化代码。CLI 会根据项目的 Flutter 版本自动下载并校验配套引擎,不会污染本机的 Flutter SDK。

  • 支持自托管:客户端只需要一个能下载静态文件的 HTTPS 地址,S3、R2、OSS、GCS 配合 CDN 都能直接用。存储和 CDN 本身不需要可信,所有下发内容都会经过签名校验。

  • 面向商店政策设计:补丁产物是 Dart 字节码,由 App 内置的解释器执行,不下发任何原生可执行文件。

  • 更新范围广:项目自身的 Dart 代码、新增或修改的纯 Dart 依赖、类结构调整,以及图片、字体、JSON 等静态资源,都能通过补丁更新。

  • 安全兜底:内置补丁签名校验;若补丁引起启动崩溃,会自动回退到安装包版本并拉黑该补丁;支持发布端撤回,已撤回的补丁无法被重放。

三、它是怎么做到的?

下面重点聊聊大家最关心的三个问题:靠什么跑、怎么换掉旧代码、为什么商店允许。

1. 基础能力:Dart 官方的动态模块

Dart 和 Flutter 团队近期在做一项实验性能力,叫动态模块(Dynamic Modules) :在 AOT 编译的 App 里内置一个字节码解释器,让应用在运行时加载并执行 Dart 字节码。flutter_patcher 就建立在这项官方能力之上。

这项能力目前还没随官方 SDK 默认发布,需要自己从源码编译引擎。flutter_patcher 把这一步替你做好了:它直接提供预编译好的引擎,在官方源码基础上开启了动态模块,并补了少量底层改动(修复 VM 字节码读取异常、打通补丁内的资源读取、拦截引擎拉起的入口)。接入时你不需要自己编译任何东西。

2. 核心难题:官方限制"只能新增,不能替换"

官方的动态模块有一条硬性限制:加载进来的代码只能新增声明,不能覆盖 App 里已有的声明。 但热更新的目的恰恰是"换掉旧代码",这该怎么绕过去?

flutter_patcher 的做法是重写包名并切换入口:

  1. 整包改名,重新编译 :生成补丁时,工具会把应用自己的代码包完整复制一份,将包名改为 patch.<模块号>.<原包名>,连带里面的 import 路径也一并改写,最后编译成字节码模块。对 Dart VM 来说,加载进来的全是新声明,没有覆盖任何已有代码,完全符合官方限制。同时工具还会做静态检查:只要补丁里还引用了未改名的旧包路径,就会直接中断报错。

  2. 从补丁的 main 启动 :执行 build 打基线包时,框架会注入一个前置启动入口,在你的 main 之前运行。App 冷启动时,它会先验签并加载补丁,然后直接调用补丁里的 main 函数。宿主包里那份旧的业务代码,在这次运行中就不会再被调用到了。

  3. 拦截其他入口:像后台 Isolate、推送回调这类由引擎直接拉起的入口,预编译引擎在底层做了拦截,会自动把调用转到补丁里对应的新函数上。

  4. 底层共享:Flutter 框架层、SDK,以及带原生代码的插件、FFI 和依赖它们的包,都不会打进补丁,而是继续以宿主包里的机器码运行,补丁通过动态接口去调用它们。

在补丁体积控制上,框架分了两档:常规情况下只打包应用自己的业务代码(lean 模式);当检测到纯 Dart 依赖有变动时,会自动升级为把未锁定的纯 Dart 依赖也一起带上(whole 模式)。因为剥离了框架和原生部分,补丁体积非常小------在示例 App 里改一行文字,生成的补丁只有约 19 KB。

3. 为什么说它在政策范围内

两大应用商店的合规底线,都是不能下发原生可执行代码,但允许解释执行的代码。flutter_patcher 的字节码补丁由 App 内置解释器运行,既不会生成新的 .so 或 .dex 文件,也不需要向系统申请可执行内存,和很多 App 下发 JS、游戏下发 Lua 脚本属于同一类做法。

当然,技术上的合规不代表可以随意使用:热更新应该只用来修线上 Bug 和做小幅调整,不能用来大幅改变 App 的核心功能或绕过审核。 这个边界需要开发者自己把控。需要说明的是,审核结果最终由商店决定,没有任何热更新方案能做出 100% 的保证。我们能做的,是让技术实现落在政策明确允许的范围内。

4. 性能与边界

  • 没打补丁时:和普通的 Release 包完全一样,行为与普通 Release 包一致。

  • 打了补丁后:应用自己的业务代码在解释器里运行,执行速度比机器码慢;但因为 Flutter 框架、渲染引擎和插件依然是机器码,常规的业务逻辑和 UI 交互通常影响不大,只有计算密集型的代码会有明显变慢。

  • 不支持热更的内容 :原生代码(Kotlin/Swift/C++)、含原生代码的插件及依赖它们的包、构建脚本、删除已有资源、升级 Flutter 版本、补丁代码里的 FFI 调用、运行时动态构造的 IconData。

这些不支持的改动,在生成补丁时都会进行静态检查,一旦越界会直接报错,避免把坏补丁发到线上。

当前环境要求:支持 Flutter 3.47.0--3.47.9(生成补丁必须使用与基线包相同的 Flutter 版本);Android 需 arm64 架构、API 28 及以上,iOS 需 13.0 及以上;不满足条件的设备会直接按原包运行,不加载补丁。

四、上手只需三步

1. 添加依赖并初始化

Bash

csharp 复制代码
flutter pub add flutter_patcher

Dart

csharp 复制代码
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterPatcher.init();
  runApp(const MyApp());
}

// 在合适的时机检查更新,更新地址配置在 flutter_patcher.yaml 的 url 字段中
final check = await FlutterPatcher.checkUpdate();
if (check.hasUpdate) {
  await FlutterPatcher.applyPatch(check.patch!);
}

2. 生成密钥,构建基线安装包

Bash

perl 复制代码
dart run flutter_patcher keys init       # 只需执行一次:生成密钥,请将生成的 flutter_patcher.yaml 提交到 Git
dart run flutter_patcher build android   # 或 build ios(需在 Apple Silicon Mac 上执行)

直接用 build 产出的安装包去上架。该命令会在本地记录当前版本的基线(Baseline),后续补丁都基于这个基线对比生成。

3. 修复 Bug,生成并发布补丁

Bash

arduino 复制代码
dart run flutter_patcher patch android   # 与基线对比,生成差异补丁
dart run flutter_patcher publish         # 签名并上传至存储服务

publish 命令可以直接把产物写入本地目录,或者自动上传到 S3 及兼容服务(如 Cloudflare R2);如果用其他云存储,把产物手动上传上去即可。

五、最后

flutter_patcher 1.0.0 只做一件事:App 上线后发现 Bug,不用重新发版就能修。

它用的是 Dart 官方的动态模块(dynamic modules)。定制引擎的编译、包名改写、入口切换、补丁签名、启动崩溃后自动回滚,这些都由工具链处理。日常修 Bug 只需要三条命令:build 打包上架,patch 生成补丁,publish 发布。

如果这个工具对你有帮助,欢迎去 GitHub 点个 ⭐️。用起来踩到坑或者有什么想法,欢迎提 Issue 或在评论区交流。

相关推荐
好的佩奇6 小时前
Dart 之方法、操作符与可调用对象
android·flutter·dart
恋猫de小郭6 小时前
Flutter Impeller Vulkan 内存持续上涨,问题没想到是文字阴影
android·前端·flutter
传奇开心果1 天前
【Flutter踩坑排雷】第3课:const 误用坑|const 错误使用引发 UI 异常与性能损耗
android·flutter
行者-全栈开发1 天前
Flutter 插件 device_calendar_plus 开源鸿蒙适配实战:从权限弹窗失灵到循环日程跑通
flutter·跨平台·harmonyos·arkts·日历·适配·开源鸿蒙
恋猫de小郭1 天前
Flutter 开始支持 Jetpack Compose Platform View
android·前端·flutter
SoaringHeart2 天前
Flutter 查缺补漏:@JsonKey 属性全解,让模型层不再被后端字段牵着走
flutter
恋猫de小郭2 天前
Flutter 可以更直接地调用原生 API 了,Dart Direct Native Interop
android·前端·flutter
码点2 天前
二.Flutter 速成第二课:核心概念与第一个自定义页面
flutter