理解 SDK 版本号的含义与演变

刚开始做 Android 开发那阵子,我跟很多新手一样,对版本号那串数字完全无感。 直到有一天,我手滑把 compileSdkVersion 从 28 改成了 30,结果项目爆出一百多个编译错误…… 嗯,从那以后我才意识到,版本号背后藏着不少门道。

版本号结构:Major.Minor.Patch

标准语义化版本号(SemVer)长这样:major.minor.patch。 比如 8.0.0、12.1.3。每个位置的含义很明确:

  • Major——不兼容的 API 变更。升级后旧代码很可能要改。
  • Minor——向后兼容的功能新增。你可以放心加依赖,一般不会炸。
  • Patch——向后兼容的 bug 修复。基本无感升级。

我见过很多项目直接把 minSdkVersion 设成 API 21,结果因为某个第三方库用了 API 24 才有的 API,运行时直接崩。 说白了,版本号的每段数字都是在告诉你:这次改动的风险有多大。

个人习惯: 每次升级依赖前,先看一眼它的 changelog。如果只变了 patch 号,直接上;如果变了 minor,先扫一眼新增 API 有没有用到;major 号变了,我会留半天时间专门适配。

Android 特有的三个版本概念

在 Gradle 配置里,你会看到三个版本号:

  • compileSdkVersion —— 编译时用的 API 级别
  • minSdkVersion —— 最低兼容的 API 级别
  • targetSdkVersion —— 目标 API 级别(系统据此决定是否启用兼容行为)

这三者的关系,我建议你记住下面这张表:

名称 作用 影响范围 常见误区
compileSdkVersion 决定你能调用哪些 API 编译期 设得过高,编译通过但旧设备运行时找不到方法
minSdkVersion 决定 App 能安装的最低系统版本 安装 & 运行时 设得太低,被迫放弃高版本 SDK 的新特性
targetSdkVersion 告诉系统「我已针对此版本做过测试」 运行时兼容行为 忘记更新,导致新系统上的兼容性弹窗不消失

举个例子,如果你的 compileSdkVersion = 33,minSdkVersion = 21,targetSdkVersion = 30:

  • 编译时你可以使用 API 33 的全部功能。
  • 但运行在 API 21 的设备上时,你需要自己判断 API 可用性,否则会 NoSuchMethodError。
  • 系统看到 targetSdkVersion = 30,会认为你没有适配 Android 12+ 的行为变更,于是保留旧兼容逻辑(比如文件访问权限、前台服务限制等)。
我曾经踩过的坑: 项目一直用 targetSdkVersion = 28,结果上架 Google Play 时被告知必须更新到 30+。改完后测试一切正常,但发布后收到大量用户反馈「通知栏不显示」。排查了一整天,发现是 Android 12 开始对通知权限做了新的运行时请求,而我 targetSdkVersion 跳到 31 后,旧代码里的权限请求流程没走。从那以后,每次升级 targetSdkVersion 我都老老实实看一遍系统行为变更文档。

版本演变历史一览

Android 从诞生到现在,API 级别一路爬升。我整理了一份快速对照表:

Android 版本 API Level 关键行为变更 (对兼容性影响大的)
4.4 KitKat 19 WebView 改用 Chromium
5.0 Lollipop 21 ART 替代 Dalvik,64 位支持
6.0 Marshmallow 23 运行时权限模型
7.0 Nougat 24 多窗口支持,文件共享限制
8.0 Oreo 26 后台执行限制,通知渠道
10 29 分区存储强制化,后台位置权限细化
12 31 通知运行时权限,精确闹钟限制
13 33 通知权限,附近设备权限,细化的媒体权限
14 34 前台服务类型声明,更严格的隐式广播限制

注意看,从 API 23 开始,每次大版本都会引入至少一项「必须适配」的行为变更。 如果你长期不更新 targetSdkVersion,总有一天会踩到强制合规的雷。

核心建议: 每年至少留出一个迭代周期,将 targetSdkVersion 更新到当年主流版本。不要等商店强制要求,那时候往往只剩两三个月,仓促适配容易出事故。

兼容性策略核心:适配的“三步走”

很多同学问我:到底怎样才算「兼容」?我个人理解就是三件事:

  1. 正确选择 compileSdkVersion: 官方建议用最新稳定版。你可以拿到新 API 的编译支持,但运行时必须做降级判断。
  2. 合理设定 minSdkVersion: 根据你的用户群数据来。我习惯看 Google Play 后台的「设备与系统版本」统计,如果某个版本占比低于 1% 且该版本有已知安全问题,果断放弃。
  3. 逐步推进 targetSdkVersion: 先读行为变更文档,然后在测试分支里把 targetSdkVersion 升到目标值,针对每个新限制写适配代码,跑通过后才合并到主分支。

下面这张图帮你梳理版本号与适配流程的关系:

SDK 版本号与适配决策流程 Major (主版本) Minor (次版本) Patch (补丁) compileSdkVersion minSdkVersion targetSdkVersion 编译时 API 范围 最低安装线 运行时兼容性开关 ⚠ 升级顺序:先 compileSdk → 再 targetSdk → 最后考虑 minSdk 每次升级后必须全量回归,尤其在 runtime permission 和后台限制方面

看到这张图,你应该能理解为什么我强调「升级 compileSdkVersion 是最安全的」——它只影响编译,不影响已有设备的行为。而 targetSdkVersion 则是真正的兼容性开关,升级它才意味着你主动拥抱新系统的规则。

小技巧: 在项目的 gradle.properties 里统一定义版本变量:
COMPILE_SDK=34
MIN_SDK=21
TARGET_SDK=33
然后在各模块的 build.gradle 中引用。这样升级的时候只需要改一个地方,避免漏改。

好了,这一章就聊到这儿。版本号组合看似简单,但选不对、配不对,后面适配起来处处是坑。 记住我上面说的三步走,至少能帮你省下 80% 的踩 bug 时间。