升级前必备的兼容性知识储备

老实说,每次接手 TargetSDK 升级任务,我第一件事不是写代码,而是先把目标版本的兼容性变更清单捋一遍。你想想看,几十上百条行为变更,哪些会影响你的 App?哪些可以无感过渡?这要是没提前做好知识储备,等上线以后被 Google Play 拒审或者用户反馈炸锅,那可就来不及了。

我个人的习惯是画一张兼容性脑图,把需要关注的改动按模块拆分。下面这张图就是我常用的知识框架——先搭好骨架,再逐一填充细节。

TargetSDK升级兼容性知识体系 TargetSDK升级兼容性知识体系 兼容性知识储备 行为变更 权限模型 后台限制 非SDK接口限制 存储与文件访问 通知/广播/前台服务 暗黑模式/画中画等 运行时权限申请 后台定位/启动限制 反射限制灰名单 分区存储适配 通知渠道/SafeLog 前台服务类型声明 包可见性查询

行为变更——这些改动会自动生效

升级 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

输出会告诉你哪些接口在灰名单/黑名单里,影响范围如何。记得在产品上线前彻底解决。

存储与文件访问——分区存储的适配要点

这个内容比较多,我放在后面专门的章节详细讲。这里先提几个关键点:

  1. 用 MediaStore 代替直接文件路径操作
  2. App 私有目录(getExternalFilesDir())不受影响
  3. 如果需要访问“所有文件”,要申请 MANAGE_EXTERNAL_STORAGE 权限,但 Google Play 有审核门槛
  4. 升级到 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 限制≥ 26Service → 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 以上才有,你可以在低版本上直接移除声明。