第2章 常见旧版本SDK的兼容性问题概述

做Android开发这么多年,我碰到最多的坑,不是新特性不会用,而是旧版本SDK那些“埋好的雷”。你想想看,Google每年发新API,市场占有率数据看着漂亮,但用户的手机可不会一夜之间全升级。我习惯把这类问题分成几大类,先给大家画个全景图。

旧版本SDK兼容性问题分类全景图 旧版本SDK兼容性问题分类全景图 旧版本SDK兼容性问题 API删除/废弃 行为变更 权限模型差异 @Deprecated Hiding API Runtime行为变化 UI默认值变化 运行时权限 其他 核心判断逻辑: 1. 先确认 minSdkVersion 与 targetSdkVersion 的差异 2. 检查 API 是否在目标版本中被标记为 @Deprecated 或 behavior change 3. 用 Lint 扫描 + 兼容性测试覆盖低版本实机 4. 尽量使用 Support Library / AndroidX 提供的兼容包装类

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 问题诊断:一张检查清单

我每次接手遗留项目,都会走一遍这个清单,节省了大量排查时间:

  1. 检查 build.gradle 里的 minSdkVersion 和 targetSdkVersion——它们直接决定了你要兼容的范围。
  2. 运行 Lint 的“API调用兼容性检查”:Android Studio -> Analyze -> Inspect Code -> Android -> Lint -> Calling new methods on older versions。
  3. 扫描代码里所有 @Deprecated 的调用:可以用搜索 @Deprecated 或 Lint 的 Deprecation 检查。
  4. 核对每个版本的 Behavior Changes 官方文档:从 minSdk 到 targetSdk 的每个版本都要看。
  5. 检查权限模型:确认危险权限都做了运行时申请,并且在旧版本有降级处理。
  6. 测试低版本真机或模拟器:至少用 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不出幺蛾子。