Flutter 客户端热更新实战

21 阅读7分钟

本文将带你深入了解 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

第二步:注册账号并登录

  1. 访问 console.shorebird.dev 注册账号
  2. 在终端登录:
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$05,000个人项目、小型 MVP
Pro$2050,000中小型应用
Business$4001,000,000大型应用、企业项目
Enterprise定制定制特殊需求

对于大多数中小团队,Pro 计划 已足够使用。如果月安装量超出配额,按 $1/2,500 次计算超额费用。


总结

Shorebird 为 Flutter 开发者提供了成熟、合规、易用的热更新解决方案。通过本文的实战指南,你应该已经掌握了:

  1. 核心原理:理解 Shorebird 如何在 Android/iOS 上实现代码热更新
  2. 基础使用:从安装到推送第一个补丁的完整流程
  3. 进阶控制:手动管理更新、灰度发布、多通道测试
  4. CI/CD 集成:自动化构建发布流程
  5. 故障排查:常见问题的解决方案

热更新是一把双刃剑,合理使用可以大幅提升迭代效率,但也要注意:

  • 不能滥用热更新绕过商店审核
  • 遵守平台政策,不改变应用主要功能
  • 做好版本追踪和数据分析

参考资源