第2章 常见旧版本SDK的兼容性问题概述
做Android开发这么多年,我碰到最多的坑,不是新特性不会用,而是旧版本SDK那些“埋好的雷”。你想想看,Google每年发新API,市场占有率数据看着漂亮,但用户的手机可不会一夜之间全升级。我习惯把这类问题分成几大类,先给大家画个全景图。
2.1 问题类型预览
说白了,旧版本SDK兼容性问题就三大类:API被砍了、行为悄悄变了、权限模型改规则了。我做了个对照表,大家可以快速对号入座。
| 问题类型 | 典型场景 | 影响程度 | 我个人的建议 |
|---|---|---|---|
| API删除/废弃 | 比如 Android 10 删除了 WifiManager.startScan() 的部分重载 | 高(直接编译失败或运行时崩溃) | 优先用 AndroidX 包装类,避免直接调用隐藏API |
| 行为变更 | Android 8.0 后台执行限制,Android 9 明文HTTP默认关闭 | 中高(不报错但功能异常) | 必须检查 targetSdkVersion 对应的行为变更清单 |
| 权限模型差异 | Android 6.0 引入运行时权限,Android 11 自动重置权限 | 高(直接导致功能不可用) | 兼容层代码必须覆盖旧版安装时授权 + 新版运行时请求 |
2.2 问题类型详解
2.2.1 API 删除与废弃标记
我记得2019年接手过一个库,编译targetSdkVersion=28,但里面调用了 android.os.StrictMode.allowThreadDiskReads()。这个API在API Level 28标注了@Deprecated,但当时还能用。等我把target升到29…嗯,直接编译不过了。
注意:@Deprecated 不代表马上不能调,但 Android 平台随时可能在后续版本移除它。我习惯的做法是见一个替换一个,不要报侥幸心理。
常见陷阱包括:
- Hiding API:有些类或方法虽然在源码里,但被标记为
@hide。低版本通过反射能调,高版本直接被阻止。我之前遇到过DownloadManager的隐藏字段,在 Android 10 上反射崩溃。 - 内部类重构:比如
Settings.System里的某些字段在高版本被移到Settings.Global,直接引用旧路径会报错。
实用技巧:用 Lint 的 Deprecation 检查 + 在代码里加 @SuppressWarnings("deprecation") 时要三思——你关掉的是警告,不是问题。
2.2.2 行为变更:那些“不报错但跑不动”的坑
这类问题最让人头疼。API没删,编译过了,运行也不抛异常,但功能就是不对。举个典型的例子——Android 8.0 的后台执行限制。
// 旧代码:在 Service 或 BroadcastReceiver 里直接 startService
Intent intent = new Intent(context, MyBackgroundService.class);
context.startService(intent);
// 在 Android 8.0+ 上,这会在后台几秒后被系统杀掉
// 正确的做法:用 JobIntentService 或 startForegroundService
我见过一个真实案例:某个地图定位App,在后台每30秒启动一次Service刷新位置。Android 7.0 跑得好好的,升级到8.0后用户反映“定位不更新了”。日志里完全没错误,就是Service被系统掐了。
关键点:行为变更与 targetSdkVersion 直接绑定。如果你坚持 targetSdkVersion=25,Android 8.0+ 设备仍然会给你的App“老版本待遇”,不会施加后台限制。但一旦你把 target 升到26,限制立刻生效。
其他经典行为变更:
- Android 9:明文HTTP流量默认被阻止,必须添加
android:usesCleartextTraffic或配置网络安全策略。 - Android 10:
READ_PHONE_STATE权限不再能获取IMEI,返回的将是 null 或 “0”。 - Android 12:
PendingIntent必须声明FLAG_IMMUTABLE或FLAG_MUTABLE,否则运行时抛异常。
2.2.3 权限模型的代际差异
为什么单独讲权限?因为这个坑覆盖面太大,而且处理不好就是闪退。
Android 6.0 之前,安装时自动授予所有权限。6.0之后,危险权限需要运行时动态申请。Android 11 又加了权限自动重置和“仅限一次”选项。如果一个App要兼容Android 4.4到12,你想想看这中间的差异有多大。
避坑指南:我曾经写过一个权限工具类,在判断权限时只检查了 checkSelfPermission 的返回值,结果在4.4上直接报了 NoSuchMethodError——因为这个方法是在API Level 23才加入的。正确的做法是先用 Build.VERSION.SDK_INT 判断版本,再决定走哪条路径。
// 兼容写法示例
public boolean hasPermission(Context context, String permission) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
// 低版本:安装时已授予所有权限,默认直接返回true
// 但要注意:如果权限被系统禁用,这里也会出问题
return true;
} else {
return ContextCompat.checkSelfPermission(context, permission)
== PackageManager.PERMISSION_GRANTED;
}
}
2.3 问题诊断:一张检查清单
我每次接手遗留项目,都会走一遍这个清单,节省了大量排查时间:
- 检查 build.gradle 里的 minSdkVersion 和 targetSdkVersion——它们直接决定了你要兼容的范围。
- 运行 Lint 的“API调用兼容性检查”:Android Studio -> Analyze -> Inspect Code -> Android -> Lint -> Calling new methods on older versions。
- 扫描代码里所有 @Deprecated 的调用:可以用搜索
@Deprecated或 Lint 的 Deprecation 检查。 - 核对每个版本的 Behavior Changes 官方文档:从 minSdk 到 targetSdk 的每个版本都要看。
- 检查权限模型:确认危险权限都做了运行时申请,并且在旧版本有降级处理。
- 测试低版本真机或模拟器:至少用 Android 4.4、6.0、8.0、10.0、12.0 各过一遍核心流程。
一个小习惯:我会在项目根目录放一个 COMPAT_CHECKLIST.md,把所有已知的兼容性问题和处理记录写进去。团队新成员接手时,不用重复踩坑。
2.4 问题处理原则
这三种问题其实有个共同的处理思路,我归纳成三步:
- 识别:搞清楚问题属于哪一类——API删除、行为变更还是权限差异。
- 降级:根据自己的 minSdkVersion,找到每个版本上的最优解。不要为了兼容低版本写一堆丑陋的 if-else,而是用设计模式(比如策略模式)把差异封装起来。
- 验证:重点关注高版本带来的行为变更是否影响了低版本的逻辑。举个例子,你为了兼容Android 12的PendingIntent变化改了代码,要确保在Android 8.0上startActivity不出幺蛾子。