34 · 动态化与热更新
一句话定位:热重载是开发期效率工具,热更新是线上不发版改代码,动态化是运行时换界面/配置。三件事经常被混为一谈,本篇把它们拆开,并给出"Flutter 到底能不能做线上热更新"的诚实答案。
开篇速览
| 问题 | 答案 |
|---|---|
| 前端对应物 | Web 的 HMR(热更新)、灰度发布 / 特性开关、微前端动态加载 |
| 核心结论 | Flutter 没有"官方一键热更新",方案碎片化且 iOS 受限;多数需求用"远程配置 + 灰度"就能解决 |
| 最容易踩的坑 | 把开发期热重载当成线上热更新能力,方案评审直接翻车 |
| 一句话红线 | iOS 侧"动态下发可执行代码"违反 App Store 审核条款;Android 可行但收益/成本要算清楚 |
代码来源说明:HaierEnergy 当前
lib/未接入任何线上热更新 SDK,也不存在动态化框架。 本篇按"决策框架 + 可行方案"撰写,属通用知识篇,落地前需与运维/后端共同评审。
一、先把三个概念拆开
| 名词 | 阶段 | 改什么 | 是否发版 | 谁用 |
|---|---|---|---|---|
| 热重载 hot reload | 开发期 | Dart 代码 | 否 | 开发者,秒级看到改动 |
| 热重启 hot restart | 开发期 | Dart 代码 + 状态 | 否 | 开发者 |
| 热更新 hot fix | 线上 | 可执行代码 | 否(下发补丁) | 线上用户 |
| 动态化 | 线上 | 界面结构 / 远程配置 | 否 | 线上用户,常配合灰度 |
开发期"热重载"与线上"热更新"是两套完全不同的技术:
- 热重载依赖 Flutter 开发框架的增量编译,产物只在你本机;
- 线上热更新需要把新的 Dart 代码打包成补丁下发到用户手机,涉及编译、签名、下发、回滚一整条链路。
二、Flutter 线上热更新的真实约束
2.1 平台限制
| 平台 | 动态下发可执行代码 | 说明 |
|---|---|---|
| iOS | ❌ 禁止 | App Store 审核条款(3.3.2 / 2.5.2)不允许下载可执行代码;解释型/脚本型同样受限,唯一例外是苹果自己的 JavaScriptCore 沙箱 |
| Android | ⚠️ 可行但有前提 | 技术上可以把 Dart 补丁打进 APK 增量包,但要自己建下发/校验/回滚体系,Google Play 对"更新代码"也有政策要求 |
| Web / 桌面 | ✅ 可行 | 本身就是远端资源,天然可更新,但要处理缓存与版本 |
2.2 为什么 Flutter 没有官方方案
Dart 代码在 AOT(提前编译)模式下被编译成机器码,不能像 JS 一样直接下发源码再解释。可行的两条技术路线:
- 整包/增量包热更新:iOS 上不可行(审核),Android 上相当于自建"应用商店";
- 脚本化动态化:在 Flutter 里内嵌脚本引擎(如 QuickJS、Lua)跑部分逻辑,把"会变的规则"从原生代码里抽出来。
第一条成本高、收益不稳;第二条能覆盖的场景有限。这就是"方案碎片化"的根源。
三、真正常用的"轻动态化":远程配置 + 灰度
大多数"想热更新"的需求,本质是不想发版就改行为。这类需求 80% 用"远程配置 + 特性开关"就能覆盖:
dart
// 伪代码:AppConfigRepository 拉取远程配置
final config = await configRepository.fetch();
if (config.isFeatureOn('device_energy_chart')) {
// 新功能对部分用户灰度开放
}3.1 常见远程配置能力
| 需求 | 方案 | 是否算"热更新" |
|---|---|---|
| 改开关、改文案、改接口地址 | 远程配置 / 特性开关 | 不算,但解决 80% 场景 |
| 改列表顺序、优先级 | 下发结构化配置(JSON) | 不算 |
| 改图标、图片、主题 | 下发静态资源(带版本号) | 弱动态化 |
| 改页面结构 | 下发"界面描述"(JSON DSL 渲染) | 动态化,成本高 |
| 改业务逻辑代码 | Dart 补丁 / 脚本引擎 | 热更新,成本最高 |
3.2 落地要点
- 开关一定要有兜底值:拉不到配置时走默认值,不能因配置失败影响核心流程;
- 配置要版本化 + 灰度:按用户分桶,先 1% 再放量;
- 本地缓存 + 增量拉取:别每次启动都全量下载;
- 和埋点联动:开关曝光、命中、回滚都要有埋点,否则灰度决策没有数据。
四、真要上"动态化"怎么选(决策框架)
| 你的诉求 | 推荐路线 | 成本 |
|---|---|---|
| 改文案/开关/接口 | 远程配置中心(自研或三方) | 低 |
| 改静态资源 | CDN 资源版本化 + 本地缓存 | 低 |
| 改少量页面结构 | JSON DSL 渲染框架(自研,边界严格) | 中 |
| 只做 Android 的热修复 | 增量包下发(自研链路) | 高 |
| 跨端一致的热更新 | 不建议,维护两条原生补丁链路不现实 | 极高 |
判断红线:如果团队连"远程配置 + 灰度 + 回滚"都还没建好,先别谈 Dart 补丁热更新。 优先级永远是:稳定发版流程 → 远程配置 → 灰度 → 资源动态化 → 代码热更新。
五、脚本引擎兜底(进阶)
对"规则频繁变化"的小段逻辑(如优惠计算、告警阈值),可以用脚本引擎把规则抽出来:
- Android / iOS 双端内嵌 JS 引擎(如
quickjsDart 绑定)跑一段规则脚本; - 脚本用远程下发,不触碰 iOS 审核红线(走 JavaScriptCore 类沙箱解释执行的灰色边界);
- 只放"非核心、容错要求低"的逻辑,核心交易/安全逻辑绝不能脚本化。
代价:多一个运行时、性能损耗、调试困难、双端行为不一致风险。只有规则变更频率确实高才值得。
六、项目建议
针对 HaierEnergy(能源 App,双端上架):
- 近期:把「远程配置 + 特性开关 + 灰度」补起来,先覆盖"接口地址、功能开关、文案";
- 中期:把图标/主题做成可下发的静态资源(带版本);
- 远期:若出现"算法规则频繁变更"的明确需求,评估脚本引擎方案;
- 不做:自建 Dart 补丁热更新(iOS 受限 + 双端不一致 + 运维成本高,收益不匹配)。
动手练习
- 设计一个「远程配置中心」的 Dart 抽象接口(含本地缓存、兜底值、增量拉取、埋点四要素),写出关键类骨架;
- 用 Widget test 验证:配置拉取失败时页面按兜底值渲染,且不阻塞主流程;
- 给一个功能写"灰度开关 + 命中埋点"的最小实现。
自检清单
- [ ] 能区分热重载 / 热更新 / 动态化 / 远程配置
- [ ] 能说清 iOS 为什么禁止动态下发可执行代码
- [ ] 能在方案评审时给出"先远程配置、再资源动态化、最后才代码热更新"的排序
- [ ] 知道远程配置落地四要素(缓存 / 兜底 / 灰度 / 埋点)
常见误区
- "Flutter 官方支持热更新":官方没有线上热更新能力,网上方案都是第三方/自研;
- "Android 能热更,iOS 后面再说":双端行为不一致,方案评审时就要说清 iOS 怎么办;
- "远程配置就是热更新":配置不改变可执行代码,两者定位完全不同;
- "先上了再说":没有回滚和灰度,热更新一次事故比发版事故更致命。
相关文档
- 00 · 学习路线图 —— 阅读顺序
- 23 · 打包签名与发布上架 —— 发版流程是动态化的前提
- 38 · 埋点与行为分析 —— 灰度与开关必须有数据支撑
- 20 · 异常捕获与错误监控 —— 线上变更要可观测