自动化 Android 应用部署到 Google Play 商店
设置自动化的 Android 构建和部署——从 Gradle 配置到通过 CI/CD 发布到 Play 商店。

将 Android 应用发布到 Play 商店听起来应该很简单——构建一个 APK,上传,完成。但在实践中,这个过程涉及签名配置、构建变体、捆绑包格式、Play 商店轨道、分阶段发布,以及足够多的 Gradle 配置,足以让任何人质疑他们的职业选择。在为多个项目(包括需要 iOS 和 Android 管道的 Flutter 应用)自动化 Android 部署之后,我建立了一套系统,可以处理从提交代码到生产发布的所有环节,无需人工干预。
Android 构建系统
在自动化任何事情之前,你需要对 Android 构建的工作原理有扎实的理解。与 iOS 不同,iOS 的 Xcode 在 GUI 背后处理了大部分复杂性,而 Android 的构建系统完全基于 Gradle,并通过代码进行配置。这实际上是自动化的一个优势——一切都是显式的且受版本控制。
发布构建的 Gradle 配置
你的 android/app/build.gradle(如果你已迁移到 Kotlin DSL,则是 build.gradle.kts)是核心配置文件。对于发布构建,你需要配置签名、代码混淆和优化。
android {
compileSdkVersion 35
defaultConfig {
applicationId "com.yourcompany.yourapp"
minSdkVersion 24
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
}
signingConfigs {
release {
keyAlias System.getenv("KEY_ALIAS") ?: properties["keyAlias"]
keyPassword System.getenv("KEY_PASSWORD") ?: properties["keyPassword"]
storeFile file(System.getenv("KEYSTORE_PATH") ?: properties["keystorePath"] ?: "keystore.jks")
storePassword System.getenv("STORE_PASSWORD") ?: properties["storePassword"]
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
签名配置首先从环境变量读取,然后回退到本地属性文件。这种模式允许开发人员使用 key.properties 文件在本地签署构建,而 CI 则使用环境变量——同一个 Gradle 文件在两种情况下无需修改即可工作。
本地属性文件
对于本地开发,在你的 android/ 目录中创建一个 key.properties 文件(并立即将其添加到 .gitignore):
storePassword=your_store_password
keyPassword=your_key_password
keyAlias=your_key_alias
keystorePath=../keys/release-keystore.jks
然后在你的 build.gradle 中引用它:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
签名配置
Android 应用签名比 iOS 代码签名简单,但它带有一个永久性的风险:如果你丢失了上传密钥,你将失去更新应用的能力。Google 推出了 Play 应用签名来缓解这个问题,我强烈建议使用它。
Play 应用签名
启用 Play 应用签名后,Google 会持有实际的分发密钥。你使用上传密钥签署你的上传包,Google 在将其交付给用户之前会使用分发密钥重新签名。其好处包括:
- 密钥丢失恢复:如果你丢失了上传密钥,Google 可以重置它。如果没有 Play 应用签名,丢失密钥意味着需要创建一个新的应用列表。
- 更小的 APK:Google 可以使用分发密钥为每个设备配置优化应用。
- 密钥轮换:你可以轮换你的上传密钥,而不会影响已安装的用户。
在 Play Console 的“设置”>“应用签名”下启用它。对于新应用,它默认是启用的。
生成密钥库
如果你需要创建一个新的密钥库:
keytool -genkey -v \
-keystore release-keystore.jks \
-keyalg RSA \
-keysize 2048 \
-validity 10000 \
-alias your_key_alias
安全地存储此密钥库文件。对于 CI,我将其进行 base64 编码并作为 GitHub Actions 密钥存储:
base64 -i release-keystore.jks | pbcopy
然后在构建之前在 CI 工作流中解码它:
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
AAB 与 APK
Google 现在要求所有在 Play 商店上发布的新应用都使用 Android App Bundles (AAB)。这种差异对你的构建管道很重要。
APK (Android Package) 是一个包含所有设备配置所需所有内容的单一文件。它更大,但通用——任何 Android 设备都可以安装任何 APK。APK 仍然适用于内部分发、在物理设备上测试以及在 Play 商店之外分发。
AAB (Android App Bundle) 包含所有代码和资源,但允许 Google Play 为每个设备生成优化的 APK。使用 Pixel 手机的用户只下载他们需要的资源,而不是所有屏幕密度和 CPU 架构的资源。捆绑包通常比通用 APK 小 15-30%。
对于你的 CI 管道,构建两者:
# For Play Store
flutter build appbundle --release
# For internal testing / direct installation
flutter build apk --release --split-per-abi
--split-per-abi 标志为每个 CPU 架构(arm64-v8a、armeabi-v7a、x86_64)生成单独的 APK,这减少了直接分发的文件大小。
Fastlane Supply 用于 Play 商店
Fastlane 的 supply action 处理 Play 商店的上传和元数据管理,类似于 deliver 在 iOS 上的作用。
设置 Supply
首先,你需要一个具有正确权限的 Google Play 服务帐号。
- 前往 Google Cloud Console 并创建一个服务帐号。
- 下载 JSON 密钥文件。
- 在 Play Console 中,前往“设置”>“API 访问”并关联该服务帐号。
- 为你的应用授予“发布经理”权限。
# android/fastlane/Appfile
json_key_file(ENV["PLAY_STORE_JSON_KEY"] || "path/to/service-account.json")
package_name("com.yourcompany.yourapp")
基本上传
default_platform(:android)
platform :android do
desc "Deploy to Play Store Internal Testing"
lane :internal do
gradle(
task: "bundle",
build_type: "Release",
project_dir: "./"
)
supply(
track: "internal",
aab: "../build/app/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
end
对于 Flutter 项目,我更喜欢直接运行 Flutter 构建命令,而不是通过 Fastlane 使用 Gradle 任务,因为 Flutter 的构建过程包含 Dart 编译步骤,而 Gradle 单独无法正确处理:
lane :internal do
Dir.chdir("..") do
sh("flutter build appbundle --release")
end
supply(
track: "internal",
aab: "../build/app/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
Play 商店轨道
Play 商店有四个轨道,了解何时使用每个轨道对你的发布策略很重要。
内部测试
- 最多 100 名测试人员。
- 无需审核——构建几乎立即可用。
- 最适合:开发团队测试、QA 周期、在推广前检查构建。
封闭测试 (Alpha)
- 无限测试人员,但你需要管理列表。
- 需要 Google 进行简短审核(通常需要几个小时)。
- 最适合:邀请用户参与的 Beta 项目、客户预览。
开放测试 (Beta)
- 任何人都可以从 Play 商店列表加入。
- 需要审核。
- 最适合:公共 Beta 项目、在广泛发布前收集反馈。
生产
- 对所有用户可用。
- 需要完整审核。
- 支持分阶段发布。
轨道间推广
Fastlane 使轨道推广变得简单:
lane :promote_to_beta do
supply(
track: "internal",
track_promote_to: "beta",
skip_upload_changelogs: false,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
lane :promote_to_production do
supply(
track: "beta",
track_promote_to: "production",
rollout: "0.1", # 10% staged rollout
skip_upload_changelogs: false,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
分阶段发布
分阶段发布是 Android 相对于 iOS 的最大优势之一。你可以向一部分用户发布,并逐步增加发布范围,在每个阶段监控崩溃率和用户反馈。
lane :staged_rollout do |options|
percentage = options[:percentage] || "0.1"
supply(
track: "production",
rollout: percentage,
aab: "../build/app/outputs/bundle/release/app-release.aab"
)
end
lane :increase_rollout do |options|
percentage = options[:percentage] || "0.5"
supply(
track: "production",
rollout: percentage,
skip_upload_aab: true,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
lane :complete_rollout do
supply(
track: "production",
rollout: "1.0",
skip_upload_aab: true,
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
典型的发布时间表:
- 第 1 天:10%——在 Firebase Crashlytics 中观察崩溃率。
- 第 2-3 天:25%——检查用户反馈和支持工单。
- 第 4-5 天:50%——验证大规模发布时没有性能退步。
- 第 6-7 天:100%——全面发布。
如果在任何阶段出现问题,你可以暂停发布并推送修复,而不会影响所有用户。
GitHub Actions 工作流
这是一个完整的 Android CI/CD 工作流:
name: Deploy to Play Store
on:
push:
branches: [main]
workflow_dispatch:
inputs:
track:
description: 'Play Store track'
required: true
default: 'internal'
type: choice
options:
- internal
- alpha
- beta
- production
jobs:
deploy:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: '3.27.x'
channel: stable
cache: true
- name: Setup Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: '3.2'
bundler-cache: true
working-directory: android
- name: Flutter dependencies
run: flutter pub get
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/keystore.jks
- name: Create service account file
run: echo '${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}' > android/play-store-key.json
- name: Build and deploy
working-directory: android
env:
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
KEYSTORE_PATH: app/keystore.jks
PLAY_STORE_JSON_KEY: play-store-key.json
run: bundle exec fastlane ${{ github.event.inputs.track || 'internal' }}
- name: Cleanup secrets
if: always()
run: |
rm -f android/app/keystore.jks
rm -f android/play-store-key.json
关于这个工作流,有几点需要注意:
Ubuntu runner 适用于 Android。与需要 macOS 的 iOS 不同,Android 构建可以在 Linux 上运行。Ubuntu runner 在 GitHub Actions 上更便宜,启动速度也更快。
对于当前版本的 Android Gradle Plugin,需要 Java 17。如果你的项目使用较旧的 AGP 版本,你可能需要 Java 11,但这越来越少见。
清理步骤即使构建失败也会删除已解码的密钥。这是一种纵深防御措施——GitHub Actions runner 是短暂的,但这是一个好习惯。
版本管理
Android 使用两个版本标识符:versionCode(一个整数,每次上传都必须增加)和 versionName(人类可读的版本字符串)。
对于 Flutter 项目,两者都来自 pubspec.yaml:
version: 1.2.3+45
# 1.2.3 = versionName
# 45 = versionCode
我使用 CI 运行编号自动化版本代码的递增:
lane :set_version_code do
build_number = ENV["GITHUB_RUN_NUMBER"] || "1"
Dir.chdir("..") do
current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
sh("sed -i 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
end
end
这确保了版本代码始终增加,即使跨分支也是如此。如果你需要更多控制,可以从 Play 商店获取最新的版本代码:
lane :smart_version_code do
current = google_play_track_version_codes(track: "internal").max || 0
new_code = current + 1
Dir.chdir("..") do
current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
sh("sed -i 's/^version: .*/version: #{current_version}+#{new_code}/' pubspec.yaml")
end
end
管理 Play 商店列表
Supply 可以从版本控制的文件管理你的整个 Play 商店列表:
android/fastlane/metadata/android/
├── en-US/
│ ├── title.txt
│ ├── short_description.txt
│ ├── full_description.txt
│ ├── changelogs/
│ │ ├── default.txt
│ │ └── 45.txt # Changelog for versionCode 45
│ └── images/
│ ├── phoneScreenshots/
│ │ ├── 1_home.png
│ │ └── 2_detail.png
│ ├── featureGraphic.png
│ └── icon.png
└── ru-RU/
├── title.txt
├── short_description.txt
└── full_description.txt
要下载你当前的列表作为起点:
fastlane supply init
然后更新文件并推送更改:
lane :update_listing do
supply(
skip_upload_aab: true,
skip_upload_apk: true
)
end
ProGuard 和 R8 配置
当 minifyEnabled 为 true 时,R8(ProGuard 的继任者)会缩小、混淆和优化你的代码。这对于生产构建至关重要——它减小了 APK 大小并使逆向工程变得更困难——但如果配置不当,它也可能导致问题。
常见问题及其 ProGuard 规则:
# Keep Flutter's classes
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
# Keep Firebase classes
-keep class com.google.firebase.** { *; }
# Keep models used with JSON serialization
-keep class com.yourcompany.yourapp.models.** { *; }
# Keep classes referenced via reflection
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes InnerClasses
# Common crash fix: OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
彻底测试你的发布构建——R8 问题通常表现为运行时崩溃,而这些崩溃在调试构建中不会出现。Flutter 中的 --obfuscate 和 --split-debug-info 标志可以启用进一步的优化:
flutter build appbundle --release --obfuscate --split-debug-info=build/symbols
保留 build/symbols 目录——你需要它来符号化崩溃报告中的堆栈跟踪。
完整的 Fastfile
default_platform(:android)
platform :android do
desc "Deploy to internal testing"
lane :internal do
set_version_code
build_release
supply(
track: "internal",
aab: "../build/app/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
desc "Deploy to closed beta"
lane :beta do
set_version_code
build_release
supply(
track: "beta",
aab: "../build/app/outputs/bundle/release/app-release.aab",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true
)
end
desc "Deploy to production with staged rollout"
lane :production do
set_version_code
build_release
supply(
track: "production",
rollout: "0.1",
aab: "../build/app/outputs/bundle/release/app-release.aab"
)
end
desc "Promote internal to beta"
lane :promote_to_beta do
supply(
track: "internal",
track_promote_to: "beta"
)
end
desc "Increase production rollout"
lane :increase_rollout do |options|
supply(
track: "production",
rollout: options[:percentage] || "0.5",
skip_upload_aab: true
)
end
private_lane :build_release do
Dir.chdir("..") do
sh("flutter build appbundle --release --obfuscate --split-debug-info=build/symbols")
end
end
private_lane :set_version_code do
build_number = ENV["GITHUB_RUN_NUMBER"] || Time.now.strftime("%Y%m%d%H%M")
Dir.chdir("..") do
current_version = sh("grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//'").strip
sh("sed -i '' 's/^version: .*/version: #{current_version}+#{build_number}/' pubspec.yaml")
end
end
end
与 iOS 自动化的区别
在自动化了这两个平台之后,有几个值得注意的关键区别。
Android 上的签名更简单。一个单一的密钥库文件与 iOS 上证书和配置文件之间的复杂操作形成对比。不需要 match 或任何证书管理工具。
没有强制性的 macOS 要求。Android 在 Linux 上构建,这意味着更便宜、更快的 CI runner。
更快的审核时间。Google 的审核过程通常比 Apple 快,而且内部轨道上传根本不需要审核。
分阶段发布是原生支持的。在 iOS 上,你可以进行分阶段发布,但你无法以相同的粒度控制百分比或在中途停止发布。
Play 商店 API 更宽松。你几乎可以通过编程方式管理所有内容,包括商店列表实验和定价。Apple 的 App Store Connect API 有更多限制。
主要缺点是 Gradle 构建时间。一个带有 R8 优化的干净 Android 构建可能比同等的 iOS 构建花费更长的时间。缓存有所帮助——包括 Gradle 的内置缓存和 CI 级别的 .gradle 目录缓存。
成熟的管道是什么样的
完成所有设置后,日常体验应该是无感的。推送到 main 分支,构建运行,几分钟后测试人员就能在内部轨道上获得新版本。标记一个发布,生产构建将通过分阶段发布进行。
这个管道消除了“在我的机器上可以运行”这类问题,确保每个构建都是可重现的,并让团队相信发布不再是一件大事。最后一点才是真正的目标——让部署变得如此常规,以至于没有人会去思考它。