Android 打包、签名与上架
Android 打包本身只有三条命令,真正容易翻车的是签名配置与上架材料:密钥丢了、key.properties 提交进了 Git、targetSdk 没跟上商店要求,都会在发版当天变成事故。本章按“看清构建体系 → 配签名 → 出包 → 备材料 → 处理被拒”的顺序走一遍。
Android 构建体系速览
| 文件 | 作用 |
|---|---|
android/app/build.gradle | 应用级配置:签名、SDK 版本、flavor、混淆 |
android/build.gradle | 工程级:插件版本与仓库地址 |
android/settings.gradle | 声明插件与 Gradle 插件版本 |
android/gradle.properties | JVM 参数、AndroidX 开关 |
| SDK 版本 | 含义 | 建议 |
|---|---|---|
compileSdk | 编译时用哪个 SDK | 跟随最新稳定版 |
targetSdk | 系统按哪个版本的行为运行 | 每年跟随商店要求升级,晚升会被拒 |
minSdk | 最低支持的系统版本 | 按用户覆盖与插件要求定,常见 21~23 |
插件要求更高 minSdk 时(如 flutter_local_notifications、部分支付 SDK),Gradle 报错信息里会直接写“requires minSdkVersion 23”,照提示改即可。
生成签名密钥
# 生成上传密钥:有效期 10000 天,算法用 RSA 2048
keytool -genkey -v -keystore upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias upload
keytool 随 JDK 提供,命令会交互式询问密码与姓名组织信息。密钥文件与密码一旦丢失,就无法再给同一个包名发布更新(Google Play 可用 Play App Signing 找回上传密钥,国内商店通常没有这个补救),所以务必异地备份。key.properties 与 *.jks 必须写进 .gitignore,不要提交到版本库。
配置 key.properties 与 build.gradle
# android/key.properties
storePassword=你的密码
keyPassword=你的密码
keyAlias=upload
storeFile=../upload-keystore.jks
// android/app/build.gradle(Groovy 写法)
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 // 不配这一行,产物是测试签名,无法上架
minifyEnabled true // 开启 R8 混淆与压缩
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
Flutter 3.x 的新模板默认使用 Kotlin DSL(build.gradle.kts),配置位置相同,只是语法变成 signingConfigs { create("release") { ... } };混用两套语法是新手最常见的一类报错。
出包:APK 与 AAB
flutter build apk --release # 通用 APK,含全部 ABI,体积最大
flutter build apk --release --split-per-abi # 按 ABI 拆分,单包最小
flutter build appbundle # AAB,Google Play 要求的上传格式
flutter build apk --release --dart-define=ENV=prod # 配合多环境注入配置
| 产物 | 体积(示例) | 适用场景 |
|---|---|---|
app-release.apk(通用) | 约 20 MB | 内测直接安装、小团队自建分发 |
app-armeabi-v7a-release.apk | 约 8 MB | 老设备下载包,但已少见 |
app-arm64-v8a-release.apk | 约 9 MB | 现代 Android 设备主力包 |
app-release.aab | 约 9 MB(每设备实际下载量) | Google Play 上架 |
结论:Google Play 用 AAB,让商店按设备下发;国内商店多数仍收 APK,建议上传 arm64-v8a 单包并在描述里说明。
混淆与符号表
Flutter 默认对 Dart 代码做 AOT 编译并混淆,Android 侧 Java/Kotlin 代码由 R8 处理。原生反射、反射式 JSON 库、JNI 入口类可能需要 proguard-rules.pro 中的 -keep 规则。要还原线上崩溃栈,必须保留符号表:
flutter build appbundle --obfuscate --split-debug-info=build/symbols
# 还原崩溃栈:flutter symbolize -i crash.txt -d build/symbols
上架材料清单
| 材料 | 要求 | 备注 |
|---|---|---|
| 应用图标 | 512×512 PNG(商店)+ 自适应图标 | 用 flutter_launcher_icons 批量生成 |
| 截图 | 至少 2~5 张,覆盖主要页面 | 各商店分辨率要求不同,准备 1080×1920 一套 |
| 隐私政策 | 可访问的 URL | 采集任何个人信息都必须提供 |
| 权限说明 | 逐条说明用途 | 相机、定位、通讯录是重点审查项 |
| 版本号 | pubspec.yaml 的 version: 1.0.0+1 | + 后是 versionCode,每次提交必须递增且不能回退 |
| 目标 API 等级 | 跟随商店当年要求 | 国内商店同样有硬性要求 |
常见被拒原因
- 隐私政策缺失或与实际采集行为不符:集成了统计、推送、崩溃上报就必须声明。
- 权限申请超出功能需要:仅拍照却在清单里声明了通讯录、短信权限。
targetSdk低于商店要求:提审时被直接退回,改配置重新出包即可。- 应用名称、图标与内容不符:马甲包、版本换名的做法风险极高。
versionCode未递增或重复:上传后商店直接拒绝。
常见坑
| 坑 | 现象 | 正确做法 |
|---|---|---|
key.properties 提交到 Git | 密钥泄露,可被冒名发版 | 加入 .gitignore,CI 用 Secrets 注入 |
忘记配 signingConfig | 产物是 debug 签名,商店拒收 | release 的 buildTypes 指定 signingConfigs.release |
| 直接上通用 APK | 下载包大、转化率下降 | Google Play 用 AAB,国内用 --split-per-abi |
| 忘了保留符号表 | 线上崩溃栈全是乱码 | 发版时固定输出 --split-debug-info 并归档 |
| 混淆后崩溃 | 反射类被裁剪 | 补 -keep 规则,不要直接关掉 minifyEnabled |
| 本地能跑 CI 报错 | JDK 或 Android SDK 版本不一致 | CI 固定镜像与 JDK 版本,先 flutter doctor -v 核对 |
小结:Android 发布链路是“备份密钥 → 写 key.properties 并加入 .gitignore → build.gradle 配 signingConfigs 与 buildTypes.release → AAB 上 Google Play、--split-per-abi 上国内商店 → 归档符号表”;上架材料按图标、截图、隐私政策、权限说明、递增的 versionCode 逐项核对,被拒最多的情况是隐私政策与权限声明,而不是代码问题。