应用版本与更新策略
应用版本与更新策略:versionCode/versionName 版本号设计、Google Play staged rollout 灰度发布、In-App Update API(Flexible/Immediate)应用内更新、强制更新策略实现。
学习目标
- 理解 versionCode 与 versionName 的语义与版本号策略
- 掌握 Google Play staged rollout 灰度发布流程
- 能用 In-App Update API 实现 Flexible 与 Immediate 两种更新模式
- 设计应用内更新 UI 与强制更新策略
- 理解国内市场更新机制与 Google Play 的差异
学习目标
- 理解
versionCode与versionName的语义与版本号策略; - 掌握 Google Play staged rollout 灰度发布流程;
- 能用 In-App Update API 实现 Flexible 与 Immediate 两种更新模式;
- 设计应用内更新 UI 与强制更新策略;
- 理解国内市场更新机制与 Google Play 的差异。
应用版本与更新策略
应用上线不是终点,而是迭代的起点。本章解决两个问题:版本号怎么编号,以及用户怎么拿到新版本。Google Play 有官方 In-App Update API,国内市场各有自己的 SDK,思路相通。
一、版本号策略
Android 在 build.gradle 中声明两个版本字段:
android {
defaultConfig {
versionCode 102 // 整数,必须递增
versionName "1.2.0" // 字符串,展示给用户
}
}
1. versionCode:内部递增编号
versionCode 是一个整数,每次发布必须严格大于上次,否则 Play 拒绝上传。它的语义:
- 用户不直接看到;
- 系统判断升级/降级(A 装了 102,新下载是 105,升级;100 是降级,Play 默认不允许);
- In-App Update API 也靠它判断“是否有新版”。
推荐策略:
- 简单递增:1, 2, 3, …,适合个人/小项目;
- 日期编码
YYYYMMDDN:如202610091,一眼看出哪天的版本,多人同日发布用 N 区分; - 语义版本转整数:
major * 10000 + minor * 100 + patch,如1.2.3=10203。简单清晰,适合常规项目; - git commit count:CI 里用
git rev-list --count HEAD,每次提交自动递增,永不冲突。
不要用 git hash 当 versionCode,因为它是字符串。也不要用“年.月.日”做 versionCode(如 2026.1009 浮点)——必须是整数。
2. versionName:用户可见字符串
versionName 是展示在「设置 → 应用信息」和商店“当前版本”位置的可读字符串:
versionName "1.2.0"
versionName "1.2.0-beta1"
versionName "1.2.0-debug" // debug 构建加后缀
主流用 语义化版本号 Semantic Versioning:MAJOR.MINOR.PATCH:
- MAJOR:不兼容的大改(API 重写、UI 全面重构);
- MINOR:兼容性新功能(新增功能但旧数据不破坏);
- PATCH:bug 修复;
- 后缀:
-alpha1/-beta1/-rc1表示预发布。
debug 构建加后缀:
buildTypes {
debug {
versionNameSuffix '-DEBUG'
}
}
debug 包显示 1.2.0-DEBUG,避免和正式版混淆。
3. 多渠道差异化版本
不同 flavor 可独立设版本号:
productFlavors {
google { versionCode 102 versionName "1.2.0" }
huawei { versionCode 10201 versionName "1.2.0-huawei" } // 华为要求自己版本序列
}
国内市场常要求每个渠道独立的版本序列(华为应用市场只认华为渠道上传的 versionCode),多渠道发布要分版本管理。
二、Google Play 灰度发布(Staged Rollout)
Google Play 内置分阶段发布机制,按比例逐步放量新版本:
1. 操作流程
- 进入 Play Console → 正式发布 → 创建新版本;
- 上传新 AAB(versionCode 递增);
- 在版本详情底部 「国家/地区版本发布阶段」 勾选 「分阶段发布」;
- 填百分比:1% / 5% / 10% / 25% / 50% / 100%;
- 提交审核(约 1-3 天)后开始放量。
2. 渐进放量
发布后可逐步提升:
- Day 1:1% 用户收到更新;
- Day 2:观察 Crashlytics 崩溃率与 ANR 率,正常则提至 10%;
- Day 3-5:提至 25% → 50% → 100%;
- 全程可点击 「更新发布阶段」 调整比例。
出问题可 「暂停发布」——已升级的用户继续用新版,但新用户不再收到。修复版提交后从暂停位置继续放量。
3. 分阶段发布的局限
- 已升级用户无法自动降级(除非卸载重装);
- 部分设备会延迟 24-72 小时收到更新(Play 客户端缓存);
- 无法按设备机型/Android 版本精细灰度(统一随机比例)。
如需更精细灰度,用 Firebase Remote Config + In-App Update 组合(见下文)。
三、In-App Update API
Google 提供 In-App Update API,让应用主动检查并下载更新,避免用户去商店手动搜。仅 Google Play 分发的应用可用(国内市场需用各家厂商 SDK)。
API 提供两种更新模式:
- Flexible(灵活更新):后台下载,下载完弹窗提示用户“安装”,用户可继续用 App;
- Immediate(立即更新):全屏强制模式,阻塞用户操作直到更新完成,适合安全/破坏性变更。
依赖:
dependencies {
implementation 'com.google.android.play:app-update:2.1.0'
// 仅 Java 项目(无 Kotlin 协程)用下面这行
implementation 'com.google.android.play:app-update-ktx:2.1.0'
}
1. 检查更新
AppUpdateManager 是入口,通过 getAppUpdateInfo 异步查询:
import com.google.android.play.core.appupdate.AppUpdateInfo;
import com.google.android.play.core.appupdate.AppUpdateManager;
import com.google.android.play.core.appupdate.AppUpdateManagerFactory;
import com.google.android.play.core.install.model.AppUpdateType;
import com.google.android.play.core.install.model.UpdateAvailability;
public class UpdateManager {
private static final int MY_REQUEST_CODE = 1001;
private final Activity activity;
private final AppUpdateManager appUpdateManager;
public UpdateManager(Activity activity) {
this.activity = activity;
this.appUpdateManager = AppUpdateManagerFactory.create(activity);
}
/** 检查是否有可用更新 */
public void checkForUpdate(UpdateCallback callback) {
com.google.android.play.core.tasks.Task<AppUpdateInfo> task =
appUpdateManager.getAppUpdateInfo();
task.addOnSuccessListener(info -> {
if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& info.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)) {
// 有更新且支持 Flexible
callback.onUpdateAvailable(info, AppUpdateType.FLEXIBLE);
} else if (info.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE
&& info.isUpdateTypeAllowed(AppUpdateType.IMMEDIATE)) {
// 仅支持 Immediate(强制更新场景)
callback.onUpdateAvailable(info, AppUpdateType.IMMEDIATE);
} else {
callback.onUpToDate();
}
});
task.addOnFailureListener(e -> callback.onError(e.getMessage()));
}
public interface UpdateCallback {
void onUpdateAvailable(AppUpdateInfo info, int updateType);
void onUpToDate();
void onError(String message);
}
}
AppUpdateInfo 包含:
updateAvailability():UPDATE_AVAILABLE/UPDATE_NOT_AVAILABLE/DEVELOPER_TRIGGERED/UNKNOWN;availableVersionCode():新版本号;isUpdateTypeAllowed(type):设备是否支持该更新类型;clientVersionStalenessDays():用户多少天没更新(用于“提醒 X 天未更新”逻辑);updatePriority():发布时设置的优先级(0-5),用于决定更新策略。
2. Flexible 灵活更新
适合常规功能更新,下载完弹窗提示用户:
public void startFlexibleUpdate(AppUpdateInfo info, ActivityResultLauncher<Integer> launcher) {
appUpdateManager.startUpdateFlowForResult(
info,
AppUpdateType.FLEXIBLE,
activity,
MY_REQUEST_CODE
);
// 注册监听下载进度
appUpdateManager.registerListener(state -> {
if (state.installStatus() == InstallStatus.DOWNLOADED) {
showCompleteUpdateSnackbar();
}
});
}
private void showCompleteUpdateSnackbar() {
// 下载完毕,提示用户安装
View rootView = activity.findViewById(android.R.id.content);
Snackbar.make(rootView, "新版本已下载,重启安装", Snackbar.LENGTH_INDEFINITE)
.setAction("安装", v -> appUpdateManager.completeUpdate())
.show();
}
新版 startUpdateFlowForResult 推荐配合 Activity Result API:
// 在 Activity/Fragment 里
private final ActivityResultLauncher<Integer> updateLauncher =
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() != RESULT_OK) {
Log.w("Update", "用户取消更新,code=" + result.getResultCode());
}
});
下载过程会持续触发 listener,可显示进度条。InstallStatus 取值:PENDING / DOWNLOADING / INSTALLING / INSTALLED / FAILED / CANCELED。
3. Immediate 立即更新
适合紧急修复/安全补丁,阻塞用户操作直到更新完成:
public void startImmediateUpdate(AppUpdateInfo info) {
appUpdateManager.startUpdateFlowForResult(
info,
AppUpdateType.IMMEDIATE,
activity,
MY_REQUEST_CODE
);
}
效果:弹出全屏更新界面,用户无法操作 App 直到下载并安装完成。用户取消会回到 App 但下次启动还会再弹。
4. 在 onResume 恢复被中断的 Immediate 更新
用户切换后台再回前台时,Immediate 更新可能中断,需在 onResume 恢复:
@Override
protected void onResume() {
super.onResume();
appUpdateManager.getAppUpdateInfo()
.addOnSuccessListener(info -> {
if (info.updateAvailability() == UpdateAvailability.DEVELOPER_TRIGGERED_UNINSTALLING
|| info.updateAvailability() == UpdateAvailability.UNKNOWN) {
// 之前的 Immediate 被中断,重新启动
appUpdateManager.startUpdateFlowForResult(
info, AppUpdateType.IMMEDIATE,
this, MY_REQUEST_CODE);
}
// 检查 Flexible 是否已下载完未安装
if (info.installStatus() == InstallStatus.DOWNLOADED) {
showCompleteUpdateSnackbar();
}
});
}
5. 设置发布优先级
在 Play Console 上传新版本时可设 inAppUpdatePriority(0-5,5 最高),通过 API 接口设置:
# 通过 Play Developer API(脚本)发布时设置
curl -X POST \
"https://androidpublisher.googleapis.com/androidpublisher/v3/applications/包名/edits" \
-H "Authorization: Bearer $ACCESS_TOKEN"
或者 Play Console 后台新版上传页面(开发者 API)填写优先级,应用读取后据此决定 Flexible 还是 Immediate:
public void checkAndPromptUpdate() {
appUpdateManager.getAppUpdateInfo().addOnSuccessListener(info -> {
int priority = info.updatePriority(); // 0-5
if (priority >= 4) {
// 优先级高(紧急修复),走 Immediate
startImmediateUpdate(info);
} else if (priority >= 1) {
// 普通更新,走 Flexible
startFlexibleUpdate(info, updateLauncher);
}
// priority == 0 不主动弹
});
}
四、强制更新策略
1. 何时需要强制更新
- 接口协议大改(旧版本无法继续使用);
- 安全漏洞修复;
- 合规要求(如 Google Play 政策变更 deadline);
- 付费/支付通道升级。
2. 服务端控制强制更新
仅靠 In-App Update 不够灵活(用户可拒绝 Immediate),更稳健的做法是服务端下发最低版本号,应用对比决定是否拦截:
服务端接口示例返回:
{
"latestVersionCode": 105,
"latestVersionName": "1.5.0",
"minSupportedVersionCode": 100,
"forceUpdate": true,
"updateTitle": "发现新版本",
"updateMessage": "修复若干 bug,建议立即更新",
"updateUrl": "market://details?id=com.example.app"
}
应用启动时拉取并判断:
public class UpdateChecker {
private final Activity activity;
private final UpdateApi api; // Retrofit 接口
private final int currentVersionCode;
public UpdateChecker(Activity activity, UpdateApi api, int currentVersionCode) {
this.activity = activity;
this.api = api;
this.currentVersionCode = currentVersionCode;
}
public void check() {
api.getUpdateInfo().enqueue(new Callback<UpdateInfo>() {
@Override
public void onResponse(Call<UpdateInfo> call, Response<UpdateInfo> resp) {
UpdateInfo info = resp.body();
if (info == null) return;
if (currentVersionCode < info.minSupportedVersionCode) {
// 低于最低支持版本,强制更新
showForceUpdateDialog(info);
} else if (currentVersionCode < info.latestVersionCode
&& shouldPrompt()) {
// 有新版但不强制,提示更新
showSoftUpdateDialog(info);
}
}
@Override
public void onFailure(Call<UpdateInfo> call, Throwable t) {
// 静默失败,不影响主流程
}
});
}
private void showForceUpdateDialog(UpdateInfo info) {
new AlertDialog.Builder(activity)
.setTitle(info.updateTitle)
.setMessage(info.updateMessage)
.setCancelable(false)
.setPositiveButton("立即更新", (d, w) -> jumpToMarket())
.show();
}
private void showSoftUpdateDialog(UpdateInfo info) {
new AlertDialog.Builder(activity)
.setTitle(info.updateTitle)
.setMessage(info.updateMessage)
.setPositiveButton("更新", (d, w) -> jumpToMarket())
.setNegativeButton("以后再说", null)
.show();
}
private void jumpToMarket() {
try {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("market://details?id=" + activity.getPackageName()));
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
activity.startActivity(intent);
} catch (ActivityNotFoundException e) {
// 没有应用商店,跳浏览器
String url = "https://play.google.com/store/apps/details?id="
+ activity.getPackageName();
startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}
}
/** 一天内最多提示一次 */
private boolean shouldPrompt() {
long last = PreferenceManager.getDefaultSharedPreferences(activity)
.getLong("last_update_prompt", 0);
long now = System.currentTimeMillis();
if (now - last < 24 * 3600_000L) return false;
PreferenceManager.getDefaultSharedPreferences(activity).edit()
.putLong("last_update_prompt", now).apply();
return true;
}
}
3. Google Play vs 国内市场更新
| 维度 | Google Play | 国内市场 |
|---|---|---|
| 应用内更新 API | In-App Update API | 各家厂商 SDK(华为/小米/应用宝)或自研弹窗 |
| 强制更新 | 通过 updatePriority 配合 Immediate | 服务端下发 + 应用商店 deeplink 跳转 |
| 灰度 | staged rollout 按比例 | 各家市场后台支持灰度 |
| 检查时机 | API 主动调用 | 启动时拉接口 |
| 审核流程 | 自动 + 人工抽查 | 各家人工审核 |
国内市场通用方案:服务端 minVersionCode + 应用启动时弹窗 + deeplink market://details?id= 跳应用商店。Google Play 上架的版本不能跳“市场 deeplink”(Play 不支持),所以国内/海外要做渠道区分。
常见坑与最佳实践
- versionCode 用 git hash:必须是整数,hash 是字符串。改用
git rev-list --count HEAD。 - 多 flavor 共用 versionCode 序列:国内市场每家要求独立版本序列,华为版本号和 Play 不能错位。每个 flavor 独立 versionCode 递增。
- staged rollout 一次到 100%:没缓冲,崩溃影响全员。每次至少 3 天观察期,10% → 25% → 50% → 100%。
- Immediate 更新没在 onResume 恢复:用户切后台回来时之前的强制更新弹窗丢失,绕过了强制更新。务必在 onResume 检查并重启。
- 每次启动弹更新:用户烦死。Flexible 模式加
shouldPrompt()限频,至少 24 小时间隔。 - 检查更新阻塞启动:网络慢的话首屏卡 1-2 秒。检查逻辑放异步线程,失败静默不阻断主流程。
- In-App Update 在国内无效:API 只在装了 Google Play 服务的设备可用,国内手机大多没有。国内版本走自研 + deeplink 跳市场方案。
- play-core 版本过旧:旧版本不支持 Immediate 优先级 / staged rollout。建议用
app-update:2.1.0以上。 - 强制更新不可关闭但用户可卸载:再强制也挡不住用户卸载。强制更新对话框文案要让用户理解“为什么必须更新”,否则用户直接卸载。
- 未提供更新回滚路径:上线后立即发现严重 bug,没有暂停 staged rollout 的预案。事先在 Runbook 文档化“暂停发布 + 恢复旧版 + 提交修复版”操作步骤。
章节小结
本章讲解了版本管理与更新分发:
- versionCode 必须整数递增,可用日期编码/git count 自动化;versionName 用语义化版本号展示给用户;
- Google Play staged rollout 按 1% → 10% → 25% → 50% → 100% 逐步放量,期间可暂停;
- In-App Update API 提供 Flexible(后台下载 + 提示)和 Immediate(全屏强制)两种模式,配合
updatePriority区分严重度; - 强制更新 真正稳健的方案是服务端下发
minSupportedVersionCode,应用对比后弹不可关闭对话框,跳转应用商店; - 国内/海外差异:国内用自研弹窗 +
market://deeplink,海外用 In-App Update。
下一章预告
应用迭代上线后,线上稳定性监控是必备能力。第 40 章将接入 Firebase Crashlytics:自动崩溃上报、自定义键值、面包屑日志、用户标识、非致命异常、NDK 崩溃符号化与 Dashboard 使用。