升级前必备的兼容性知识储备
老实说,每次接手 TargetSDK 升级任务,我第一件事不是写代码,而是先把目标版本的兼容性变更清单捋一遍。你想想看,几十上百条行为变更,哪些会影响你的 App?哪些可以无感过渡?这要是没提前做好知识储备,等上线以后被 Google Play 拒审或者用户反馈炸锅,那可就来不及了。
我个人的习惯是画一张兼容性脑图,把需要关注的改动按模块拆分。下面这张图就是我常用的知识框架——先搭好骨架,再逐一填充细节。
行为变更——这些改动会自动生效
升级 TargetSDK 后,Android 系统会自动启用对应 API 级别的行为变更,不需要你改一行代码。但正是因为“自动”,很多人容易忽略测试。我记得有一次项目升到 Android 12(API 31),结果短信应用里的通知样式全变了——原来系统默认启用了 NOTIFICATION_TRAMPOLINE 行为,非精确通知被归并。
常见的自动行为变更包括:
- Android 9.0(API 28):禁止明文 HTTP 通信(默认启用 Network Security Config)
- Android 10(API 29):Scoped Storage 强制启用;后台启动 Activity 限制
- Android 11(API 30):包可见性限制(Package Visibility)
- Android 12(API 31):精确闹钟权限(SCHEDULE_EXACT_ALARM);通知 trampoline 限制
- Android 13(API 33):通知运行时权限(POST_NOTIFICATIONS)
⚠️ 警告:别以为把这些变更列成表格就完了。我吃过一次亏——Android 10 的 requestLegacyExternalStorage 只兼容到 API 29,升到 API 30 就直接失效。你必须在升 TargetSDK 之前就确认每个行为变更的 生效范围,有些有宽限期(比如 API 29→30 的分区存储),有些没有。
权限模型——从“安装时授权”到“运行时授权”
这个不用多说,Android 6.0 引入运行时权限,但很多老 App 升到 API 23+ 会直接 crash——因为没有做权限检查。我建议你先把所有危险权限的调用点列出来,然后统一处理授权逻辑。
给一段最简单的权限请求代码示例:
// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 请求权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, REQUEST_CODE);
} else {
// 权限已有,打开相机
openCamera();
}
但难点通常在权限组合场景。比如扫码 App 需要同时申请相机和存储权限。我遇到过一个案例:用户拒绝了“始终允许”的位置权限,但 App 没有处理拒绝回调,导致地图一直空白。
💡 技巧:用 shouldShowRequestPermissionRationale() 来判断用户是否曾经拒绝过。如果返回 true,最好弹出一个解释说明,再请求权限。这样能提升授权成功率——我在 Google Play 上架测试中验证过,解释后的授权率从 40% 升到了 65%。
后台限制——你的 Service 还能跑吗?
从 Android 8.0(API 26)开始,后台 Service 被大幅限制。如果 App 在后台,启动 startService() 会抛 IllegalStateException。解决办法是用 startForegroundService() + 在 5s 内弹出通知。
但要注意,Android 9.0 之后,前台 Service 也多了限制——必须声明 foregroundServiceType。具体查官方 Manifest 里的 <service> 标签。下面是一个合规的声明示例:
<service
android:name=".MyLocationService"
android:foregroundServiceType="location"
android:exported="false" />
另外,Android 12 引入了 闹钟提醒豁免 机制,如果你的 App 需要精确的定时任务,必须在 Manifest 中声明 SCHEDULE_EXACT_ALARM 权限,并且引导用户去系统设置里开启。
非 SDK 接口限制——反射党的噩梦
Android 9.0 开始,Google 对非 SDK 接口(也就是你用反射访问的 @hide 方法、字段)进行灰名单管理。到 Android 11 以上,反射调用会一直报 NoSuchMethodError 或 AccessDeniedException。
我自己踩过一个坑:某个第三方 SDK 用反射调用 ActivityManager.getRunningTasks,在 Android 11 上直接崩溃。解决方法只能是联系 SDK 作者更新,或者自己替换成 UsageStatsManager。你可以用 Google 提供的 Veridex 工具扫描 APK,看看有哪些隐式调用:
# 解压 apk,然后运行
java -jar veridex.jar --apk your_app.apk
输出会告诉你哪些接口在灰名单/黑名单里,影响范围如何。记得在产品上线前彻底解决。
存储与文件访问——分区存储的适配要点
这个内容比较多,我放在后面专门的章节详细讲。这里先提几个关键点:
- 用
MediaStore代替直接文件路径操作 - App 私有目录(
getExternalFilesDir())不受影响 - 如果需要访问“所有文件”,要申请
MANAGE_EXTERNAL_STORAGE权限,但 Google Play 有审核门槛 - 升级到 API 30 时,记得检查
android:requestLegacyExternalStorage="true"的兼容期(仅 API 29)
📌 举个例子:以前你可能是这样保存图片的:
// 旧方式(API 29 以下)
File file = new File(Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES), "photo.jpg");
// 直接写入
现在要改成:
// 新方式(API 29+)
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.DISPLAY_NAME, "photo.jpg");
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
Uri uri = getContentResolver().insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
// 通过 uri 打开输出流写入
通知渠道、广播限制、前台服务类型等
这三个我合在一起说,因为都和用户体验密切相关。
- 通知渠道:Android 8.0 强制要求。你必须在
NotificationManager.createNotificationChannel()中注册所有通知渠道。未注册的渠道发通知会静默失败。 - 隐式广播限制:Android 8.0 开始,大部分隐式广播不再支持静态注册。改为用
JobScheduler或WorkManager替代。 - 前台服务类型:Android 10+ 对
foregroundServiceType做了细分(如 camera, location, connectedDevice, mediaPlayback 等)。声明错误会导致ForegroundServiceStartNotAllowedException。
我把这些知识整理成了一个快速核对表,方便你逐项检查:
| 检查项 | 受影响 API 级别 | 预设动作 |
|---|---|---|
| 运行时权限适配 | ≥ 23 | 检查所有危险权限调用点 |
| 后台 Service 限制 | ≥ 26 | Service → Foreground Service |
| 隐式广播限制 | ≥ 26 | 改用动态注册或 WorkManager |
| 非 SDK 接口检测 | ≥ 28 | 运行 Veridex 扫描 |
| 分区存储适配 | ≥ 29 | 用 MediaStore 文件 URI |
| 前台服务类型声明 | ≥ 29 | 在 manifest 中添加 foregroundServiceType |
| 包可见性限制 | ≥ 30 | 添加 <queries> 声明 |
| 精确闹钟权限 | ≥ 31 | 申请 SCHEDULE_EXACT_ALARM |
💡 最后一个小技巧:我习惯在 AndroidManifest.xml 里写好 android:maxSdkVersion 来避免老设备上的不必要变更。比如某个权限只在 API 30 以上才有,你可以在低版本上直接移除声明。