《Flutter 签名与打包:从 keystore 到应用商店》
先说边界
应用开发到上架的最后一公里:生成签名材料、配好 Android 的 release 签名、用一条命令出 aab/apk、走 Play Console 的上架主干,iOS 证书与 TestFlight 一段带过,CI 给骨架。边界先说死:iOS 的证书体系(App ID、描述文件、推送配置)在 Mac 加 Xcode 里才讲得清,本文只给主干和最常见报错;代码混淆与加固不展开(Gradle 的 R8 收缩默认已开);多渠道打包(flavor)是另一个话题;商店审核政策细节以官方文档为准。版本口径:Flutter 3.24(2024-08-06 GA,配套 Dart 3.5),Android 模板在这个版本仍是 Groovy 构建脚本(build.gradle)。产出是一条可重复的出包命令链加一份上架主干清单。
版本号从哪来
Flutter 把双端版本号收进 pubspec.yaml 一行:
version: 2.1.0+17
加号前是 versionName(用户看见的 2.1.0),加号后是 versionCode(商店认的 17)。规则只有一条:versionCode 只增不减,商店拿它判断”这是不是新版本”。构建时可覆盖:flutter build appbundle --build-number=18——CI 里最常见的就是把流水线序号喂给它,人不用记今天该填几。心智要分开:name 随便起,code 是一本严格递增的账。最容易犯的错是”发一版忘改一版”:pubspec 不动、CI 也不喂 build-number,第二次上传被拒才想起来。我的做法是发布类构建永远带 –build-number,本地出包才允许用 pubspec 默认值——两套来源别混用。
生成签名材料
签名材料就一个密钥库文件加一份密码。keytool 随 JDK 自带(flutter doctor -v 里能看到 Java 路径):
keytool -genkey -v -keystore ~/upload-keystore.jks
-keyalg RSA -keysize 2048 -validity 10000 -alias upload
Windows 把路径换成 %USERPROFILE%upload-keystore.jks。两个做完就不许反悔的决定:有效期——10000 天是官方示例值,够长,但商店密钥的生命周期是”应用活多久它活多久”;备份——文件加密码离线存好,丢了没有任何找回渠道(启用 Play App Signing 的话上传密钥可重置,但别把希望押在这个流程上)。
接着写配置文件 android/key.properties(此文件不进版本库):
storePassword=你的库密码
keyPassword=你的钥匙密码
keyAlias=upload
storeFile=/Users/you/upload-keystore.jks
storeFile 建议指向版本库外的绝对路径(官方部署文档的推荐做法),keystore 与 key.properties 都进 .gitignore——签名材料进 git 仓库是真实发生过的安全事故类别。
把签名接进构建
android/app/build.gradle 里读配置、建 signingConfig、挂到 release:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
android {
signingConfigs {
release {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
storePassword keystoreProperties['storePassword']
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
这段是官方部署文档的骨架,逐字可复制。exists() 判断是给 CI 留的活口:本地有 key.properties 就签名,没有(比如 PR 构建)就落回 debug 签名,流水线不至于直接红。配置完做个冒烟:flutter build apk --debug 与 flutter build apk --release 各跑一遍,release 不再报 signingConfig 缺失就算接通。
出包:aab 与 apk 各走各路
# 上架 Play:应用包
flutter build appbundle --release
1. 产物:build/app/outputs/bundle/release/app-release.aab
1. 侧载 / 国内商店:分包 APK
flutter build apk --split-per-abi
1. 产物:build/app/outputs/flutter-apk/app-arm64-v8a-release.apk 等
选型不纠结:Play 商店 2021-08 起新应用强制 aab(公开口径),商店按设备动态下发,包体最小;国内商店或直接发安装包,用 –split-per-abi 的 apk,按 ABI 分包后每个包都比 fat apk 小一截。aab 包体小的原理也值得知道:它不是”压缩更狠”,是把”这台设备需要哪些资源与 so”的拆分计算交给商店——同一份 aab 装到不同设备,落地的安装包并不一样。出完包验一次签名(apksigner 在 Android SDK 的 build-tools 里):
apksigner verify --print-certs build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
打印出的别名是 upload、指纹与本地密钥库一致,签名链就贯通了。release 包务必真机装一遍冒烟:图标、启动、核心流程。debug 手感好不代表 release 没问题——release 的裁剪与混淆偶尔会碰掉反射式的代码,插件在 release 下行为异常时,先怀疑这里再查代码。
Play Console 上架主干
开发者账号一次性 25 美元(公开口径)。流程主干:
- 创建应用,填商店信息:名称、描述、截图、512 图标——素材可后补,提审前必须齐;截图与文案后台随时能迭代,先把隐私政策和数据安全表单做对,这两样改起来最折腾;
- 应用内容问卷:隐私政策 URL、数据安全表单、内容分级、目标受众——数据安全表单要与代码里实际申请的权限对得上,”表单说不用收集、代码里却在要定位”是常见驳回原因;
- 先走内部测试轨道:传 aab、拉几个测试邮箱,通常很快能装上(注意一条政策:2023 年底后新注册的个人开发者账号,要先跑一段封闭测试、满足人数与时长要求才能开放生产轨道——以官方当前政策为准);
- 生产提审:首次审核常以天计,之后的更新一般更快——定性口径,别拿别人的一次经验当 SLA。
Play App Signing 会在上传第一个 aab 时引导启用:Google 保管应用签名密钥,你手里的 upload key 只负责上传——这也是上传密钥丢失后能重置的原因。每个轨道都要求新上传的 versionCode 大于该轨道已有版本,那本”只增不减”的账在这里兑现。
iOS:一段带过
主干一句话:flutter build ipa 出 archive(build/ios/archive/Runner.xcarchive)与 ipa,需要 Apple Developer Program(99 美元/年,公开口径);Xcode 的 Signing & Capabilities 配签名,个人团队自动签名最省事,团队协作建议手动证书加描述文件;需要指定导出方式时给 --export-options-plist 传一份 ExportOptions.plist(method 选 app-store-connect),细节以 flutter build ipa -h 的输出为准;ipa 经 Transporter 或 Xcode Organizer 上传 App Store Connect;TestFlight 分内部测试(上限 100 人,基本即时可用)与外部测试(需一次轻量审核,可容纳上万名测试者);最后在 App Store Connect 提审。最常见的报错是证书或描述文件过期——Xcode 报 signing error 先查这个,再查 Bundle ID 是否唯一。
CI 一段
GitHub Actions 的骨架(关键步骤节选,action 版本以各仓库 README 为准):
# .github/workflows/release.yml 关键步骤
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with: { distribution: 'temurin', java-version: '17' }
- uses: subosito/flutter-action@v2
with: { flutter-version: '3.24.0' }
- run: flutter pub get
- run: flutter test
- run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > "${{ runner.temp }}/upload-keystore.jks"
- run: |
printf 'storePassword=%snkeyPassword=%snkeyAlias=uploadnstoreFile=%sn'
"${{ secrets.STORE_PASSWORD }}" "${{ secrets.KEY_PASSWORD }}"
"${{ runner.temp }}/upload-keystore.jks" > android/key.properties
- run: flutter build appbundle --build-number ${{ github.run_number }}
思路两条:签名材料从 secrets 还原到临时目录,用完即弃;github.run_number 天然单调递增,正好当 build-number。产物(aab)用 actions/upload-artifact 存档,或推商店的发布 API——商店自动发布链路水更深,先把”绿色按钮出正确签名包”这一步做扎实。依赖下载可以用 actions/cache 按 pubspec.lock 缓存 pub 目录,构建提速是可感的(定性口径),加不加看流水线时长焦虑程度。
常见翻车现场
几个高频事故,先认识再绕行:
- versionCode 没涨,上传直接被拒——Play 的报错会明说版本号必须大于已有值。回到 pubspec 或交给 –build-number,别手工猜;
- 密码写死进 build.gradle 还提交了——key.properties 加 .gitignore 从第一天就做对;已经提交过的,改密钥库密码是重活,历史清理更重;
- release 包闪退而 debug 正常——先用
flutter run --release真机复现,再怀疑混淆与裁剪碰了插件,最后查插件是否支持 64 位(老插件的高频坑); - CI 上 keystore 路径不对——storeFile 用 ${{ runner.temp }} 拼绝对路径,别用相对路径猜工作目录;
- 内测装包后秒退——adb logcat 或 flutter logs 抓第一现场,栈顶信息比盲猜快十倍。
验收清单
git status干净:keystore 与 key.properties 都不在版本库里;- 本地
flutter build appbundle产出 aab,apksigner 打印的证书别名是 upload; - 改 pubspec 的 version 后重出包,versionName/versionCode 与预期一致;
- 内部测试轨道的 aab 上传成功,测试邮箱能装包且应用能启动;
- CI 全绿且产物可下载,无签名材料的 PR 构建不因缺 keystore 而红;
- (可选)TestFlight 收到构建并能在真机安装。
学完之后
发布流水线接在功能开发之后:最后一公里里如果还有”功能要碰原生”的部分,看《Flutter 平台通道:与原生代码握手》;整棵技能树的位置见《Flutter 学习路线》的收尾阶段——路线的最后一站,就是把今天这条流水线亲手跑通一次。
本文数据核对时间:2024-08-12:Flutter 3.24(2024-08-06 GA,配套 Dart 3.5),Android 模板该版本仍为 Groovy(build.gradle);签名与部署步骤以 docs.flutter.dev/deployment/android(2024-08 口径)为骨架;Play 商店 aab 强制(2021-08 起新应用)与 Play App Signing 为公开口径;账号费用(Play 25 美元一次性、Apple Developer Program 99 美元/年)与 TestFlight 内部测试上限 100 人为公开口径;GitHub Actions 的 action 版本以各仓库 README 为准。