打包上架看起来只是「点一下 Build 再上传」,但真正踩过坑的人都知道:签名配错一次密钥就永久丢失、versionCode 冲突导致上传被拒、mapping 没上传导致崩溃栈全是 a.b.c、targetSdk 没跟上直接被商店下架。这些问题的共同点是一旦发生就不可逆。
本文按「先搞懂签名,再打通流程,最后自动化」的顺序展开:先讲密钥体系与 Play App Signing,再讲 AAB、版本号与上架流程,最后给出 R8 mapping、bundletool、Gradle Play Publisher 与 targetSdk 适配的实践清单。
一、签名体系:keystore 与签名配置
Android 用签名来证明「这个包确实来自同一个开发者」。同一应用的所有版本必须用同一把密钥签名,否则系统会拒绝安装升级。
keytool -genkeypair -v \
-keystore release.jks \
-alias myapp \
-keyalg RSA -keysize 4096 -validity 10000 \
-storetype JKS # 交互式输入密码与证书信息,有效期 10000 天
android {
signingConfigs {
create("release") {
// 密钥信息从环境变量读取,绝不提交到仓库
storeFile = file(System.getenv("KEYSTORE_PATH") ?: "release.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD")
keyAlias = System.getenv("KEY_ALIAS")
keyPassword = System.getenv("KEY_PASSWORD")
}
}
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
signingConfig = signingConfigs.getByName("release")
}
}
}
| 项 | 说明 | 注意 |
|---|---|---|
| keystore 文件 | 存放密钥的容器 | 丢失后无法再发布同名应用更新 |
alias | 密钥别名 | 一个 keystore 可存多把密钥 |
| 有效期 | -validity 天数 | Play 要求至少到 2033 年 |
| 签名方案 | v1 到 v4 | v2 起支持整包校验,v3 支持密钥轮换 |
| 密码管理 | 环境变量或 CI Secret | 禁止硬编码进 build.gradle.kts |
一句话总结: 签名的铁律是**「同一应用永远用同一把密钥,且密钥与密码绝不进版本库」**;keystore 一旦丢失,该应用在 Play 上就再也无法发布更新。
二、Play App Signing:上传密钥与应用签名密钥
2017 年后 Google 引入 Play App Signing,把「应用签名密钥」托管在 Google 侧,开发者只持有「上传密钥」。这是新应用的默认方案,也是强烈推荐的方案。
| 概念 | 持有方 | 作用 | 可否轮换 |
|---|---|---|---|
| 应用签名密钥 | Google 托管 | 真正给用户安装的包签名 | 可申请轮换,需审核 |
| 上传密钥 | 开发者 | 给上传到 Play 的 AAB 签名 | 可自行轮换,无需审核 |
| 本地 keystore | 开发者 | 仅用于本地调试与自签名分发 | 随意 |
路径 A(推荐):新应用直接用 Play App Signing
1. Play Console 创建应用时选择「使用 Google 管理密钥」
2. 上传用上传密钥签名的 AAB,Google 用应用签名密钥重签后分发
路径 B(存量应用迁移):
1. 生成新的上传密钥,导出现有应用签名密钥
2. 在 Play Console 提交密钥迁移申请,之后用上传密钥签名所有版本
关键收益:上传密钥泄露可立刻轮换而不失去更新能力,
应用签名密钥由 Google 保管,不会因本地硬盘损坏而丢失
一句话总结: Play App Signing 把「不可轮换的应用签名密钥」与「可轮换的上传密钥」分离,让密钥泄露从「应用报废」降级为「换一把密钥」;新应用没有理由不用它。
三、AAB 与 APK 的差异
Google Play 自 2021 年 8 月起要求新应用使用 AAB(Android App Bundle)上传。AAB 不是安装包,而是一份「包含所有代码与资源的构建产物」,由 Play 按用户设备配置生成对应的 APK 下发。
| 维度 | APK | AAB |
|---|---|---|
| 形态 | 可直接安装的包 | 构建产物,不可直接安装 |
| 分发方式 | 直接上传或第三方渠道 | 上传 Play,由 Play 生成 APK |
| 体积 | 包含所有密度与 ABI 资源 | 按设备拆分,用户下载更小 |
| 动态交付 | 不支持 | 支持 Play Feature Delivery |
| 本地验证 | 直接 adb install | 需 bundletool 生成 APK 集 |
| 签名 | 开发者签名 | 上传密钥签名,Play 重签 |
./gradlew :app:bundleRelease # 产出 app/build/outputs/bundle/release/app-release.aab
./gradlew :app:assembleRelease # 产出 APK,用于第三方渠道或本地验证
对于国内的第三方应用商店或企业分发,仍然需要 APK;因此同时产出 AAB 与 APK 是常见做法,两者共用同一份 signingConfigs。
一句话总结: AAB 是「上传格式」,APK 是「安装格式」;上架 Google Play 用 AAB 换取更小的下载体积与动态交付能力,第三方渠道继续用 APK。
四、versionCode 与 versionName 策略
versionCode 是给系统看的整数,必须单调递增;versionName 是给用户看的字符串,可任意格式。两者都在 defaultConfig 中声明。
android {
defaultConfig {
versionCode = 42 // 每次发布必须严格递增,Play 拒绝重复
versionName = "2.4.0" // 面向用户的语义化版本
}
}
// 常见做法:用 CI 构建号自动生成 versionCode,避免人工冲突
val buildNumber = (System.getenv("BUILD_NUMBER") ?: "1").toInt()
android.defaultConfig.versionCode = 10000 + buildNumber
| 方案 | versionCode 生成方式 | 优点 | 风险 |
|---|---|---|---|
| 手工递增 | 每次发布前手动改 | 直观可控 | 多人协作易冲突或回退 |
| CI 构建号 | 10000 + BUILD_NUMBER | 全局唯一、自动递增 | 需保证构建号单调 |
| 时间戳 | YYMMDDnn | 天然递增 | 位数增长快,接近上限 |
| 版本号映射 | major*10000 + minor*100 + patch | 与 versionName 对应 | 大版本超过 2 位数会溢出 |
versionCode 上限是 2100000000,约 21 亿,正常项目远远够用,但绝不能回退:一旦上传了 versionCode = 42,之后所有渠道的版本都必须大于 42。
一句话总结:
versionCode必须单调递增且全局唯一,versionName随意但要与用户认知一致;把它交给 CI 用构建号生成,是避免「上传被拒」最省心的做法。
五、Play Console 上架流程
上架不是上传文件就结束,而是一组必填的合规表单与审核环节。首次上架的完整链路如下:
首次上架步骤:
1. 创建应用:填写应用名、默认语言、应用类型(应用/游戏)、免费或付费
2. 设置商品详情:应用图标(512x512)、特色图片(1024x500)、截图、描述
3. 完成内容分级问卷:IARC 问卷,决定分级标签
4. 填写数据安全表单:声明收集哪些数据、是否加密传输、是否可删除
5. 声明目标受众与内容:是否面向儿童,是否含广告
6. 上传 AAB 到测试轨道,验证后再推进到正式轨道并提交审核
| 表单 | 关键点 | 常见驳回原因 |
|---|---|---|
| 内容分级 | IARC 问卷必须与真实内容一致 | 声明与实际内容不符 |
| 数据安全 | 必须覆盖所有 SDK 的数据收集行为 | 遗漏第三方 SDK 收集项 |
| 目标受众 | 面向儿童需符合 Families 政策 | 儿童应用含未过滤广告 |
| 隐私政策 | 所有收集数据的应用都必须提供 URL | 链接失效或内容空泛 |
| 广告声明 | 含广告必须勾选 | 未声明导致下架 |
| 权限说明 | 敏感权限需提供使用场景说明 | 权限与功能不匹配 |
关于「目标 API 级别」的硬要求:
Play 要求新应用与更新每年跟进 targetSdk
例如 2024 年 8 月 31 日起新应用需 targetSdk 34
2025 年 8 月 31 日起新应用需 targetSdk 35
未跟进的应用会被拒绝更新,存量应用可能被限制可见性
自查命令:检查构建产物中声明的 targetSdk
./gradlew :app:assembleRelease
aapt2 dump badging app/build/outputs/apk/release/app-release.apk | grep targetSdkVersion
一句话总结: 上架的难点不在技术,而在**「数据安全表单、内容分级、隐私政策」这三张合规答卷**;填错或遗漏的代价是审核驳回甚至下架,因此务必逐项核对每个第三方 SDK 的数据行为。
六、发布轨道与分阶段发布
Play 提供多条发布轨道,让新版本先小范围验证,再逐步放量。
| 轨道 | 面向对象 | 审核 | 适用场景 |
|---|---|---|---|
| 内部测试(internal) | 最多 100 个指定邮箱 | 通常最快 | 每日构建、快速验证 |
| 封闭测试(closed) | 邮件列表或 Google Group | 需要审核 | 小范围灰度、beta 用户 |
| 开放测试(open) | 所有人可加入 | 需要审核 | 大范围公测 |
| 正式(production) | 全部用户 | 需要审核 | 正式发布 |
| 分阶段发布 | 按百分比放量 | 同正式 | 降低线上事故影响 |
分阶段发布的典型节奏:
第 1 天 1% 用户 → 看崩溃率与 ANR 率
第 2 天 5% 用户 → 看核心指标与用户反馈
第 3 天 20% 用户 → 看留存与转化
第 4 天 50% 用户 → 第 5 天 100% 用户
任何一步异常都可以「暂停发布」或「回滚到上一版本」:
暂停:停止继续放量,已升级用户不受影响
回滚:需上传 versionCode 更高的旧功能版本,不能直接降级
注意「回滚」在 Play 上并不等于降级:你不能让用户装回旧版本,只能发布一个 versionCode 更大的「功能等价于旧版」的新版本。因此分阶段发布期间的崩溃监控比什么都重要,上线前先建立性能与崩溃基线,才能判断放量是否安全。
一句话总结: 内部轨道用于每日验证,分阶段发布用于降低事故半径;但要记住 Play 不支持真正降级,所谓回滚是用更高
versionCode重新发一个旧功能版本。
七、R8 混淆与 mapping 上传
release 构建开启 R8 后,类名与方法名会被重命名,崩溃栈里的 com.example.Foo.bar 变成 a.b.c。要还原它,必须把 mapping.txt 上传到崩溃监控平台或 Play Console。
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
app/build/outputs/mapping/release/mapping.txt # mapping 文件位置
retrace app/build/outputs/mapping/release/mapping.txt crash.txt # 用 retrace 还原崩溃栈
| 方式 | 操作 | 适用 |
|---|---|---|
| Play Console 上传 | 发布时勾选「上传去混淆文件」 | 依赖 Play 的崩溃报告 |
mappingFileUpload | AGP DSL 自动上传 | 自动化流水线 |
| CI 归档 | 把 mapping 与版本号一起存对象存储 | 自有崩溃平台 |
retrace 命令行 | 手动还原单条崩溃栈 | 排查个案 |
// AGP 8.x:自动上传 mapping 到 Play(需配置 Play 凭据)
android {
buildTypes {
release {
// 与 Gradle Play Publisher 配合时通常由 GPP 处理
// 单独使用时可通过 play 服务账号自动上传
}
}
}
mapping 文件必须与版本严格对应:一旦丢失,该版本的线上崩溃就永久无法还原。因此 CI 中应把 mapping.txt 作为构建产物归档,并带上 versionCode 命名。
一句话总结:
mapping.txt是 release 崩溃可读的唯一钥匙;把它与versionCode绑定归档,并在发布流程里自动上传,否则 R8 的收益会被「崩溃无法定位」抵消。
八、bundletool 本地验证
AAB 不能直接安装,本地验证需要 bundletool 把它转换成 APK 集或直接部署到设备。
bundletool build-apks --bundle=app-release.aab --output=app.apks \
--ks=release.jks --ks-key-alias=myapp # 生成 APK 集(含所有拆分的 APK)
bundletool install-apks --apks=app.apks # 按设备配置安装,模拟 Play 的分发行为
bundletool get-size total --apks=app.apks --dimensions=ABI,SCREEN_DENSITY # 估算下载体积
| 命令 | 作用 | 场景 |
|---|---|---|
build-apks | AAB 转 APK 集 | 本地安装验证 |
install-apks | 按设备配置安装 | 真机回归 |
get-size total | 估算各维度下载体积 | 体积优化 |
dump manifest | 查看合并后的 manifest | 权限与 SDK 核查 |
为什么必须本地验证:
AAB 的拆分逻辑只在 Play 侧生效,本地 assembleRelease 出来的 APK
与用户实际下载的 APK 不是同一份,典型问题只会在某个 ABI 或密度下暴露
验证方式:用 install-apks 装到真机,或 get-size 看拆分是否符合预期
一句话总结: AAB 必须用
bundletool转成 APK 才能本地验证,install-apks能复现 Play 的拆分分发行为,是发现「特定机型资源缺失」最有效的手段。
九、CI 自动发布:Gradle Play Publisher
手动上传容易出错且不可追溯,com.github.triplet.play 插件(Gradle Play Publisher,GPP)让发布变成一条 Gradle 任务。当前主流版本是 3.12.x。
// app/build.gradle.kts
plugins {
id("com.android.application")
id("com.github.triplet.play") version "3.12.0"
}
play {
// 服务账号 JSON,来自 Play Console 的 API 访问设置
serviceAccountCredentials.set(file(System.getenv("PLAY_JSON_KEY") ?: "play-key.json"))
track.set("internal") // 目标轨道:internal / beta / production
defaultToAppBundles.set(true) // 默认上传 AAB
userFraction.set(0.1) // 分阶段发布:按百分比放量
resolutionStrategy.set(com.github.triplet.gradle.play.ResolutionStrategy.AUTO) // 自动递增版本号
}
./gradlew :app:publishReleaseBundle # 发布到配置的轨道
./gradlew :app:publishReleaseBundle --track production --user-fraction 0.05 # 5% 分阶段
./gradlew :app:publishListing # 只更新商品详情,不发新版本
| 配置项 | 作用 | 建议 |
|---|---|---|
serviceAccountCredentials | Play API 凭据 | 存 CI Secret,禁止入库 |
track | 目标轨道 | 先 internal,验证后推 production |
defaultToAppBundles | 上传 AAB 而非 APK | 设为 true |
userFraction | 分阶段放量比例 | 首次发布用 0.05 到 0.1 |
resolutionStrategy | 版本号处理策略 | AUTO 自动递增 versionCode |
CI 流水线中的发布步骤:
1. 拉取代码并检出发布 tag
2. 用 CI Secret 注入 keystore 与 Play 服务账号 JSON
3. ./gradlew :app:bundleRelease 构建 AAB
4. ./gradlew :app:publishReleaseBundle --track internal 先发内测
5. 归档 mapping.txt 与 AAB 到对象存储
6. 人工确认后推进到 production 并设置 userFraction
注意:GPP 会调用 Play Developer API,服务账号需在 Play Console
中授予「发布到测试轨道」与「管理正式版发布」权限。
一句话总结: GPP 把「构建 AAB + 上传 + 设置轨道与放量比例」收敛成一条 Gradle 任务,配合 CI Secret 与归档 mapping,才让发布过程可追溯、可回放。
十、targetSdk 适配与隐私合规
targetSdk 决定系统以哪个版本的行为兼容模式运行你的应用,每年跟进是硬要求。近年变化集中在权限、后台行为与数据访问。
| targetSdk | 关键变化 | 适配要点 |
|---|---|---|
| 33 | 通知需运行时权限、细分媒体权限 | 请求 POST_NOTIFICATIONS,改用 READ_MEDIA_IMAGES |
| 34 | 前台服务类型必填、隐式 Intent 限制 | 声明 foregroundServiceType,显式 Intent |
| 35 | 边到边强制、后台启动限制收紧 | 处理 WindowInsets,避免后台启动 Activity |
| 通用 | 精确闹钟、剪贴板、位置权限 | 按需申请,提供使用场景说明 |
// targetSdk 33 起:通知需要运行时权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
// targetSdk 33 起:读取图片用细分权限替代 READ_EXTERNAL_STORAGE
val imagePermission = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
<!-- targetSdk 34 起:前台服务必须声明类型 -->
<service
android:name=".SyncService"
android:foregroundServiceType="dataSync"
android:exported="false" />
隐私合规方面,除了数据安全表单,还要注意 SDK 的数据收集声明与「同意管理」:面向欧洲经济区用户的应用需要 Google 认证的 CMP(Consent Management Platform),并在收集广告标识符前获取同意。
一句话总结:
targetSdk跟进是「不做就下架」的硬约束,隐私合规是「做了但不填表也会被拒」的软约束;两者都要在发版清单里逐项核对,而不是等审核驳回再补。
十一、常见坑清单
| 坑 | 现象 | 对策 |
|---|---|---|
| keystore 丢失 | 无法再发布该应用更新 | 用 Play App Signing,本地备份 keystore 与密码 |
| 密码硬编码 | 密钥随仓库泄露 | 改用 CI Secret 或环境变量 |
versionCode 重复或回退 | 上传被拒 | 交给 CI 用构建号生成并保证单调 |
| 忘记上传 mapping | 崩溃栈不可读 | 发布流程中归档并上传 mapping.txt |
| 只测 APK 不测 AAB | 特定机型资源缺失 | 用 bundletool install-apks 真机验证 |
| 数据安全表单漏填 | 审核驳回或下架 | 逐个第三方 SDK 核对数据行为 |
| targetSdk 未跟进 | 无法更新或限制可见性 | 每年 8 月前完成适配 |
| 分阶段发布当成降级 | 以为可以回滚旧版 | 用更高 versionCode 重发旧功能版本 |
| 凭据权限不足 | GPP 报 403 | 服务账号授予发布与正式版管理权限 |
一句话总结: 上架坑的本质是**「密钥不可再生、版本号不可回退、合规不可含糊」**三条不可逆约束;把这三条写进发布 checklist 并自动化,比任何事后补救都有效。
小结
| 主题 | 关键结论 |
|---|---|
| 签名 | 同一应用同一密钥,密钥与密码绝不入库 |
| Play App Signing | 上传密钥可轮换,应用签名密钥由 Google 托管 |
| AAB 与 APK | AAB 是上传格式,APK 是安装格式,两者共用签名配置 |
| 版本号 | versionCode 单调递增,交给 CI 生成 |
| 上架流程 | 内容分级、数据安全、隐私政策三张答卷必须逐项核对 |
| 发布轨道 | 内测验证、分阶段放量,Play 不支持真正降级 |
| mapping | 与 versionCode 绑定归档并自动上传 |
| 本地验证 | bundletool install-apks 复现 Play 的拆分分发 |
| 自动化与 targetSdk | GPP 3.12.x 收敛发布流程,targetSdk 每年跟进 |
一句话记住:上架流程的核心是「不可逆约束的自动化」——密钥不丢、版本号不乱、mapping 不丢、合规不漏,这四件事都靠流程与 CI 保证,而不是靠记忆。发布之后,真正的考验才刚开始:线上崩溃率、启动耗时与帧率都会影响留存,性能基线可以沿用 Android 性能优化与启动加速 里的测量方法;而版本迭代越快,模块边界与构建耗时越会成为瓶颈,可参考 Android 多模块架构与 Gradle 优化 ,依赖注入层的拆分思路则见 Hilt 依赖注入实践 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。