Flutter 基础体系 · 第 22/80 篇。示例基于当前稳定 Flutter 与 Dart 3 语言能力;Android、iOS、桌面和 Web 差异会明确说明。
Flutter 应用发布:签名、Flavor、商店、Web/桌面、灰度和回滚
1. 先确定“发布物”是什么
Flutter 项目通常不是只生成一个文件。一次发布至少包含以下维度:
| 目标 | 典型产物 | 发布渠道 | 是否需要平台签名 |
|---|---|---|---|
| Android | .aab、.apk |
Google Play、企业分发、其他商店 | 是 |
| iOS | .ipa |
App Store、TestFlight、企业或特定分发渠道 | 是 |
| Web | build/web 静态文件 |
CDN、对象存储、Web 服务器 | 没有应用商店签名,但需要 HTTPS 和部署安全 |
| Windows | .exe、MSIX 等 |
Microsoft Store、官网、企业分发 | 通常应进行代码签名 |
| macOS | .app、.dmg、.pkg |
Mac App Store、官网 | 通常需要代码签名和公证 |
| Linux | 二进制、tar、deb、rpm 等 | 官网、软件仓库、发行版渠道 | 没有统一的 Flutter 商店签名模型 |
Flutter 的 release 模式只表示“针对生产运行时优化并关闭调试能力”,不等于已经具备商店要求的签名、版本号、隐私声明、权限说明和分发配置。
一个较完整的发布链路可以表示为:
flowchart LR
A[源代码和依赖锁定] --> B[CI 构建]
B --> C[Flavor 与环境配置]
C --> D[编译和打包]
D --> E[签名]
E --> F[自动验证]
F --> G[内部测试]
G --> H[商店审核或企业分发]
H --> I[灰度发布]
I --> J[全量发布]
I --> K[暂停/回滚]
J --> L[监控与复盘]
K --> M[兼容性修复版本]
这里有三个容易混淆的概念:
- 构建:把 Dart、Flutter 框架和平台代码编译成目标平台产物。
- 签名:证明产物来自某个受信任的发布者,并允许平台验证其完整性和升级关系。
- 发布:把签名后的产物提交给商店或服务器,并控制哪些用户可以获得它。
只有完成三者,才形成可交付的软件版本。
2. 版本号、构建号与升级关系
Flutter 项目通常在 pubspec.yaml 中声明版本:
version: 1.4.2+87
这里:
1.4.2是面向用户的版本名称;87是构建号;+后面的值不直接展示给用户,但平台用它判断构建是否更新。
常见映射如下:
| Flutter 版本字段 | Android | iOS |
|---|---|---|
1.4.2 |
versionName |
CFBundleShortVersionString |
87 |
versionCode |
CFBundleVersion |
平台通常要求同一个应用的新上传构建号严格递增。因此,下面的构建通常无效:
已上传:1.4.2+87
再次上传:1.4.2+86
即使代码内容不同,商店也可能拒绝它,因为构建号不能倒退。
版本号和构建号解决的是“识别和排序”,不解决回滚。回滚时也不能简单地重新上传旧构建号;通常需要:
- Android:暂停当前灰度,或发布一个更高版本号的修复版本;
- iOS:停止当前版本的进一步发布,重新提交一个新的构建或版本;
- Web:切换服务器指向,但必须处理浏览器和 CDN 缓存;
- 桌面:重新分发一个更高版本的安装包,或切换下载渠道。
3. Release、Profile 与 Debug
Flutter 常见的构建模式有三种:
debug:用于开发,包含断言、调试服务和较多诊断能力;profile:用于性能分析,保留部分分析能力;release:用于生产分发,关闭调试服务并进行优化。
典型命令:
flutter run
flutter run --profile
flutter run --release
flutter build apk --release
flutter build appbundle --release
flutter build ipa --release
flutter build web --release
flutter build windows --release
flutter build macos --release
flutter build linux --release
默认情况下,flutter build 多数目标会使用发布配置,但在 CI 中显式写出 --release 更容易审查,也避免脚本默认值变化造成误解。
发布构建需要特别验证:
flutter analyze
flutter test
flutter build appbundle --release
这三步分别验证静态分析、Dart/Flutter 测试和 Android 生产产物。它们不能互相替代:测试通过不代表 release 编译通过,release 编译通过也不代表真实设备上的权限、深链接和商店安装流程正确。
4. Flavor:同一套代码生成不同应用
4.1 Flavor 的含义
Flavor 是一组命名的构建变体。它可以改变:
- 应用 ID;
- 应用名称和图标;
- API 基地址;
- Firebase 或其他平台配置;
- 日志、崩溃上报和功能开关;
- 是否允许调试菜单;
- Android/iOS 的原生资源和权限。
例如:
staging:测试环境,com.example.app.staging
production:生产环境,com.example.app
Flavor 不是“运行时切换环境”的同义词。它主要在构建时决定原生资源和编译输入;运行时远程配置则是另一种机制。
4.2 Dart 入口与 --dart-define
可以为不同环境建立不同入口:
lib/
main.dart
main_staging.dart
app.dart
lib/main_staging.dart:
import 'app.dart';
void main() {
runApp(const App());
}
lib/main.dart:
import 'app.dart';
void main() {
runApp(const App());
}
环境参数可以使用编译时常量:
class BuildConfig {
static const apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'https://api.example.com',
);
static const appEnvironment = String.fromEnvironment(
'APP_ENV',
defaultValue: 'production',
);
static const enableDebugPanel = bool.fromEnvironment(
'ENABLE_DEBUG_PANEL',
defaultValue: false,
);
}
构建命令:
flutter build apk \
--release \
--flavor staging \
-t lib/main_staging.dart \
--dart-define=APP_ENV=staging \
--dart-define=API_BASE_URL=https://staging-api.example.com \
--dart-define=ENABLE_DEBUG_PANEL=true
如果使用文件集中管理参数,可以使用 Flutter 支持的 --dart-define-from-file:
{
"APP_ENV": "staging",
"API_BASE_URL": "https://staging-api.example.com",
"ENABLE_DEBUG_PANEL": "true"
}
flutter build apk \
--release \
--flavor staging \
-t lib/main_staging.dart \
--dart-define-from-file=config/staging.json
这里的值会进入编译产物。即使配置文件没有提交到 Git,最终 APK、IPA、Web JavaScript 或桌面程序也可能包含这些值。因此以下内容不能放入 dart-define:
- API 私钥;
- 云服务管理员凭据;
- 数据库密码;
- 对所有客户端都相同的签名密钥;
- 能绕过服务器授权的令牌。
dart-define 适合放公开的环境标识和接口地址,不适合保存秘密。
4.3 Android Flavor 配置
Android 端需要在 Gradle 中声明 flavor。以 Groovy DSL 为例,android/app/build.gradle 可以包含:
android {
namespace "com.example.app"
compileSdk flutter.compileSdkVersion
defaultConfig {
applicationId "com.example.app"
minSdk flutter.minSdkVersion
targetSdk flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
flavorDimensions "environment"
productFlavors {
staging {
dimension "environment"
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
}
production {
dimension "environment"
}
}
}
构建:
flutter build appbundle --release --flavor production -t lib/main.dart
flutter build apk --release --flavor staging -t lib/main_staging.dart
执行成功后,Android 产物通常位于:
build/app/outputs/bundle/productionRelease/
build/app/outputs/flutter-apk/
具体目录会随 Flutter 和 Android Gradle Plugin 模板变化,CI 不应盲目假设所有版本的路径完全相同。更可靠的做法是构建后显式查找 .aab 或 .apk,并把文件复制到统一的 artifacts 目录。
Android flavor 的关键约束是:发布到同一个商店应用的构建必须使用同一个 application ID。如果 staging 使用 com.example.app.staging,它会被商店视为另一个应用,不能覆盖 com.example.app 的生产安装。
4.4 iOS 的 Flavor 实际上依赖 Scheme 和 Build Configuration
iOS 没有 Android productFlavors 的同名机制。Flutter 项目通常通过 Xcode 的以下对象组合实现环境:
- Build Configuration,例如
Debug-staging、Release-staging; - Scheme,例如
staging、production; - 不同的 Bundle Identifier;
- 不同的
.xcconfig或原生资源文件。
在 Xcode 中为每个环境创建 Scheme,并让 Scheme 选择对应的 Build Configuration。生产环境还必须配置正确的:
- Apple Developer Team;
- Bundle Identifier;
- 签名证书;
- Provisioning Profile;
- Associated Domains、Push Notifications 等 Entitlements。
构建命令形式为:
flutter build ipa \
--release \
--flavor production \
-t lib/main.dart
这里的 --flavor production 需要对应 Xcode 中存在的 Scheme。若 Scheme 不存在、名称大小写不一致,常见失败表现是 Flutter 找不到目标配置或 Xcode 构建失败。
一个实用的区分方式是:
- Dart
--dart-define:传递跨平台的编译时参数; - Android Gradle flavor:决定 Android 应用变体;
- iOS Scheme/Configuration:决定 iOS 应用变体;
- 远程配置:应用安装后动态决定部分功能。
四者可以组合,但不能把其中一个误认为其他三个。
5. 签名:平台如何确认发布者和升级关系
5.1 签名的基本过程
数字签名可以抽象为:
- 对产物内容计算摘要
h = Hash(package); - 使用私钥
sk对摘要签名,得到s = Sign(sk, h); - 把签名和证书链附加到产物;
- 设备或商店使用公钥
pk验证:- 签名是否由对应私钥产生;
- 证书是否受信任;
- 产物内容是否被修改;
- 新版本是否允许覆盖旧版本。
如果产物内容变为 package',通常有:
Hash(package') != Hash(package)
原签名就无法验证。因此“重新压缩一下 APK/IPA 再上传”可能破坏签名。
签名不是加密:
- 用户仍然可能反编译客户端;
- 签名不能隐藏 Dart 常量;
- 签名不能阻止用户分析公开 API 地址;
- 签名主要保证来源、完整性和平台升级规则。
5.2 Android 签名
Android 发布至少涉及两个概念:
- 上传密钥:开发者用来向 Google Play 上传构建;
- 应用签名密钥:Google Play 用来为最终分发 APK 签名。
使用 Play App Signing 时,上传密钥泄露通常可以通过重新注册上传证书来恢复;应用签名密钥的处理则更关键。不同商店的托管和签名规则可能不同,不能把 Google Play 的流程直接套到其他渠道。
生成上传密钥示例:
keytool -genkeypair \
-v \
-keystore android/upload-keystore.jks \
-alias upload \
-keyalg RSA \
-keysize 2048 \
-validity 10000
命令会要求输入密码和证书信息。android/upload-keystore.jks 不应提交到公共仓库。
可以把路径和密码放在本地或 CI 的安全变量中,例如 android/key.properties:
storePassword=...
keyPassword=...
keyAlias=upload
storeFile=/secure/path/upload-keystore.jks
Gradle 中读取并应用签名配置时,应避免在没有密钥的机器上悄悄回退到 debug 签名。典型逻辑是:
def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
if (keystorePropertiesFile.exists()) {
keyAlias keystoreProperties["keyAlias"]
keyPassword keystoreProperties["keyPassword"]
storeFile file(keystoreProperties["storeFile"])
storePassword keystoreProperties["storePassword"]
}
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
生产 CI 中还应增加显式检查:如果 release 没有有效签名配置,直接失败,而不是生成一个看似成功但无法上架的包。
验证 APK 签名:
$ANDROID_HOME/build-tools/<version>/apksigner verify --verbose app-release.apk
预期结果应包含验证成功的信息。对 AAB,可以先用 Google Play 的测试轨道验证,或使用 Android 工具检查其签名信息;不能把 AAB 当作可直接安装的 APK。
常见 Android 失败:
INSTALL_FAILED_UPDATE_INCOMPATIBLE:新旧包签名不一致,设备无法覆盖安装;- 商店提示证书不匹配:上传了错误 keystore 或应用 ID 不对应;
versionCode已存在:构建号没有递增;- 本地能构建、CI 失败:密钥路径是本机绝对路径,CI 中不存在。
5.3 iOS 签名与 Provisioning Profile
iOS 签名不仅是给 IPA 加一个证书。Provisioning Profile 还描述了:
- 哪个 App ID 可以运行;
- 哪个 Team 签发;
- 哪些设备或分发方式适用;
- 哪些 Entitlements 被允许。
因此,即使 Bundle Identifier 正确,Push Notifications、Associated Domains 等能力没有正确写入 Entitlements,运行或审核仍可能失败。
常见分发类型包括:
- Development:开发设备;
- Ad Hoc:登记设备的有限分发;
- App Store:上传 App Store 或 TestFlight;
- 企业分发:需要满足 Apple 的企业计划和用途要求,不能当作普通公网分发手段。
使用 Xcode 自动签名时,机器需要登录有权限的 Apple 开发者账号,并且 CI 还需要处理证书和 profile 的安全导入。使用手工签名时,必须让证书、Profile、Bundle ID 和 Entitlements 成套匹配。
构建 IPA:
flutter build ipa --release --flavor production -t lib/main.dart
验证 .app 的签名可使用:
codesign --verify --strict --verbose=2 path/to/Runner.app
如果要在 macOS 分发到非 App Store 渠道,还通常需要配合 Developer ID 签名、公证和 Gatekeeper 验证。一个命令成功只说明当前检查通过,不代表 Apple 商店审核一定通过,因为隐私用途描述、数据收集声明和权限使用仍是独立要求。
6. 商店发布不是“上传文件”这么简单
6.1 Android 商店
Google Play 常用 .aab,因为商店可以针对设备生成更合适的 APK。发布前应检查:
applicationId是否为生产值;versionCode是否高于已发布版本;- target SDK 是否满足当前商店政策;
- 64 位和 ABI 配置是否符合目标设备;
- 权限和数据安全表单是否真实;
- 签名是否使用正确的上传密钥;
- 深链接、通知和应用内购买是否在发布包中可用。
签名验证、安装验证和商店验证是三个不同层次:
本地签名验证通过
≠
真实设备安装和升级通过
≠
Google Play 审核通过
例如,应用可能签名正确,但因为生产服务器拒绝了新版本的 API 请求,升级后启动即报错。
6.2 iOS App Store 和 TestFlight
iOS 构建上传到 App Store Connect 后,通常经历:
- 上传构建;
- Apple 处理构建;
- TestFlight 内部或外部测试;
- 提交 App Review;
- 手动或自动发布;
- 可选的分阶段发布。
TestFlight 测试的是构建在 Apple 分发链路中的行为,但不等于生产用户规模。仍需验证:
- App Store 构建使用的 Bundle Identifier;
- 推送环境;
- App Tracking Transparency 等权限流程;
- 隐私清单和数据收集声明;
- 生产后端和第三方 SDK 配置;
- 首次安装、升级安装和恢复安装。
iOS 的应用版本通常不能像服务器部署那样瞬间替换所有用户。已经安装旧版本的用户可能继续使用旧客户端;已经安装新版本的用户也可能无法被商店强制降级。因此客户端必须尽量向后兼容服务器。
7. Web 发布:没有签名,但缓存和安全成为核心
7.1 构建与部署
构建 Web:
flutter build web --release
产物位于:
build/web/
典型目录包含入口 HTML、JavaScript、资源文件和字体等。部署时应把整个目录上传到静态服务器或 CDN,而不是只上传 index.html。
如果应用使用路径式路由,例如:
/orders/123
服务器需要配置 SPA fallback:当静态文件不存在时,将请求回退到 index.html。否则用户直接刷新深层 URL 时会得到 404。
Web 还有一个与移动端不同的失败路径:
用户访问 index.html
-> index.html 引用旧或新的 JS
-> CDN 缓存组合不一致
-> JS 找不到对应资源
-> 白屏或加载失败
因此应区分缓存策略:
index.html:短缓存或不缓存;- 带内容哈希的 JS、字体、图片:可以长缓存;
- 发布时原子更新资源目录,避免入口文件先指向尚未上传的资源。
如果 CDN 的旧节点仍返回旧入口,而资源已被删除,就可能造成部分用户白屏。回滚 Web 时,不能只恢复入口文件,还要确保入口引用的资源仍然存在。
7.2 Web 没有客户端秘密
Web 应用的 JavaScript、配置和资源最终都会发给浏览器。以下写法并不能保护秘密:
const secret = String.fromEnvironment('API_SECRET');
它只是在构建时注入,最终仍可能被用户下载和分析。真正的秘密必须留在服务器,通过服务端鉴权、短期令牌和最小权限接口使用。
Web 发布还应验证:
- HTTPS;
- CSP 和资源来源;
- CORS;
- Service Worker 或浏览器缓存策略;
- OAuth 回调地址;
- 文件上传和下载权限;
- WebView 与浏览器行为差异;
- 不把用户隐私数据写入 URL、日志或前端持久化存储。
Flutter Web 的渲染器、浏览器支持和包兼容性会随 Flutter 稳定版本变化。不能仅依据移动端测试结果宣称 Web 等价可用;应在目标浏览器、低性能设备和受限网络中执行发布验证。
8. 桌面发布:平台差异比 Flutter 命令更重要
8.1 Windows
构建:
flutter build windows --release
输出通常在:
build/windows/x64/runner/Release/
Windows 桌面应用可以通过压缩包、安装器、MSIX 或 Microsoft Store 分发。不同方式对以下内容的要求不同:
- 安装和卸载;
- 应用数据目录;
- 自动更新;
- 文件关联;
- 防火墙和网络权限;
- 代码签名;
- SmartScreen 信任;
- CPU 架构。
未签名的 .exe 可能被系统或杀毒软件提高风险提示。签名证书应由 CI 安全使用,不能把私钥随安装包分发。
8.2 macOS
构建:
flutter build macos --release
macOS 官网分发通常需要:
- Developer ID 签名;
- 对
.app或安装包进行封装; - 提交 Apple 公证;
- Staple 公证票据;
- 在干净机器上用 Gatekeeper 验证。
验证示例:
codesign --verify --deep --strict --verbose=2 path/to/Runner.app
spctl --assess --type execute --verbose path/to/Runner.app
--deep 可以帮助诊断嵌套代码,但生产签名策略仍应对每个嵌套组件正确签名,而不是依赖最后一步“递归修补”。
沙盒权限、摄像头、麦克风、文件访问和网络权限需要在 macOS 原生配置中声明。Flutter 层调用成功不代表系统一定授予权限。
8.3 Linux
构建:
flutter build linux --release
Linux 没有一个覆盖所有发行版的官方统一应用商店签名流程。发布时要考虑:
- glibc 或系统库兼容性;
- deb、rpm、tar 或其他包格式;
- 桌面文件和图标;
- 依赖安装;
- 自动更新;
- 发行版安全策略。
Linux 的“可执行”不等于“可在所有 Linux 发行版运行”。应在目标发行版或容器化构建环境中验证。
8.4 桌面 Flavor 的边界
Android 和 iOS 的 --flavor 有明确的平台构建概念;Flutter 桌面端并不天然提供完全相同的 flavor 模型。桌面环境通常使用:
- 不同的
--dart-define; - 不同的原生工程配置;
- 不同的签名和打包脚本;
- 不同的安装包名称和资源目录。
因此,CI 不应假设下面的命令在所有桌面目标上都等价有效:
flutter build windows --flavor staging
应先确认当前 Flutter 版本和目标平台工具是否支持该参数,再决定采用原生配置还是编译时变量。
9. 灰度发布:控制“谁拿到哪个版本”
9.1 灰度的形式化定义
灰度发布是让用户集合 U 被划分为不同发布组:
U = U_old ∪ U_new
U_old ∩ U_new = ∅
其中:
U_old使用旧版本;U_new使用新版本;- 两者并集覆盖当前参与发布的用户。
最简单的随机分配是:
group = hash(stable_user_id) mod 100
if group < p:
使用新版本
else:
使用旧版本
p 是灰度比例。例如 p = 5 表示理论上约 5% 的用户进入新版本。
必须使用稳定标识。若每次启动都随机分配,用户可能在新旧版本之间反复切换,导致:
- 同一用户的行为数据无法比较;
- 数据库迁移重复触发;
- 问题复现困难;
- 版本体验不稳定。
移动商店灰度与服务端功能灰度是两种不同层次:
- 商店灰度:控制哪些用户可以下载新客户端;
- 服务端灰度:同一个客户端中,控制哪些用户启用新功能。
二者可以组合。例如先把新客户端投放给 5% 用户,再只对其中 20% 开启新支付流程。此时真实受影响比例不是简单地看某一个开关,而要计算交集。
9.2 Android 与 iOS 的灰度差异
Android 商店通常支持测试轨道和分阶段发布。发布比例提高前,应比较:
- 崩溃率;
- ANR;
- 启动失败;
- 登录失败;
- 支付成功率;
- 关键接口错误率;
- 机型和系统版本分布。
暂停灰度只能阻止更多用户获得新版本,不能让已经安装新版本的用户自动回到旧版本。真正的回滚可能需要:
- 服务端关闭新功能;
- 发布兼容性修复;
- 引导用户更新到修复版本;
- 在极端情况下通过商店或企业渠道重新分发旧版本,但这受平台限制。
iOS 支持 TestFlight 测试和商店分阶段发布,但已经安装新版本的用户同样不会因为停止发布而自动降级。iOS 商店通常不能把已安装用户静默退回旧版本。
9.3 Web 灰度
Web 灰度可以在 CDN、边缘网关或服务端路由层完成:
请求
-> 根据 cookie、用户 ID 或地域计算 cohort
-> cohort=A 返回 release-2025-01
-> cohort=B 返回 release-2025-02
关键是入口文件、资源目录和 API 兼容性必须成套。不能让用户拿到新 index.html,却从缓存中加载旧资源,除非资源命名和兼容策略明确支持这种组合。
Web 灰度常用 cookie 保持用户分组。若按照 IP 分组,移动网络、企业代理和 NAT 会导致多个用户共享分组;若按照每次请求随机分组,则刷新页面可能在不同版本之间跳转。
10. 回滚不是“把版本号改回去”
10.1 回滚对象
一次发布至少有四个可能的回滚对象:
- 客户端安装包;
- Web 静态资源;
- 服务端 API;
- 远程配置或功能开关。
它们的恢复速度和风险不同:
| 对象 | 通常能否立即回滚 | 主要限制 |
|---|---|---|
| 服务端开关 | 通常可以 | 新旧客户端都必须理解该状态 |
| Web 入口和资源 | 通常较快 | CDN、浏览器缓存和资源完整性 |
| API 服务 | 可以切换版本 | 数据库迁移和已写入数据 |
| Android/iOS 客户端 | 不能保证立即降级 | 商店审核、用户安装状态和构建号规则 |
因此,生产回滚优先设计为“关闭风险功能并恢复服务端兼容路径”,而不是依赖商店让所有客户端降级。
10.2 一个完整故障例子
假设版本 2.0.0+120 引入了新的订单接口,旧客户端使用:
POST /orders
新客户端使用:
POST /v2/orders
若服务端先删除旧接口,再发布新客户端,仍在使用 1.9.0 的用户会失败。正确顺序应是:
- 服务端先支持
/orders和/v2/orders; - 发布客户端
2.0.0+120; - 灰度观察新客户端;
- 确认旧版本占比足够低;
- 再考虑废弃旧接口。
数据库迁移也有类似约束。假设旧版本读取字段 name,新版本读取字段 display_name。直接删除 name 会使旧客户端崩溃。更安全的迁移过程是:
阶段 1:增加 display_name,保留 name
阶段 2:服务端双写 name 和 display_name
阶段 3:发布读取 display_name 的客户端
阶段 4:确认旧客户端比例下降
阶段 5:停止双写
阶段 6:最后删除 name
这称为向后兼容的渐进式迁移。其因果关系是:客户端升级不是瞬时完成的,所以服务端和数据库必须在一段时间内同时支持多个协议版本。
10.3 回滚状态机
可以把灰度发布抽象成状态机:
stateDiagram-v2
[*] --> Built: 构建
Built --> Signed: 签名
Signed --> Verified: 自动验证
Verified --> Internal: 内部测试
Internal --> Review: 提交商店/分发
Review --> Canary: 小比例灰度
Canary --> Expanded: 扩大比例
Expanded --> Full: 全量
Canary --> Paused: 指标异常
Expanded --> Paused: 指标异常
Full --> Paused: 生产事故
Paused --> Mitigated: 关闭开关/恢复服务端
Mitigated --> FixedRelease: 发布修复版本
FixedRelease --> Canary
每次状态转换都应有验证条件。例如:
Verified -> Internal:
签名有效,安装成功,启动成功,关键冒烟测试通过
Canary -> Expanded:
新版本崩溃率、登录成功率、支付成功率没有超过阈值
Paused -> Mitigated:
新功能关闭,旧 API 恢复,错误率回落并持续观察
如果没有定义“什么情况下扩大灰度”和“什么情况下暂停”,灰度就只是慢速全量发布,而不是风险控制。
11. 发布验证:验证产物,不只验证源代码
11.1 构建前验证
flutter doctor -v
flutter pub get
flutter analyze
flutter test
如果项目使用代码生成,还应执行对应生成命令,并确认生成结果已纳入构建。依赖应使用锁定文件构建,避免 CI 在同一个提交上解析出不同版本。
发布前应确认:
- Flutter SDK 版本;
- Dart SDK 版本;
- Android SDK、NDK 和 Gradle 工具链;
- Xcode 和 macOS 版本;
- 桌面打包工具;
- 依赖是否包含平台不支持的插件。
11.2 产物验证
Android:
flutter build appbundle --release --flavor production
验证内容:
- 包名;
- 版本名和版本号;
- 签名证书;
- 资源和图标;
- release 模式下的 API 地址;
- AndroidManifest 权限;
- 真实设备安装和升级。
iOS:
flutter build ipa --release --flavor production
验证内容:
- Bundle Identifier;
- 签名和 Profile;
- Entitlements;
- 推送环境;
- 真实设备安装;
- 从旧版本升级;
- 卸载重装和恢复数据。
Web:
flutter build web --release
验证内容:
- 根路径和深层路由;
- 刷新和浏览器前进后退;
- 资源缓存;
- 旧入口与新资源是否混用;
- Chrome、Safari、Firefox 等目标浏览器;
- 网络中断和重新加载。
桌面:
- 全新安装;
- 覆盖升级;
- 卸载;
- 用户数据保留;
- 无开发环境机器运行;
- 签名和系统安全提示;
- 目标 CPU 架构。
11.3 运行时观测
发布后的指标应按版本、平台、系统版本、设备型号和 Flavor 维度拆分。至少要能够回答:
哪个版本开始出错?
哪些用户受影响?
错误发生在启动、登录、支付还是升级?
新版本错误是否显著高于旧版本?
关闭功能开关后错误是否回落?
Flutter 层可以通过错误上报工具记录异常,但要注意:
- 不记录密码、访问令牌和完整身份证明;
- 对用户 ID 做脱敏或哈希;
- 区分 Dart 异常、Flutter 框架异常、原生崩溃和 ANR;
- 发布模式下不要依赖
print作为唯一日志; - 记录构建版本、平台和环境,避免无法定位构建来源。
观测系统本身也应经过验证:人为制造一个受控异常,确认它能够带上正确的版本和环境标签,并且隐私过滤有效。
12. 常见误解与失败边界
误解一:用了 --release 就可以上传
错误。--release 不会自动完成:
- Android 上传密钥配置;
- iOS 证书和 Provisioning Profile;
- 商店版本号;
- 隐私声明;
- 应用图标和启动图;
- API 环境切换;
- 灰度策略;
- 回滚准备。
误解二:Flavor 可以保护生产密钥
错误。Flavor 只决定构建变体。客户端中的值都可能被提取。真正的授权必须由服务器验证,客户端只能持有低权限、可撤销的凭据。
误解三:暂停商店发布等于回滚
错误。暂停通常只阻止新用户继续获得该版本,不能保证已经安装的用户降级。已经执行的数据迁移也不会自动撤销。
误解四:Web 回滚只需替换 index.html
错误。如果旧入口依赖的 JavaScript、字体或图片已经删除,回滚入口后仍会加载失败。静态资源必须保留与多个入口版本兼容的生命周期。
误解五:签名能阻止客户端被逆向
错误。签名主要保护发布来源和完整性。敏感业务规则、授权判断和秘密必须放在服务端;客户端混淆也不能替代服务端安全控制。
误解六:新客户端发布后,服务端可以立即删除旧接口
错误。用户升级具有延迟,旧客户端会长期存在。服务端接口和数据库迁移应采用兼容窗口,并用实际版本分布决定何时清理旧路径。
13. 一个可审计的 CI 发布流程
一个最小但完整的生产流水线可以是:
set -euo pipefail
flutter --version
flutter pub get
flutter analyze
flutter test
flutter build appbundle \
--release \
--flavor production \
-t lib/main.dart \
--dart-define-from-file=config/production.json
# 随后执行:
# 1. 检查 AAB 文件存在
# 2. 检查版本号和 applicationId
# 3. 检查签名
# 4. 上传内部测试轨道
# 5. 执行安装、启动、升级冒烟测试
# 6. 再进入人工批准的灰度阶段
这里的生产配置文件不应直接包含秘密;签名文件和密码应由 CI 的秘密存储注入。构建过程还应保存:
- Git commit;
- Flutter 和 Dart 版本;
- 依赖锁定文件摘要;
- 构建参数;
- 产物摘要;
- 签名证书指纹;
- 测试报告;
- 发布审批记录。
这样发生故障时,才能证明“线上这个包究竟由什么源代码、什么依赖和什么工具链生成”。
最终,可靠发布的核心不是某一个 Flutter 命令,而是建立一条可验证的因果链:
确定版本和环境
-> 使用锁定工具链构建
-> 使用正确身份签名
-> 验证产物和升级路径
-> 通过目标渠道灰度
-> 按版本观测故障
-> 优先关闭风险功能和恢复兼容服务
-> 发布更高版本的修复构建
Android 和 iOS 的签名与商店升级规则、Web 的缓存模型、桌面的代码签名和安装方式都不同。Flutter 负责跨平台代码和构建入口,但不会消除这些平台边界;发布系统必须把它们分别建模,才能在灰度失败时真正恢复服务。
系列导航与关联阅读
- 系列入口:Flutter 完整学习路线:从 Dart 与 Widget 到多端架构和应用发布
- 上一篇:Flutter 大型应用架构:分层、Feature、依赖注入和多端边界
- 下一篇:Dart 函数与闭包:参数、类型、捕获、Callable 和 API 设计
- 延伸:Flutter 项目工具链:SDK、Pub、Flavor、代码生成和环境配置
- 延伸:Flutter 应用安全:Secret、网络、存储、WebView、证书和供应链
- 延伸:Flutter 错误处理与可观测性:Zone、日志、Crash、性能和隐私
官方资料
本文依据 Flutter 与 Dart 官方文档重新梳理;正文与示例由 WR BLOG 编写。

评论
0 条讨论