Android 打包、签名与上架

Android 打包本身只有三条命令,真正容易翻车的是签名配置与上架材料:密钥丢了、key.properties 提交进了 Git、targetSdk 没跟上商店要求,都会在发版当天变成事故。本章按“看清构建体系 → 配签名 → 出包 → 备材料 → 处理被拒”的顺序走一遍。

Android 构建体系速览

文件作用
android/app/build.gradle应用级配置:签名、SDK 版本、flavor、混淆
android/build.gradle工程级:插件版本与仓库地址
android/settings.gradle声明插件与 Gradle 插件版本
android/gradle.propertiesJVM 参数、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.yamlversion: 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 并加入 .gitignorebuild.gradlesigningConfigsbuildTypes.release → AAB 上 Google Play、--split-per-abi 上国内商店 → 归档符号表”;上架材料按图标、截图、隐私政策、权限说明、递增的 versionCode 逐项核对,被拒最多的情况是隐私政策与权限声明,而不是代码问题。

笔记加载中…