A
第 39 章JAVA35 分钟

应用版本与更新策略

应用版本与更新策略: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. 操作流程

  1. 进入 Play Console → 正式发布 → 创建新版本;
  2. 上传新 AAB(versionCode 递增);
  3. 在版本详情底部 「国家/地区版本发布阶段」 勾选 「分阶段发布」;
  4. 填百分比:1% / 5% / 10% / 25% / 50% / 100%;
  5. 提交审核(约 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 不支持),所以国内/海外要做渠道区分。

常见坑与最佳实践

  1. versionCode 用 git hash:必须是整数,hash 是字符串。改用 git rev-list --count HEAD。
  2. 多 flavor 共用 versionCode 序列:国内市场每家要求独立版本序列,华为版本号和 Play 不能错位。每个 flavor 独立 versionCode 递增。
  3. staged rollout 一次到 100%:没缓冲,崩溃影响全员。每次至少 3 天观察期,10% → 25% → 50% → 100%。
  4. Immediate 更新没在 onResume 恢复:用户切后台回来时之前的强制更新弹窗丢失,绕过了强制更新。务必在 onResume 检查并重启。
  5. 每次启动弹更新:用户烦死。Flexible 模式加 shouldPrompt() 限频,至少 24 小时间隔。
  6. 检查更新阻塞启动:网络慢的话首屏卡 1-2 秒。检查逻辑放异步线程,失败静默不阻断主流程。
  7. In-App Update 在国内无效:API 只在装了 Google Play 服务的设备可用,国内手机大多没有。国内版本走自研 + deeplink 跳市场方案。
  8. play-core 版本过旧:旧版本不支持 Immediate 优先级 / staged rollout。建议用 app-update:2.1.0 以上。
  9. 强制更新不可关闭但用户可卸载:再强制也挡不住用户卸载。强制更新对话框文案要让用户理解“为什么必须更新”,否则用户直接卸载。
  10. 未提供更新回滚路径:上线后立即发现严重 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 使用。