1. 两个字段,两件事,混用就出事。Google 版本说明把二者分开定义:versionCode 是「用作内部版本号的正整数。它用于判断一个版本是否比另一个更新,数字越大版本越新。它不是展示给用户的版本号,展示值是 versionName。Android 系统用 versionCode 防止降级——不允许安装 versionCode 低于设备上已装版本的安装包。」versionName 则是「展示给用户的版本号字符串……是唯一对用户可见的值」。Play Console 帮助进一步点明可操作差异:versionCode 是「随每次发布持续递增的 32 位整数」,versionName 是「面向用户的版本号字符串,例如 1.0.0、1.1 Beta、2.0」。营销侧把 4.9 改名成 5.0 完全自由;整数一旦不递增,上传当场被拒。(§https://developer.android.com/studio/publish/versioning / §https://support.google.com/googleplay/android-developer/answer/17367361)
2. Play 的上传闸门只有一条比较,而且是严格大于。官方「更新你的应用」硬条件按顺序写死三条:「更新后的 Android App Bundle 的包名必须与当前版本一致;版本号必须大于当前版本;更新包必须与当前版本使用相同签名。」没有「二进制不同就可以同号」的例外,也没有在审核里争辩的空间——这条检查是机械的,发生在任何人工或政策审核之前。(§https://support.google.com/googleplay/android-developer/answer/9859350)
3. 一个 versionCode 用过就永远不能再用,而且号段属于应用,不属于轨道。版本文档原话:「你不能用之前版本已经用过的 versionCode 向 Play 商店上传 APK。」在控制台里就是那句熟悉的拒绝——「Version code X has already been used」。因为计数器是挂在应用(包名)上的,内部测试、封闭测试、开放测试、生产四条轨道共用同一套序列;把同一个包放到第二条轨道,正确动作是从库中「添加(Add from library)」,而不是重新上传。换一个包名(比如 staging 应用、带独立 applicationId 的白标变体)才是独立序列——这也是为什么调参、试打包应当发生在 staging 应用里,而不是在正式应用上烧号。(§https://developer.android.com/studio/publish/versioning)
4. 上限是 2,100,000,000,撞上它不是提个工单,而是终局。两份文档给的是同一个数字:「Google Play 允许的 versionCode 最大值为 2100000000。」控制台的「版本号」页把后果写透:「如果应用的 versionCode 达到或超过该上限,你就无法再为该应用上传新的 APK 或 App Bundle。这意味着用户再也拿不到缺陷修复、新功能或兼容性更新。」Google 写明的唯一补救是「Google Play 可能可以提供支持」——是「可能」,不是重置按钮。对靠线上应用赚钱的业务来说,这个数字是有保质期的资产,必须当成资产来花。(§https://support.google.com/googleplay/android-developer/answer/17367361)
5. 三种编码习惯会加速烧号。用 Unix 秒级时间戳做版本号,今天已经 17.9 亿并随每次构建上涨;用跨分支、跨渠道、跨兄弟应用的全局 CI 计数器,一条失败的流水线也会把号推走;用「构建号 = 提交数」,仓库历史一重写就爆炸。Play 自己给的对策是三条:「采用可持续的 versionCode 策略:确保渐进、稳定递增,避免快速消耗可用区间;监控 versionCode 使用量:定期跟踪版本号推进,预判何时接近上限;实现自动化检查:在构建与发布流程中设置告警或拦截,防止使用过大的 versionCode。」(§https://support.google.com/googleplay/android-developer/answer/17367361)
6. 号是被「上传」消耗的,不是被「发布」消耗的。传到内部测试、放进库、建一个后来被丢弃的草稿发布,这个号都已经花掉了——库里之所以有「从库中添加」,正是因为同一个二进制可以被后来的发布重新挂上,但不能被重新生成。那种「先传上去看看能不能过」的习惯,每次尝试都扣一个号;在长寿应用加上快流水线的情况下,原本宽裕的余量就是这样在几年后变成危机的。
7. 暂停不是回滚,因为 Android 拒绝降级。第 1 条引用的系统级规则(设备不会安装 versionCode 低于已装版本的包)就是为什么控制台里没有「把用户退回上周版本」的按钮。Google 的分阶段发布指引在操作层面说的是同一件事:「如果你发现现有 App Bundle 有问题,请创建一个带修复包的新发布并 rollout。」所以一次坏的生产版本,唯一的真修复是一个 versionCode 更高、越快过审越好的新包——这正是急救位必须在需要之前就存在的原因。(§https://support.google.com/googleplay/android-developer/answer/6346149)
8. 分阶段发布只能用于更新,而且永远不会自己往前走。「你可以用分阶段发布把应用更新投放到生产和测试轨道。分阶段发布只覆盖一定比例用户,你可以随时间提高比例。分阶段发布只能用于应用更新,首次发布应用时不能使用。」还有那个让版本卡在 5% 一周的坑:「应用的分阶段发布比例不会自动提高。」必须有人回到控制台把比例调上去,Google 侧不会代劳。用户也不会被告知自己收到的是分阶段更新——「用户在分阶段发布中收到应用版本不会收到通知」——所以灰度期的反馈只能从 vitals 和客服里捞,不能指望评论区。(§https://support.google.com/googleplay/android-developer/answer/6346149)
9. 暂停分阶段发布到底改了什么。「如果你发现问题,可以暂停分阶段发布,以减少遇到该问题的用户数量。暂停后,不会再有新用户收到该分阶段发布中的应用版本;已经收到的用户会停留在该版本上。」由此有两条常被忽略的推论:恢复不会换一批人——「暂停后再恢复发布,影响的还是同一批用户」;在上一次没走完之前就发新版本,新版本会复用同一批人——「在上一次发布未完成时就分阶段发布新版本,新版本会使用与上一次相同的那批用户(取决于发布比例)」。也就是说,你的修复是被同批「倒霉的 5%」验证的,不是被一批新样本验证的。(§https://support.google.com/googleplay/android-developer/answer/6346149)
10. 暂停一个已 100% 全量发布的版本:它做什么,不做什么。「如果你发现严重问题,暂停发布功能可以通过阻止新老用户安装或更新到受影响版本,把对用户的干扰降到最低。暂停全量发布后,应用的上一版本会自动接管,向新用户以及所有未使用被暂停版本的用户提供。」这是「投放层面的替换」,不是卸载:官方说「被暂停的版本不再向用户提供」,但已经装上的设备会留在该版本,原因还是那条降级保护。同一页还写死三条限制:仅适用于「除内部测试轨道外的任何轨道」;「不能暂停某个轨道上的首个发布」;「如果上一版本存在政策违规,它不能用来替补被暂停的版本」。之后的恢复可以给任意比例,这才是把修复版本逐步放回去的正确姿势。(§https://support.google.com/googleplay/android-developer/answer/16285429)
11. 每条轨道同时只能有一个未完成的发布——这是最要命时刻卡死紧急修复的原因。「当你存在未完成的发布时,不能创建新发布。请先把分阶段发布推进到 100%,或在「发布概览」页移除变更并丢弃所有未发布的发布。」一条被丢弃在半路的 20% 灰度、一个填了一半的送审、一个留在生产轨道上的托管发布挂起,都会挡在你和紧急包之间。防它的习惯很简单:每次发布都闭环——推到 100%,或者移除并丢弃——并把轨道状态写进发布日志,避免下一个人(或下一个你)去调一个根本不肯打开新发布的控制台。(§https://support.google.com/googleplay/android-developer/answer/9859348)
12. 一套能扛十年的编号方案。用定宽分带,让算式可读、离上限永远很远:versionCode = 主版本 × 100000 + 次版本 × 1000 + 修订 × 10 + 热修,可容纳主版本 1 到 20999、每个主版本 1000 个次版本、每个次版本 10 个修订/热修位。在构建脚本里把这个工作区间显式写在 21 亿以下,并在计算值越过自设软阈值(比如 15 亿)时让构建失败(而不是让上传失败)。CI 只在「上传时」递增,绝不在每次提交时递增;所有探索性构建跑在 staging 的 applicationId 下,让它的号消耗在另一条序列上;每次发布把实际 versionCode 记进 release notes,让这个数字可审计,而不是靠口口相传。
13. 一次过拦住上述所有问题的上传前清单:(a)versionCode 严格大于该应用在任何轨道历史上上传过的所有号,而不只是本次目标轨道;(b)versionName 只是展示文本,可以重复也可以跳跃;(c)包名未变、包用线上签名密钥签名——更换应用签名密钥的后果是官方原话「已安装应用的内部测试和封闭测试用户将不再收到更新,这些用户必须卸载并重装应用才能收到更新」;(d)升级路径要在「已装旧版本」的设备上验证,因为全新安装完全不能证明升级没问题;(e)号值与 2,100,000,000 上限以及自设软阈值都核对过;(f)目标轨道上没有其他未完成的发布;(g)当前号段里仍留着至少一个急救位。这七条全部是字段或状态,不是判断题——所以整张清单可以同时活在 CI 和发布 checklist 里,而不是活在某个人的记忆里。(§https://support.google.com/googleplay/android-developer/answer/9859348)