25、构建产物签名与分发:Google Play签名、内部分发平台、构建产物版本管理
说到构建产物的最后一步,其实就是两件事:签名和分发。签名是为了证明这个APK是你写的,没被人动过手脚;分发则是让用户能装到手机上。这两件事看起来简单,但坑特别多。我见过不少团队,开发阶段跑得好好的,一到上架就崩,十有八九是签名配置出了问题。
25.1 签名的基础逻辑
Android要求所有APK必须经过数字签名才能安装。这个签名用的是证书,说白了就是一个公私钥对。你用私钥给APK打上标记,Google Play或者手机系统用公钥来验证这个标记是否有效。
这里有个关键点:签名证书的指纹(SHA1、SHA256)会作为应用的唯一标识。同一个包名,如果签名变了,系统会认为这是两个不同的应用。我之前接手过一个项目,前任把签名文件丢了,结果所有用户都得卸载重装——那叫一个惨。
25.2 Google Play签名(App Signing by Google Play)
Google Play在2017年推出了这个功能。它的核心思路是:你把上传用的密钥交给Google,Google再用另一个密钥去签最终分发给用户的APK。
为什么要这么做?说白了就是安全。你想想看,如果你的上传密钥泄露了,攻击者最多能上传一个恶意版本到Google Play,但拿不到最终分发的签名密钥。Google那边会帮你保管好最终签名密钥,你只需要管好上传密钥就行。
配置方式很简单,在Google Play Console里开启就行。但要注意一点:一旦开启,就不能关闭。而且你必须在首次上传APK之前就决定好,因为Google需要你提供上传证书的指纹。
Gradle配置示例:
android {
signingConfigs {
release {
storeFile file("upload-keystore.jks")
storePassword "your-store-password"
keyAlias "upload"
keyPassword "your-key-password"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
这里有个细节:不要把密码硬编码在build.gradle里。我习惯用gradle.properties或者环境变量来传。比如:
// gradle.properties
RELEASE_STORE_PASSWORD=xxx
RELEASE_KEY_PASSWORD=xxx
// build.gradle
storePassword System.getenv("RELEASE_STORE_PASSWORD") ?: project.findProperty("RELEASE_STORE_PASSWORD")
25.3 内部分发平台
不是所有APK都要上Google Play。测试版本、内部试用版、企业版,这些都需要一个内部分发渠道。
25.3.1 Firebase App Distribution
Firebase App Distribution是Google官方的内测分发工具。它跟Firebase生态深度集成,你可以在Firebase Console里管理测试人员名单,还能看到谁安装了哪个版本。
集成方式很简单,在build.gradle里加一个插件:
plugins {
id 'com.google.firebase.appdistribution'
}
android {
// ...
}
firebaseAppDistribution {
appId = "1:123456789:android:abc123"
releaseNotes = "修复了登录闪退的问题"
groups = "qa-team"
}
然后执行:
./gradlew assembleRelease appDistributionUploadRelease
嗯,这里要注意:Firebase App Distribution要求你登录Google账号。在CI/CD环境里,你需要用服务账号来认证。我踩过这个坑,当时CI一直报401,查了半天才发现是服务账号权限没配好。
25.3.2 蒲公英(国内常用)
如果你在国内做分发,蒲公英是个不错的选择。它不需要测试人员注册Google账号,直接扫码或者点链接就能安装。蒲公英支持API上传,你可以把它集成到CI流程里。
蒲公英的上传API很简单:
curl -F "file=@app-release.apk" \
-F "uKey=your_ukey" \
-F "_api_key=your_api_key" \
https://upload.pgyer.com/apk/v1/upload
我个人习惯在Gradle里写一个自定义task,把上传逻辑封装起来:
task uploadToPgyer {
doLast {
def apk = file("build/outputs/apk/release/app-release.apk")
def uKey = project.findProperty("PGYER_UKEY")
def apiKey = project.findProperty("PGYER_API_KEY")
exec {
commandLine "curl", "-F", "file=@${apk.absolutePath}",
"-F", "uKey=${uKey}",
"-F", "_api_key=${apiKey}",
"https://upload.pgyer.com/apk/v1/upload"
}
}
}
25.4 构建产物版本管理
版本管理这件事,看起来就是改个versionCode和versionName,但实际做起来很容易乱。我见过一个项目,versionCode直接从1写到了999,中间跳了好几百个数字,完全看不出哪个版本对应哪个发布。
我的做法是:用CI的构建号作为versionCode。比如Jenkins的BUILD_NUMBER,或者GitHub Actions的run_number。这样每个构建都有一个唯一的递增ID,不会重复。
Gradle配置示例:
android {
defaultConfig {
versionCode System.getenv("CI_BUILD_NUMBER")?.toInteger() ?: 1
versionName "2.3.${versionCode}"
}
}
versionName的格式我推荐用语义化版本:主版本.次版本.修订号。比如2.3.1表示主版本2,次版本3,第1次修订。每次发布新版本时,手动更新主版本或次版本,修订号自动用构建号填充。
还有一个容易被忽略的点:版本信息要能追溯到代码。我习惯在APK的meta-data里写入Git commit hash:
android {
defaultConfig {
buildConfigField "String", "GIT_COMMIT_HASH", "\"${getGitHash()}\""
}
}
def getGitHash() {
def process = "git rev-parse --short HEAD".execute()
return process.text.trim()
}
这样出问题的时候,看一眼APK就能知道它对应哪个代码版本,不用去猜。
25.5 知识体系总览
下面这张图把签名、分发、版本管理的关系串起来了:
25.6 一些实战经验
最后分享几个我踩过的坑:
- 签名文件不要放在Git仓库里。就算仓库是私有的,也不安全。我习惯把签名文件放到CI的密钥存储里,或者用公司的密钥管理服务。
- versionCode不要手动改。手动改一定会出错。要么用CI构建号,要么用时间戳(比如20250315)。
- 测试版和正式版的签名要分开。我见过有人把debug签名当成release签名上传到Google Play,结果安装失败。debug签名是Android Studio自动生成的,每台机器都不一样。
- Firebase App Distribution的测试人员要提前加好。别等到要发版了才去加人,Google的权限同步有延迟,有时候要等十几分钟。
嗯,签名和分发这件事,说白了就是把正确的文件,用正确的方式,送到正确的人手里。流程搭好了,后面就省心了。