如果你在 Android 项目里同时见过 compileSdk 35、targetSdk 34、minSdk 23 这样的组合,又不太确定它们分别管什么,那你不是一个人。我见过不少被这三个版本号支配的开发者,几乎每次升级 targetSdk 都像拆盲盒:运气好一把过,运气不好半夜三更还在排查运行时权限、分区存储、前台服务类型。这篇内容就把 MinSdkVersion、CompileSdkVersion、TargetSdkVersion 三者的区别、作用和关系一次性讲透,顺便把我踩过的坑也写进来。
先说结论:这三个版本号不是互相独立的配置项,它们分别在编译期、安装期、运行期扮演完全不同的角色。你只有把这三个角色分开理解,再去读 build.gradle 里的配置,才会觉得那些报错信息是有逻辑的,而不是玄学。
1. 一次构建报错,三个版本号同时现形
1.1 一个真实的报错现场
先还原一个我最近处理过的场景。同事从仓库拉完代码,直接执行./gradlew assembleDebug,Gradle 跑了一会儿,突然弹出一段红色的构建失败信息:
Dependency 'androidx.core:core:1.16.0' requires compileSdkVersion 35.当时项目里的compileSdk还停留在 34。同事的第一反应是"我把 core 库升到最新版本了,为什么要求 compileSdk 升到 35?这两个不是一个东西吗?"
其实这种报错特别典型。很多人会把"依赖库的版本号"和"项目编译 SDK 的版本号"混为一谈,但只要看明白 Gradle 这一段提示,就能意识到:AndroidX 库本身是用 SDK 35 编译出来的,它里面引用了一些 SDK 35 才有的 API。你的项目只有用 compileSdk 35 去编译,才能解析这些 API,否则编译器连这个类都找不到。
而这个报错还只是引子。等你把 compileSdk 改成 35,可能又会遇到 targetSdk 相关的问题,比如系统弹窗、文件读写限制、通知权限行为变化。你会发现,版本号之间像一根链条一样连着,动一个往往牵动其他两个。
1.2 三张"身份证"放在一起看
为了不绕弯,先把 Android 项目build.gradle里最常见的配置放出来,再逐个解释:
android { compileSdk 35 defaultConfig { applicationId "com.example.demo" minSdk 23 targetSdk 34 versionCode 1 versionName "1.0" } }这里不是随便写的,三个数字分别对应:
- compileSdk 35:编译阶段使用 API 35 的 Android SDK 库。
- minSdk 23:应用最低支持 Android 6.0 系统。
- targetSdk 34:应用声明自己已经在 Android 14 上做过兼容性适配。
用表格看得更直观:
| 版本号 | 配置位置 | 作用时机 | 一句话概括 |
|---|---|---|---|
| compileSdk | android.compileSdk | 编译期 | 决定你能调用哪些系统 API |
| minSdk | defaultConfig.minSdk | 安装期 | 决定哪些系统版本能安装应用 |
| targetSdk | defaultConfig.targetSdk | 运行期 | 决定系统用新规则还是旧规则对待应用 |
这三个字段不是同一维度的东西,所以它们可以有不同的值,也必须有逻辑地搭配。接下来我分别拆开讲。
2. 编译期、安装期、行为开关:三个版本号各管一段
2.1 CompileSdkVersion:编译器的"题库"边界
CompileSdkVersion 决定的是编译期能看到多少 API。Android SDK 每发布一个新系统版本,都会带着一份新的android.jar,里面定义了当前系统版本支持的全部公开 API。你的项目用 compileSdk 35 编译,Gradle 就把 API 35 的android.jar放到编译路径里,编译器对照这份"题库"检查你调用的Activity、Service、ContentResolver等方法是否存在。
所以,compileSdk 越高,你能使用的新 API 就越多。例如 Android 13 引入了NotificationManager#canPostNotifications(),如果你的 compileSdk 低于 33,调用这类方法会在编译期直接报错,显示cannot find symbol。这个报错跟真机系统版本无关,哪怕你手头的测试手机已经是 Android 16,只要 compileSdk 低,代码就编不过。
另一个容易忽略的点:android.jar只是"接口壳",并不包含系统真正的实现。你的 app 在编译期能引用高版本 API,但如果运行在一个老版本系统上,系统类库中根本没有这个方法,运行时就会抛NoSuchMethodError或NoClassDefFoundError。这也是为什么高 compileSdk 不等于可以无脑用新 API,你仍然需要在代码里判断Build.VERSION.SDK_INT。
2.2 MinSdkVersion:应用与设备的最小公约数
MinSdkVersion 是应用和设备之间的"最小公约数",它在安装期生效。系统在安装 APK 时会读取这个值,如果设备系统版本低于它,直接拒绝安装,并提示"此应用与您的设备不兼容"。
这个字段影响的不光是安装,还包括两件很重要的事:
第一,它决定了 lint 的检查标准。比如你在代码里写了getSystemService(NotificationManager.class),如果 minSdk 低于 23,lint 会警告你Class.getSystemService(Class)在 API 23 才引入,并要求你补充版本判断。如果你把 minSdk 提升到 23,这个警告就会消失。所以,minSdk 提高,代码里需要写的版本分支判断会减少,但能覆盖的用户设备也会变少。
第二,它决定了依赖库的可用范围。第三方库也有自己的 minSdk,例如某个新版本库要求 minSdk 26,你的项目如果还停留在 21,Gradle 会直接构建失败。在这种情况下,要么你想办法换低版本的库,要么你调整项目的 minSdk。这背后是一条很现实的产品决策:你的应用愿意为多少老设备付出兼容成本。
2.3 TargetSdkVersion:系统眼中的"行为开关"
TargetSdkVersion 是我认为最容易被人误解的一个字段。它既不是"目标设备的最低版本",也不是"期望运行的系统版本"。它的准确含义是:应用声明自己已经针对某个 API level 做过适配,系统可以放心地按照该版本及以后的新行为规则来运行它。
Android 系统非常喜欢做"兼容性开关":当某个行为变更会导致旧应用崩溃或异常时,系统会先看看 targetSdk 的数值。如果 targetSdk 低于该行为变更对应的 API level,系统就启用兼容模式,让应用像旧系统一样运行;如果 targetSdk 已经达到或超过该 API level,系统才执行新规则。
举几个最典型的例子:
| 系统版本 | API level | 行为变更 | 触发条件 |
|---|---|---|---|
| Android 6.0 | 23 | 运行时权限模型 | targetSdk >= 23 时,危险权限需要动态申请 |
| Android 8.0 | 26 | 通知渠道 | targetSdk >= 26 时,必须创建通知渠道才能发通知 |
| Android 10 | 29 | 分区存储 | targetSdk >= 29 时,默认访问受限外部存储 |
| Android 12 | 31 | 组件导出声明 | targetSdk >= 31 时,带 intent-filter 的组件必须声明android:exported |
| Android 14 | 34 | 前台服务类型 | targetSdk >= 34 时,前台服务必须指定类型 |
这也是为什么 Google Play 每次都会强制开发者把 targetSdk 升级到某个版本。因为如果你一直把 targetSdk 停在旧版本上,虽然系统为了兼容性会“容忍”你,但用户运行在最新系统上时,你会发现各种功能表现得很诡异:文件读不到、通知不显示、服务频繁被杀。这些大部分都是 targetSdk 太低,系统不知道该用哪套规则导致的。
3. 三者的数值关系与配置逻辑:不是越大越好,也不是越小越省
3.1 为什么通常 compileSdk >= targetSdk >= minSdk
虽然这三个字段语法上并没有强制约束,但在实际项目里,几乎所有合理配置都满足:
compileSdk >= targetSdk >= minSdk先说compileSdk >= targetSdk。targetSdk 声明的是"我适配到了某个 API level",这个级别的 API 在你的代码里自然会被引用,编译时就必须有对应的 SDK 支持。如果 compileSdk 小于 targetSdk,编译器连 targetSdk 对应的 API 都看不全,适配工作就无从谈起。所以一般情况下 compileSdk 会高于或等于 targetSdk,而且业界主流做法是让 compileSdk 直接保持最新稳定版。
再说targetSdk >= minSdk。这句话的意思是:你至少要在你所支持的最低系统版本上,验证过 targetSdk 对应的行为规则。听起来绕,但逻辑其实很直接:假如 minSdk 是 29,targetSdk 是 28,系统会认为一个跑在 Android 10 上的应用只适配到了 Android 9,于是存储读写、后台限制等行为全部走到旧分支,你就相当于白装了 Android 10 用户手上的 app。
这条表达式不必死记,把它理解成"三角形的三条边"更形象:compileSdk 是上边界,代表你能看见多少未来;minSdk 是下边界,代表你要兼容多少过去;targetSdk 是中点,代表你当前在哪个位置承诺了适配。
3.2 Android 版本与 API Level 对照表
配置之前,先把手头可以查的表列出来。很多刚接触 Android 的人会因为"安卓版本号"和"API level"对不上而迷糊,比如 Android 10 是 API 29,Android 11 是 API 30,Android 12 是 API 31,Android 12L 是 API 32,Android 14 是 API 34,Android 15 是 API 35。完整的常用对照如下:
| Android 系统版本 | API level | 发布时间 |
|---|---|---|
| Android 5.0 | 21 | 2014 |
| Android 6.0 | 23 | 2015 |
| Android 7.0 | 24 | 2016 |
| Android 8.0 | 26 | 2017 |
| Android 9 | 28 | 2018 |
| Android 10 | 29 | 2019 |
| Android 11 | 30 | 2020 |
| Android 12 | 31 | 2021 |
| Android 12L | 32 | 2022 |
| Android 13 | 33 | 2022 |
| Android 14 | 34 | 2023 |
| Android 15 | 35 | 2024 |
这个表比较实用,因为看 lint 报错、查行为变更文档、选依赖库版本时,都要用到 API level。建议收藏一份。
3.3 依赖库的"隐藏要求"
实际的 Android 项目很少是纯手写代码,基本都依赖十几个甚至几十个第三方库。每个库在自己的AndroidManifest.xml或构建配置里也有compileSdk、minSdk、targetSdk。当你构建 APK 时,Gradle 会做 manifest 合并,如果库的 minSdk 高于你项目的 minSdk,就会报类似这样的错误:
uses-sdk:minSdkVersion 21 cannot be smaller than version 24 declared in library [androidx.room:room-runtime:2.6.0]很多人第一次遇到这个错误时很慌,以为是依赖库写错了。其实这是三方版本之间的"下限冲突"。解决办法一般有三个方向:
- 把项目 minSdk 提升到库要求的版本,这是最直接的做法,前提是你愿意放弃一部分老设备用户。
- 换成支持更低 minSdk 的旧版本库,适合那些体质特殊、必须兼容老系统的项目。
- 检查是不是某个传递依赖在作怪,用
./gradlew :app:dependencies查看依赖树,把不需要的高 minSdk 依赖排除掉。
这类问题虽然没有那么高频,但一旦遇到,对那三条版本号的理解就会暴涨。
4. 实际项目配置与升级策略:从新项目到存量项目
4.1 新建项目的初始值怎么定
如果你要起一个新项目,我建议不要直接照搬模板里的数字,先想清楚你的用户画像是谁。
compileSdk直接给最新稳定版。理由很简单,编译 SDK 越新,你能用到的新 API 越多,lint 提示也更准确。而且 AndroidX 库的新版本几乎都要求较高的 compileSdk,你给到最高能减少很多版本冲突。到 2025 年,新项目直接用 API 35 是稳妥选择。
minSdk要结合业务。如果做的是面向大众市场的工具类应用,可以考虑 minSdk 23 或 24,覆盖 Android 6.0/7.0 以上的用户,兼容成本可控;如果做的是企业内部应用,设备统一都由 IT 采购,minSdk 可以直接提到 29,这样代码里可以省掉大量SDK_INT分支判断;如果你做的是一些涉及底层能力、必须拿到老版本设备特性的应用,minSdk 可能还得继续往下压。
targetSdk初始值建议比当前最新 API 低一到两个大版本。比如最新 API 是 35,新项目可以先定 34。不是说你不能直接定 35,而是你通常没有在 Android 15 真机上验证过所有功能,贸然把 targetSdk 定到 35,很可能发布后用户手上的 Android 15 设备给你触发一堆行为变更。先适配,再提升,比直接拉满更稳妥。
4.2 升级 targetSdk 的完整步骤:从 33 升到 34 为例
存量项目升级 targetSdk 是最容易翻车的事。很多人一收到商店邮件"请在 XX 日期前完成 targetSdk 升级",就直接把数字从 33 改成 34,然后重新打包上传,结果线上各种崩溃。正确的顺序应该是反过来的:
第一步:先把 compileSdk 升到 34。这一步不会影响系统行为,只是让代码有能力使用新 API。升完之后先编译一次,处理掉明显的deprecated接口报错。
第二步:通读新版本的行为变更清单。以 Android 14 为例,至少要关注前台服务类型、隐式 Intent 限制、动态广播限制、后台 Activity 启动限制等几项。这些变更不是全部立刻生效,但你要逐条对照自己的代码,找出可能命中项。
第三步:在代码里提前适配。比如 Android 14 要求每个前台服务必须声明android:foregroundServiceType,并申请对应的权限。你在 targetSdk 还是 33 的时候改好这些配置,系统不会启用新规则,代码也不会崩溃,但等 targetSdk 切到 34,新规则就会立刻接管。
第四步:做一轮真机兼容测试。重点是行为变更涉及的功能,比如 Android 14 设备上的前台服务是否正常启动、通知是否弹出、相册读写是否受限。有条件的话,覆盖 Android 13 和 Android 14 两台设备,因为新规则只在 targetSdk >= 34 时生效,你仍然需要确保老设备上的原有逻辑没有回退。
第五步:最后才修改 targetSdk 并发布。这个顺序非常重要,核心逻辑是"先让代码适应新规则,再让系统启用新规则",而不是反过来。
4.3 多模块项目如何统一版本号
项目大了以后,经常有多个 Gradle module。如果版本号散落在每个 module 的build.gradle里,升级一次 targetSdk 就要改十几个文件,很容易漏。
我建议统一收口。老项目可以在根目录的build.gradle或gradle.properties里定义变量:
ext { compileSdkVersion = 35 minSdkVersion = 23 targetSdkVersion = 34 }然后在各 module 里引用:
android { compileSdk rootProject.ext.compileSdkVersion defaultConfig { minSdk rootProject.ext.minSdkVersion targetSdk rootProject.ext.targetSdkVersion } }新项目更推荐用 Gradle 自带的libs.versions.toml版本目录。这是目前 Android Studio 新工程默认的配置方式,比 ext 更规范,还能把依赖版本和 SDK 版本统一管起来,做自动升级提醒也方便:
[versions] compileSdk = "35" minSdk = "23" targetSdk = "34"android { compileSdk libs.versions.compileSdk.get().toInteger() defaultConfig { minSdk libs.versions.minSdk.get().toInteger() targetSdk libs.versions.targetSdk.get().toInteger() } }版本号统一管理之后,升级任务就从"改一堆文件"变成"改一行配置",出错概率明显下降。
5. 常见误区和排查方法:当版本号不听话时
5.1 改了 targetSdk 后仍然触发旧行为?
有一个现象很常见:targetSdk 明明已经改成 34,代码里的某种行为却还是旧的,比如文件还是能直接读写外部存储、通知还是不用权限。
先说结论:有些行为变更是"强制性的",与 targetSdk 无关;有些则存在缓存和延迟。
典型的强制性变更包括 Android 14 的"动态广播必须声明导出标志"、Android 12 以后的PendingIntent必须显式声明可变性等。这类变更不管你 targetSdk 是多少,只要系统版本达到要求,就会生效。
而另一类变更,比如分区存储,在 targetSdk >= 29 时启用,但它依赖系统和应用的运行状态。你升级 targetSdk 后,如果 app 没有重新安装,或者旧数据目录还在,系统可能仍然沿用旧策略。这时候可以尝试在测试设备上卸载应用再重装,或者清除应用数据后重新验证。很多"改了没生效"的问题,其实是测试方式不对,不是配置问题。
5.2 compileSdk 低于依赖库要求时的正确解法
回到开头那个报错场景,Dependency requires compileSdk 35。有些人的第一反应是去降低依赖库版本,这虽然可行,但如果库的新版本包含了安全修复和 bug fix,降版本并不是好选择。
正确的做法是:先确认你本机有没有安装 API 35 的 SDK Platform。打开 Android Studio 的 SDK Manager,找到Android SDK Platform 35并勾选安装。然后再去项目里把 compileSdk 改到 35。
这里有一个细节:改完 compileSdk 后,旧代码里那些被新版本标记为deprecated的 API 可能会爆出一堆警告,但这只是警告,并不会阻止编译。真正需要担心的是如果你使用了某些在 API 35 中已经被移除的 API,编译会直接失败,这时候替代方案通常是改用新 API,或者保持条件判断兼容旧设备。
5.3 用 lint 和构建任务校准版本号
最后分享一个我平时用来排查版本号问题的技巧。Android Studio 的 lint 非常智能,打开app模块的lint报告,能直接看到类似这样的提示:
Warning: androidx.core:core requires minSdkVersion 23 Warning: This app has a targetSdkVersion lower than the recommended 35.这些警告虽然不影响构建,但能帮你提前发现问题。另一种方式是在命令行执行:
./gradlew :app:lint然后打开app/build/reports/lint-results-debug.html,里面会详细列出每个版本号相关的警告和对应的文件位置。排查慢的"隐性版本冲突"时,会比眼睛看代码高效得多。
如果你同时想知道当前项目最终的依赖版本号,也可以执行:
./gradlew :app:dependencies --configuration debugRuntimeClasspath这个命令会打出完整的依赖树,配合版本号冲突检查使用,基本能解决九成以上的版本问题。反正我现在的习惯是,每次升级 compileSdk 或 targetSdk 前,先跑一遍依赖树,再对照行为变更文档改代码,最后才动版本号字段。这套流程看起来慢,但实际踩坑的成本比省下来的几分钟高太多。