第一关:搞懂SDK版本号、API级别、编译与目标版本

做 Android 回归测试,第一件事就是弄清楚版本号到底怎么玩的。你想想看,每年 Google 发一个新版本,从 Android 10 到 11 再到 12,名字换来换去,但咱们在 Gradle 里配的其实是 API 级别。

我个人习惯把版本号拆成两套系统:一套是面向用户的「甜点名」(现在改数字了),另一套是面向开发者的「API Level」。后者才是我们真正需要关心的。Android 12 对应 API 31,Android 13 对应 API 33,Android 14 对应 API 34…… 这张对应表你最好记在脑子里,或者贴在工位上。

Android 版本 API Level 发布时间(大致)
Android 4.4 (KitKat)192013
Android 5.0 (Lollipop)212014
Android 6.0 (Marshmallow)232015
Android 7.0 (Nougat)242016
Android 8.0 (Oreo)262017
Android 9.0 (Pie)282018
Android 10292019
Android 11302020
Android 12312021
Android 13332022
Android 14342023
Android 15352024

compileSdkVersion、minSdkVersion、targetSdkVersion 分别是什么?

每次新建项目或者修改 build.gradle 时,你都会看到这三个家伙。我刚入行时也搞混过,后来有一次踩坑才彻底明白。

  • compileSdkVersion:说白了就是「你拿哪个版本的 API 来编译代码」。比如你配了 34,你就可以用 Android 14 里新增的那些 API。但要注意,这只是一个编译时的约束,它不会影响真机运行。
  • minSdkVersion:你的 App 最低支持到哪个 API 版本。比如你设成 21,那就意味着 Android 5.0 以下的手机根本装不了你的 App。我记得之前有个项目要求兼容 Android 4.4,结果 minSdkVersion 设成 19 之后,一大堆新 API 用不了,只能靠 Support Library 兜底。
  • targetSdkVersion:这个字段最阴险。它告诉系统:「我是在哪个 API 级别上做的测试,请按那个版本的行为逻辑来运行我。」举个例子,Android 6.0 开始引入了运行时权限,如果你的 targetSdkVersion 低于 23,系统就会默认帮你兼容旧行为——不给用户弹权限请求,直接通过。但这其实是个隐患。我曾遇到过一个 App,targetSdkVersion 一直停在 28,结果上 Android 12 后,因为没适配 SplashScreen API,每次启动界面都怪怪的。

一句话记忆:compileSdkVersion 是你能用什么新东西,minSdkVersion 是你放弃了多少老用户,targetSdkVersion 是你承诺系统按哪个版本的行为干活。

android {
    compileSdkVersion 34
    defaultConfig {
        minSdkVersion 21
        targetSdkVersion 34
        // ...
    }
}

版本号背后的行为差异——为什么要回归测试?

你可能会想:改动 targetSdkVersion 有什么大不了的?不就是升级个数字吗?嗯,我以前也这么想,直到有一次我把一个老项目的 targetSdkVersion 从 28 升到 31,结果用户反馈说「用不了存储卡了」。查了半天,原来是 Android 11 开始强制推行分区存储(Scoped Storage),而我代码里还在用老路径读写文件。

这种「静默行为变化」才是回归测试最该盯住的。哪怕你的代码一行都没改,只要 targetSdkVersion 变了,系统就会启用新的限制或特性。比如:

  • 前台服务类型声明(Android 13+)
  • 通知权限必须动态申请(Android 13+)
  • 后台位置访问大幅受限(Android 10+)
  • WiFi 扫描受限于精确位置权限(Android 8+)

每一个版本升级,都相当于给你的 App 换了一个「运行环境」。不做回归测试,就像开着一辆没检查刹车的车下山。

我的习惯:每次打算提升 targetSdkVersion 时,我会先翻一下 Android 版本行为变更文档,把影响自己功能的那几条圈出来,写进测试用例。然后至少跑一遍全量 monkey 测试和核心流程手工测试。

编译版本 vs 目标版本——到底听谁的?

编译版本决定了「你能调用的 API 集合」。目标版本决定了「系统怎么对待你的 App」。这两个不一定非要同步。你完全可以用 compileSdkVersion 34 编译,但 targetSdkVersion 还是 30。这样做的好处是,你可以提前用新 API 写代码,但暂时不触发新版本的行为变更,留给自己更多适配时间。

但要注意,Google Play 对 targetSdkVersion 有硬性要求。我记得从 2022 年开始,新上架的应用必须至少 target 到 API 31。所以别拖太久。

我曾经踩过的一个坑:在开发过程中为了测试新特性,把 compileSdkVersion 升到 34,但忘记同步升 targetSdkVersion。结果用户手机上 behavior 没变,但编译时用了新 API,导致代码里有些 deprecated 的用法没报错。上线后才发现某些场景下行为不符合预期。后来我强制规定:每次升级 compileSdkVersion 时,必须同步 review 一遍 targetSdkVersion 是否也该升了,至少要把行为变更清单过一遍。

把关系理清楚——一张图就够了

下面这张图把三个核心版本号以及它们与设备、代码的关系画了出来。看懂了它,你在配置 build.gradle 时心里就有底了。

Android SDK 版本核心概念关系图 compileSdkVersion minSdkVersion targetSdkVersion 编译时使用哪些 API 最低兼容的 API 版本 系统按此版本行为运行 实际设备 API Level 编译时检查 设备 API < min → 无法安装 系统按 target 行为运行 关键原则 compileSdk ≥ targetSdk ≥ minSdk 设备 API ≥ minSdk 才能安装,但行为只取决于 target 与设备版本

看这张图时,重点看箭头方向。其实设备上的真实 API 级别是硬件决定的,我们无法改变。我们能控制的只有这三个配置。它们加起来,决定了 App 能跑在哪些设备上、能用新 API 吗、运行时系统给不给开绿灯。

一个小工具:检查当前设备的 API Level

做回归测试时,你可能需要快速知道测试手机的 API Level。用 adb 就能搞定:

adb shell getprop ro.build.version.sdk

它返回一个整数,比如 34。配合 ro.build.version.release 还能看到 Android 版本号。我常在写自动化脚本时先获取这个值,然后动态决定跑哪些测试集。

嗯,SDK 版本管理其实就是这么回事。数字背后藏着行为变化,而行为变化就是测试人员的战场。搞定这三个配置,你就迈出了多版本回归测试的第一步。