Flutter 客户端热更新实战
本文将带你深入了解 Flutter 热更新的实现原理,并手把手教你使用 Shorebird 实现生产环境的热更新能力。
为什么需要热更新?
作为 Flutter 开发者,你是否遇到过这样的场景:
- 刚上线的版本发现了一个紧急 Bug,但应用商店审核需要 2-5 天
- 想快速验证一个小功能的效果,不想等待漫长的审核周期
- 需要在不发布新版本的情况下修复线上问题
传统应用发布流程:代码修改 → 构建 → 提交审核 → 等待审核 → 发布 → 用户更新,整个过程可能需要数天甚至数周。
而热更新可以将这个周期缩短到 几分钟:代码修改 → 推送补丁 → 用户下次启动生效。
Flutter 热更新的技术挑战
Flutter 采用 AOT(Ahead-Of-Time)编译,代码在发布时已经编译为机器码。这与 React Native 的 JS Bundle 模式不同,Flutter 无法直接像 RN 那样替换代码包。
但 Flutter 官方团队核心成员创立的 Shorebird 项目,通过巧妙的技术方案解决了这个问题:
- Android:直接替换 Dart VM 中的编译代码,性能几乎无影响
- iOS:使用自定义 Dart 解释器 + 智能链接器,仅对修改部分使用解释执行,约 98% 的代码仍以 AOT 模式全速运行
更重要的是,Shorebird 完全符合 Google Play 和 Apple App Store 的政策要求。
Shorebird 快速上手
第一步:安装 CLI 工具
# macOS / Linux
curl --proto '=https' --tlsv1.2 https://raw.githubusercontent.com/shorebirdtech/install/main/install.sh -sSf | bash
# 验证安装
shorebird --version
安装完成后,运行诊断命令确保环境正常:
shorebird doctor
第二步:注册账号并登录
- 访问 console.shorebird.dev 注册账号
- 在终端登录:
shorebird login
这将打开浏览器完成认证,成功后会显示登录信息。
第三步:初始化项目
对于现有 Flutter 项目,只需执行:
cd your_flutter_project
shorebird init
这会在项目根目录生成 shorebird.yaml 配置文件:
# shorebird.yaml
app_id: ee322dc4-3dc2-4324-90a9-04c40a62ae76
# auto_update: false # 取消注释可禁用自动更新
注意:
app_id可以公开,不是敏感信息。
第四步:创建 Release
在推送热更新之前,必须先创建一个基准版本:
# Android
shorebird release android
# iOS(需要 macOS)
shorebird release ios
命令执行后会看到类似输出:
✓ Building release (17.0s)
✓ Fetching apps (0.3s)
✓ Detecting release version (0.2s)
🚀 Ready to create a new release!
📱 App: my_app (51751336-6a7c-4972-b4ec-8fc1591fb2b3)
📦 Release Version: 1.0.0+1
🕹️ Platform: android (arm64, arm32, x86_64)
Would you like to continue? (y/N) Yes
✓ Published Release!
生成的文件位置:
-
Android:
./build/app/outputs/bundle/release/app-release.aab -
iOS:
./build/ios/ipa/<app_name>.ipa
第五步:上传到应用商店
Android (Google Play) : 将生成的 .aab 文件上传到 Google Play Console。
iOS (App Store) :
推荐使用以下方式之一上传:
# 方式一:使用 Transporter(推荐)
# 在 App Store 下载 Transporter 应用,然后拖入 .ipa 文件
# 方式二:使用 xcrun notarytool(命令行)
xcrun notarytool submit build/ios/ipa/app.ipa \
--apple-id YOUR_APPLE_ID \
--password YOUR_APP_SPECIFIC_PASSWORD \
--team-id YOUR_TEAM_ID
# 方式三:使用 Xcode
# 打开 Xcode → Window → Organizer → 选择归档 → Distribute App
iOS 重要提示:在 Xcode 分发时,必须取消勾选 "Manage Version and Build Number",否则会影响补丁功能。
第六步:推送热更新补丁
当发现 Bug 或需要更新时:
# 修改 Dart 代码后,推送补丁
shorebird patch android
# 推送到指定通道
shorebird patch android --track=staging
输出示例:
✓ Building patch (17.2s)
✓ Fetching apps (0.7s)
✓ Detecting release version (0.2s)
🚀 Ready to publish a new patch!
📱 App: my_app (...)
📦 Release Version: 1.0.0+1
📺 Channel: stable
🕹️ Platform: android [arm64 (135 B), arm32 (150 B), x86_64 (135 B)]
Would you like to continue? (y/N) Yes
✅ Published Patch!
恭喜!你的第一个热更新补丁已经推送成功。用户下次启动应用时会自动下载并应用更新。
进阶:手动控制更新流程
默认情况下,Shorebird 会在应用启动时自动检查并下载更新。但有时你需要更精细的控制,比如:
- 显示更新进度
- 用户确认后再应用更新
- 指定更新通道进行灰度测试
添加依赖
# pubspec.yaml
dependencies:
shorebird_code_push: ^2.0.5
代码实现
import 'package:shorebird_code_push/shorebird_code_push.dart';
class MyApp extends StatefulWidget {
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final updater = ShorebirdUpdater();
@override
void initState() {
super.initState();
_checkCurrentPatch();
}
Future<void> _checkCurrentPatch() async {
final patch = await updater.readCurrentPatch();
print('当前补丁版本: ${patch?.number}');
}
/// 手动检查并安装更新
Future<void> checkForUpdates() async {
try {
final status = await updater.checkForUpdate();
if (status == UpdateStatus.outdated) {
// 有新版本可用
final shouldUpdate = await _showUpdateDialog();
if (shouldUpdate) {
await updater.update();
_showSuccessDialog('更新成功,下次启动生效');
}
} else if (status == UpdateStatus.current) {
_showInfoDialog('已是最新版本');
}
} on UpdateException catch (e) {
_showErrorDialog('更新失败: $e');
}
}
Future<bool> _showUpdateDialog() async {
return await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text('发现新版本'),
content: Text('检测到有新的更新,是否立即下载?'),
actions: [
TextButton(
child: Text('稍后'),
onPressed: () => Navigator.pop(context, false),
),
ElevatedButton(
child: Text('立即更新'),
onPressed: () => Navigator.pop(context, true),
),
],
),
) ?? false;
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('热更新示例')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: checkForUpdates,
child: Text('检查更新'),
),
],
),
),
),
);
}
}
禁用自动更新
如果希望完全手动控制更新时机,在 shorebird.yaml 中禁用自动更新:
# shorebird.yaml
app_id: your-app-id
auto_update: false
灰度发布与测试
使用多通道进行灰度发布
Shorebird 支持多个发布通道,非常适合灰度测试:
# 创建测试通道补丁
shorebird patch android --track=beta
# 创建预发布通道补丁
shorebird patch android --track=staging
# 创建生产通道补丁(默认)
shorebird patch android --track=stable
代码中指定通道
// 使用预定义的 beta 通道(推荐)
final status = await updater.checkForUpdate(track: UpdateTrack.beta);
if (status == UpdateStatus.outdated) {
await updater.update(track: UpdateTrack.beta);
}
// 或使用自定义通道名称
const customTrack = UpdateTrack('my-custom-track');
final status = await updater.checkForUpdate(track: customTrack);
if (status == UpdateStatus.outdated) {
await updater.update(track: customTrack);
}
完整的灰度发布流程
# 1. 推送测试版本到 beta 通道
shorebird patch android --track=beta
# 2. 本地预览(可选)
shorebird preview --track beta --app-id YOUR_APP_ID --release-version 1.0.0+1
# 3. 测试通过后,提升到 stable 通道
# 方式一:在控制台操作 https://console.shorebird.dev
# 方式二:重新推送到 stable 通道
shorebird patch android --track=stable
CI/CD 集成
在自动化构建流程中集成 Shorebird,实现一键发布:
生成 CI Token
shorebird login:ci
输出:
A token was generated.
SHOREBIRD_TOKEN="eyJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ..."
You can use this token in CI environments by setting the SHOREBIRD_TOKEN environment variable.
GitHub Actions 配置
# .github/workflows/release.yml
name: Release with Shorebird
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.16.0'
- name: Setup Shorebird
uses: shorebirdtech/setup-shorebird@v1
- name: Get dependencies
run: flutter pub get
- name: Release Android
env:
SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }}
run: |
shorebird release android --force
- name: Upload to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
packageName: com.example.app
releaseFiles: build/app/outputs/bundle/release/app-release.aab
track: internal
patch:
runs-on: ubuntu-latest
needs: release
if: github.event_name == 'push' && contains(github.ref, 'patch')
steps:
- uses: actions/checkout@v4
- name: Setup Shorebird
uses: shorebirdtech/setup-shorebird@v1
- name: Push Patch
env:
SHOREBIRD_TOKEN: ${{ secrets.SHOREBIRD_TOKEN }}
run: |
shorebird patch android --force
常见问题与解决方案
Q1: 补丁推送后用户没有收到更新?
排查步骤:
# 1. 确认补丁已发布到正确通道
shorebird patches list
# 2. 检查应用版本号是否匹配
# 补丁只能应用于相同的 release 版本
# 3. 确认补丁发布到了 stable 通道(默认)
# 如果推送到 beta/staging,需要手动指定通道
注意:补丁在应用第二次启动时才会生效,这是设计如此。
Q2: iOS 补丁体积为什么比 Android 大很多?
这是正常现象:
| 平台 | 补丁大小 | 原因 |
|---|---|---|
| Android | 通常 KB 级别 | 直接替换编译代码 |
| iOS | 通常几百 KB | 使用解释器模式,包含额外元数据 |
iOS 由于 App Store 政策限制,必须使用解释器执行更新代码,因此补丁体积较大。
Q3: 构建失败怎么办?
# 1. 检查环境
shorebird doctor
# 2. 清理缓存
shorebird cache clear
# 3. 重置 Shorebird 配置
rm -rf .shorebird
shorebird init
# 4. 检查 Flutter 版本
# Shorebird 需要使用其提供的 Flutter 版本
flutter --version
Q4: 哪些场景不能使用热更新?
| 场景 | 能否热更新 | 说明 |
|---|---|---|
| 修改 Dart 代码 | ✅ 支持 | 最常见的使用场景 |
| 修改 UI 布局 | ✅ 支持 | 纯 Dart 实现 |
| 修改业务逻辑 | ✅ 支持 | 纯 Dart 实现 |
| 修改原生代码(Java/Kotlin/Swift/ObjC) | ❌ 不支持 | 需要发布新版本 |
| 新增权限 | ❌ 不支持 | 需要商店审核 |
| 更新原生依赖版本 | ❌ 不支持 | 需要发布新版本 |
| 改变应用主要功能 | ❌ 违规 | 违反商店政策 |
Q5: 如何追踪补丁版本用于数据分析?
void trackPatchVersion() async {
final patchNumber = await ShorebirdUpdater().readCurrentPatch();
// 上报到分析平台
analytics.logEvent('app_launch', {
'app_version': '1.0.0',
'patch_number': patchNumber?.number ?? 0,
});
}
Q6: 补丁会改变应用版本号吗?
不会。补丁是在相同版本号下的更新,不会修改 pubspec.yaml 中的版本号。
这可能导致分析平台无法区分热更新,建议通过补丁号进行追踪(见上一条)。
最佳实践
1. 合理规划版本发布策略
┌─────────────────────────────────────────────────────────────┐
│ 推荐发布策略 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Release (商店版本): │
│ - 主要功能更新 │
│ - 原生代码变更 │
│ - 频率:每 2-4 周 │
│ │
│ Patch (热更新): │
│ - 紧急 Bug 修复 │
│ - UI/文案调整 │
│ - 业务逻辑优化 │
│ - 频率:按需,随时推送 │
│ │
└─────────────────────────────────────────────────────────────┘
2. 使用语义化版本号
# pubspec.yaml
version: 1.0.0+1
# 版本格式:major.minor.patch+buildNumber
# - major: 重大功能变更
# - minor: 功能迭代
# - patch: Bug 修复
# - buildNumber: 构建号(每次构建递增)
3. 建立 Staging 测试流程
# 开发 → Staging 测试 → 生产发布
# 1. 推送到 staging
shorebird patch android --track=staging
# 2. 测试人员验证
shorebird preview --track staging
# 3. 验证通过后提升到 stable
# 在控制台操作或使用命令
4. 为用户提供清晰的更新提示
// ✅ 好的做法
class UpdatePrompt {
void showUpdateNotice(String version) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('发现新版本'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('版本 $version 包含以下更新:'),
SizedBox(height: 8),
Text('• 修复了登录页闪退问题'),
Text('• 优化了首页加载速度'),
Text('• 更新了用户协议'),
],
),
actions: [
TextButton(
child: Text('稍后'),
onPressed: () => Navigator.pop(context),
),
ElevatedButton(
child: Text('立即更新'),
onPressed: applyUpdate,
),
],
),
);
}
}
// ❌ 不好的做法:静默更新且不告知用户
// 这可能导致用户困惑,且违反某些地区的用户知情权法规
定价与方案选择
Shorebird 提供灵活的定价方案,适合不同规模的团队:
| 计划 | 月费 | 月安装量 | 适用场景 |
|---|---|---|---|
| Free | $0 | 5,000 | 个人项目、小型 MVP |
| Pro | $20 | 50,000 | 中小型应用 |
| Business | $400 | 1,000,000 | 大型应用、企业项目 |
| Enterprise | 定制 | 定制 | 特殊需求 |
对于大多数中小团队,Pro 计划 已足够使用。如果月安装量超出配额,按 $1/2,500 次计算超额费用。
总结
Shorebird 为 Flutter 开发者提供了成熟、合规、易用的热更新解决方案。通过本文的实战指南,你应该已经掌握了:
- 核心原理:理解 Shorebird 如何在 Android/iOS 上实现代码热更新
- 基础使用:从安装到推送第一个补丁的完整流程
- 进阶控制:手动管理更新、灰度发布、多通道测试
- CI/CD 集成:自动化构建发布流程
- 故障排查:常见问题的解决方案
热更新是一把双刃剑,合理使用可以大幅提升迭代效率,但也要注意:
- 不能滥用热更新绕过商店审核
- 遵守平台政策,不改变应用主要功能
- 做好版本追踪和数据分析