
如果你在 Android 项目里同时见过 compileSdk 35、targetSdk 34、minSdk 23 这样的组合又不太确定它们分别管什么那你不是一个人。我见过不少被这三个版本号支配的开发者几乎每次升级 targetSdk 都像拆盲盒运气好一把过运气不好半夜三更还在排查运行时权限、分区存储、前台服务类型。这篇内容就把 MinSdkVersion、CompileSdkVersion、TargetSdkVersion 三者的区别、作用和关系一次性讲透顺便把我踩过的坑也写进来。先说结论这三个版本号不是互相独立的配置项它们分别在编译期、安装期、运行期扮演完全不同的角色。你只有把这三个角色分开理解再去读 build.gradle 里的配置才会觉得那些报错信息是有逻辑的而不是玄学。1. 一次构建报错三个版本号同时现形1.1 一个真实的报错现场先还原一个我最近处理过的场景。同事从仓库拉完代码直接执行./gradlew assembleDebugGradle 跑了一会儿突然弹出一段红色的构建失败信息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 上做过兼容性适配。用表格看得更直观版本号配置位置作用时机一句话概括compileSdkandroid.compileSdk编译期决定你能调用哪些系统 APIminSdkdefaultConfig.minSdk安装期决定哪些系统版本能安装应用targetSdkdefaultConfig.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 低于 23lint 会警告你Class.getSystemService(Class)在 API 23 才引入并要求你补充版本判断。如果你把 minSdk 提升到 23这个警告就会消失。所以minSdk 提高代码里需要写的版本分支判断会减少但能覆盖的用户设备也会变少。第二它决定了依赖库的可用范围。第三方库也有自己的 minSdk例如某个新版本库要求 minSdk 26你的项目如果还停留在 21Gradle 会直接构建失败。在这种情况下要么你想办法换低版本的库要么你调整项目的 minSdk。这背后是一条很现实的产品决策你的应用愿意为多少老设备付出兼容成本。2.3 TargetSdkVersion系统眼中的行为开关TargetSdkVersion 是我认为最容易被人误解的一个字段。它既不是目标设备的最低版本也不是期望运行的系统版本。它的准确含义是应用声明自己已经针对某个 API level 做过适配系统可以放心地按照该版本及以后的新行为规则来运行它。Android 系统非常喜欢做兼容性开关当某个行为变更会导致旧应用崩溃或异常时系统会先看看 targetSdk 的数值。如果 targetSdk 低于该行为变更对应的 API level系统就启用兼容模式让应用像旧系统一样运行如果 targetSdk 已经达到或超过该 API level系统才执行新规则。举几个最典型的例子系统版本API level行为变更触发条件Android 6.023运行时权限模型targetSdk 23 时危险权限需要动态申请Android 8.026通知渠道targetSdk 26 时必须创建通知渠道才能发通知Android 1029分区存储targetSdk 29 时默认访问受限外部存储Android 1231组件导出声明targetSdk 31 时带 intent-filter 的组件必须声明android:exportedAndroid 1434前台服务类型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 是 29targetSdk 是 28系统会认为一个跑在 Android 10 上的应用只适配到了 Android 9于是存储读写、后台限制等行为全部走到旧分支你就相当于白装了 Android 10 用户手上的 app。这条表达式不必死记把它理解成三角形的三条边更形象compileSdk 是上边界代表你能看见多少未来minSdk 是下边界代表你要兼容多少过去targetSdk 是中点代表你当前在哪个位置承诺了适配。3.2 Android 版本与 API Level 对照表配置之前先把手头可以查的表列出来。很多刚接触 Android 的人会因为安卓版本号和API level对不上而迷糊比如 Android 10 是 API 29Android 11 是 API 30Android 12 是 API 31Android 12L 是 API 32Android 14 是 API 34Android 15 是 API 35。完整的常用对照如下Android 系统版本API level发布时间Android 5.0212014Android 6.0232015Android 7.0242016Android 8.0262017Android 9282018Android 10292019Android 11302020Android 12312021Android 12L322022Android 13332022Android 14342023Android 15352024这个表比较实用因为看 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 34android { 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 前先跑一遍依赖树再对照行为变更文档改代码最后才动版本号字段。这套流程看起来慢但实际踩坑的成本比省下来的几分钟高太多。