Flutter 生产环境打包记录
最后整理一下商业 IM 软件的 Flutter 打包脚本,主要是 Android 和 iOS。
平时用 flutter run 比较多,正式打包还得多看几个地方:这次编译选了哪个后端,Xcode 有没有沿用之前的参数,最后生成的是归档还是要交付的 IPA。下面按脚本的处理顺序记一下。
Debug、Profile、Release 分别解决什么问题
Flutter 的构建模式会影响调试、断言、优化和性能分析能力。Debug 适合开发和热重载,Profile 适合性能分析,Release 面向发布。性能判断应当使用真实设备上的合适模式,而不是直接用 Debug 的表现代表发布包。参见 Flutter 构建模式。
在移动端,Dart 的开发体验与 JIT、增量编译相关,发布时则可以通过 AOT 提前生成机器码。JIT 和 AOT 解决的是编译执行方式,不是测试后端与生产后端的选择。参见 Dart 平台说明。
也就是说,--release 不会替业务代码判断应该连接哪个服务器。构建模式和业务环境是两个维度。
同时,Flutter 包里还涉及原生宿主、插件、资源和平台打包过程。Dart 编译结束以后,Android 的构建工具和 iOS 的 Xcode 仍然有自己的工作,最后还涉及签名与分发产物。
用编译参数固定这次产物的环境
项目的两个生产构建脚本分别使用下面的命令:
flutter build apk --release --dart-define=APP_BACKEND_ENV=prod
flutter build ipa --release --dart-define=APP_BACKEND_ENV=prod
它们是实际脚本里的构建行,执行时还需要各自的依赖、签名和平台环境。
App 通过常量环境声明读取这个值:
static const String backendEnv = String.fromEnvironment(
EnvState.backendEnvKey,
defaultValue: kDebugMode ? EnvState.backendDev : EnvState.backendProd,
);
Dart 的编译环境声明与运行时操作系统环境变量不是同一机制;Flutter 可以通过 --dart-define 传入这些值。参见 Dart 编译环境声明。
这种做法让环境选择成为构建输入,不需要打包前手动修改请求地址,再记得改回去。
它也不等同于把参数隐藏起来。环境标识适合放在这里,但不应该把编译参数当成保存客户端秘密的保险箱。
默认值是一项项目策略
当前实现按是否为 Debug 选择默认值,并对显式参数继续判断:
| 输入 | Debug 的后端选择 | Release/Profile 的后端选择 |
|---|---|---|
| 未注入 | 测试 | 生产 |
prod | 生产 | 生产 |
dev | 测试 | 测试 |
| 其他值或空串 | 测试 | 生产 |
这里说的是 Dart 层的选择,后面还有 iOS 构建门禁。
项目希望避免正式产物因为漏传或拼错参数,意外连到测试后端,于是让 Release/Profile 的未知值回落生产。
这不是适用于所有项目的唯一策略。未知值直接阻断构建,也是一种选择;回落生产意味着拼写错误仍可能产出一个连接生产的包。选择哪种方式,需要结合团队的发布约束,并让构建日志或产物信息能够说明最终选择。
为什么直接去 Xcode Archive 还要检查一次?
有一条打包路径需要注意:联调时传入测试环境参数,Flutter 生成了 iOS 构建配置;随后直接从 Xcode 归档,可能继续沿用之前生成的参数。
因此,代码仓库里没有修改服务器地址,不代表当前机器的生成配置一定符合这次发布意图。
项目给 Runner 加了 Verify Backend Environment 构建阶段,在运行 Flutter 构建脚本之前检查 DART_DEFINES。它会解码逗号分隔的值,在 Release 或 Profile 配置下遇到精确的 APP_BACKEND_ENV=dev 时失败退出。
这个检查有明确范围:它识别的是这两个确切配置名和这个精确参数。未来增加自定义配置名称时,需要一起检查规则是否仍然覆盖。
它也不是拒绝所有未知输入的通用校验器。未注入或未知值允许继续,由前面的 App 选择规则处理。
这样从 Xcode 直接归档时,也能挡住残留的 dev 参数。
clean 能清理什么,不能证明什么?
当前 Android 和 iOS 脚本都会执行 flutter clean,然后获取依赖。Android 还清理 Gradle 构建,iOS 执行 pod install,最后显式注入生产参数进行打包。
这是项目对生成状态的处理方式,不意味着每次日常开发都必须全部清理,也不能把 clean 当成解决任何构建失败的通用答案。
Flutter 构建、原生依赖、签名配置属于不同层次。如果失败发生在证书或导出阶段,重新编译 Dart 并不能替代相应配置。
同样,清理后能构建,不代表构建已经可复现。可复现还需要明确 SDK、原生工具链、依赖锁文件、构建参数和签名条件。当前脚本本身没有锁住所有这些输入。
归档成功,为什么还要检查 IPA?
iOS 的归档和分发文件需要分开看。Flutter 官方文档说明 flutter build ipa 会生成归档和 IPA,分别位于 build/ios/archive/ 与 build/ios/ipa/。参见 iOS 构建与发布。
项目的 iOS 脚本在构建前检查分发证书,缺少时提示,但不会立即退出。构建命令结束后,还会查找 IPA;没有找到就按失败处理。
这体现了一个实际的验收标准:准备交付 IPA 时,只看到归档不够,还要确认需要的文件确实产生了。
不过“找到了一个 IPA”仍然只是产物存在性检查,不等于已经验证签名、安装、版本号和运行时后端。项目脚本也没有逐项验证归档中所有符号文件。调试符号还需要另外检查。
留下构建记录
如果需要进一步完善交付,我会让每次构建记录对应代码版本、模式、业务环境、工具版本和产物位置,再用实际安装包核对版本与启动后的环境。
交付前还得安装一次实际生成的包,核对版本和后端环境。脚本找到了 IPA,只能说明文件存在,不能代替这一步。