Android 版本适配
★ 版本适配是考察 Android 基础扎实度和工程经验的试金石。不要死记硬背每个 API,重点讲清“为什么改”以及“线上怎么平滑过渡”。
一、Android 6.0 - 9.0 核心适配回顾
虽然年代久远,但这是现代 Android 权限和后台限制的基石:
- Android 6.0 (M): 动态权限机制(Runtime Permissions)。核心思想:敏感权限必须在用到时申请,不能全靠安装时授权。
- Android 7.0 (N): FileProvider。严禁在 Intent 中传递
file://URI,必须使用content://,提升跨应用文件共享的安全性。 - Android 8.0 (O):
- 后台执行限制:应用在后台时不能随便启 Service,推荐用 JobScheduler 或 WorkManager。
- 通知渠道(Notification Channels):通知必须分类,把控制权交给用户。
- Android 9.0 (P): 限制 Http 明文请求(需配置 networkSecurityConfig),以及非 SDK 接口限制(深反射受限)。
二、Android 10 - 11:存储与隐私大修
这是近年来适配痛点最集中的版本,核心是分区存储与包可见性。
- Android 10 (Q):
- Scoped Storage (分区存储):应用默认只能访问自己的沙盒目录和公共媒体集合(MediaStore)。不能再随便遍历
/sdcard。 - 后台定位限制:细分了前台定位和后台定位权限。
- Scoped Storage (分区存储):应用默认只能访问自己的沙盒目录和公共媒体集合(MediaStore)。不能再随便遍历
- Android 11 (R):
- 强制分区存储:TargetSDK 30 时
requestLegacyExternalStorage失效,必须彻底适配 MediaStore 或 SAF。 - 包可见性 (Package Visibility):不能再无脑
getInstalledPackages()。必须在 Manifest 中用<queries>声明你要交互的应用包名,防流氓应用拉取用户应用列表。
- 强制分区存储:TargetSDK 30 时
三、Android 12:用户体验与安全加固
- SplashScreen API:系统强制的闪屏规范。启动时自动接管显示 App 图标,需适配新的主题属性,否则会出现“双闪屏”。
- PendingIntent 可变性:必须显式声明
FLAG_IMMUTABLE或FLAG_MUTABLE,防止组件劫持。 - 精确闹钟限制:
AlarmManager.setExact()需要申请SCHEDULE_EXACT_ALARM权限,防止滥用唤醒系统。 - 蓝牙权限细分:分离了定位和蓝牙权限,扫蓝牙不再必须申请定位权限(需声明
neverForLocation)。
四、Android 13:细粒度权限时代
- 通知运行时权限:发通知不再是默认开启的,必须动态申请
POST_NOTIFICATIONS权限。 - 细化的媒体权限:废弃了粗放的
READ_EXTERNAL_STORAGE,拆分为:READ_MEDIA_IMAGESREAD_MEDIA_VIDEOREAD_MEDIA_AUDIO
- 照片选择器 (Photo Picker):无需任何存储权限即可让用户选择图片,强烈建议替换掉自研的图片选择器。
五、Android 14:前台服务与后台行为收紧
- 前台服务类型强制:启动 Foreground Service 必须声明对应的类型(如
location,mediaPlayback,dataSync),且必须附带具体的原因和对应权限。 - 隐式 Intent 限制:内部组件通信必须使用显式 Intent 或指定 package,防止被外部恶意截获。
- 精确闹钟默认拒绝:TargetSDK 34 的非日历/闹钟类应用,精确闹钟权限默认关闭,需引导用户去设置页开启。
六、Android 15/16 时代:从“能跑”到“体验与合规默认正确”
新版本适配不再只是补权限和崩溃点,而是系统把更安全、更一致的用户体验变成默认约束。面试中可以把 Android 15/16 作为“升级 targetSdk 的系统工程”来讲。
| 方向 | 高频变化 | 适配思路 |
|---|---|---|
| Edge-to-edge | 更强调沉浸式与系统栏区域处理 | 全局梳理状态栏/导航栏、WindowInsets、底部输入框和列表 padding |
| Predictive Back | 返回手势需要预览与可预测动画 | 用 AndroidX Back APIs 统一返回栈,不在页面里散落 onBackPressed |
| 媒体与相册权限 | 部分照片/视频授权成为常态 | 优先 Photo Picker;必须读媒体库时兼容“只授权部分资源” |
| 后台任务 | 前台服务、精确闹钟、后台启动继续收紧 | 能延迟就 WorkManager;必须前台服务就给用户可见通知和明确类型 |
| Privacy Sandbox | 广告归因、跨 App 跟踪能力继续收缩 | 不依赖单一设备标识;把归因、风控、合规披露拆开设计 |
| 大屏与折叠屏 | 面试更常追问适配能力 | 用 WindowSizeClass、响应式布局、状态保存和多窗口测试 |
Edge-to-edge 的典型坑:只把 decorFitsSystemWindows=false 打开,但没有处理输入法、底部导航栏和 RecyclerView 最后一项遮挡。正确做法是把 Insets 当成布局输入,统一沉淀到基础容器或设计系统。
Predictive Back 的典型坑:页面自己拦截返回键、弹窗/Sheet/导航栈各管各的,导致返回预览和实际结果不一致。应统一到 Navigation/OnBackPressedDispatcher/OnBackInvokedCallback 这类生命周期感知入口。
七、版本适配速查矩阵与面试策略
为了在面试中快速调取记忆,可以参考以下核心版本适配矩阵:
| 版本与 TargetSDK | 核心变更 | 面试常问痛点 | 常见解决方案与对策 |
|---|---|---|---|
| Android 10 (29) | 分区存储初步引入、后台定位 | 读写 SD 卡崩溃 | 声明 requestLegacyExternalStorage 过渡,切沙盒目录 |
| Android 11 (30) | 强制分区存储、包可见性 | 第三方分享/支付失败 | Manifest 添加 <queries> 标签声明依赖的包名 |
| Android 12 (31) | PendingIntent 可变性、四大组件 Exported | 通知点击崩溃、安装失败 | 补充 FLAG_IMMUTABLE,强声明 android:exported |
| Android 13 (33) | 通知权限、细粒度媒体权限 | 发不出通知、读图失败 | 动态申请 POST_NOTIFICATIONS,接入 PhotoPicker |
| Android 14 (34) | 前台服务类型、精确闹钟收缩 | 后台服务崩溃 | 补充 foregroundServiceType 及对应权限,转用 WorkManager |
| Android 15/16 (35/36) | Edge-to-edge、Predictive Back、隐私与后台限制持续收紧 | 页面被系统栏遮挡、返回动画异常、部分媒体授权漏处理 | 统一 Insets/Back 处理,优先系统 Picker,按场景治理后台任务 |
TargetSDK 升级工程化 Playbook
- 盘点风险面:权限、存储、通知、前台服务、后台任务、隐式 Intent、PendingIntent、第三方 SDK。
- 先升 CompileSDK 再升 TargetSDK:先解决编译 API 和依赖兼容,再打开新的运行时限制。
- 建立兼容矩阵:覆盖 Android 10–最新版本、主流厂商 ROM、深色模式、大屏/折叠屏、弱网和后台场景。
- 第三方 SDK 准入:推送、广告、统计、支付、地图、登录 SDK 必须确认支持目标版本,否则灰度期很容易“自己业务没问题,SDK 崩了”。
- 埋点与灰度:按系统版本、targetSdk、渠道、机型聚合 crash/ANR/权限拒绝/关键业务转化。
- 可回滚设计:客户端二进制无法真正回滚,必须通过配置开关、服务端降级、暂停灰度和热修复预案降低影响。
面试实战:版本适配的回答结构
当被问到“讲讲你做过哪些版本适配”时,用 STAR 法则 构建你的回答:
- 情境(Situation): “在升级 TargetSDK 到 33 时…”
- 任务(Task): “…我们遇到了图片选择和通知发送失效的问题。”
- 行动(Action): “…我将存储权限拆分请求,并接入了官方的 Photo Picker,同时利用一套版本判断工具类统一封装了通知权限的申请逻辑。”
- 结果(Result): “…不仅解决了崩溃,还把包体积减小了(因为移除了臃肿的第三方相册组件),并且减少了向用户索要危险权限的次数,合规性提升。”
高频面试题
Q1:你们是如何平滑过渡分区存储(Scoped Storage)的?
答:我们分了两步走。首先在 TargetSDK 29 时通过 requestLegacyExternalStorage="true" 作为过渡方案。其次,梳理了所有文件读写场景:将应用内缓存切到 Context.getExternalFilesDir();将需要分享给用户的图片/视频,改为通过 MediaStore API 插入;对于需要用户选择外部文件的场景,接入了 Storage Access Framework (SAF)。
Q2:Android 12 的 PendingIntent 崩溃问题怎么排查和解决?
答:这是因为 TargetSDK 31 后未声明可变性。我们首先通过全局搜索 PendingIntent.getActivity/getBroadcast 找到所有调用点。对于只用于拉起界面的点击通知,统一加上 PendingIntent.FLAG_IMMUTABLE;对于需要回传输入结果(如 Direct Reply)的场景,使用 FLAG_MUTABLE。同时检查了第三方推送 SDK 是否已升级到兼容版本。
Q3:如何处理 Android 14 前台服务的限制?
答:梳理业务中所有的 Foreground Service。比如定位打卡服务,在 Manifest 声明 foregroundServiceType="location",并在启动前确保已获得前台定位权限。如果是数据同步,尽量将其迁移到 WorkManager,因为 Android 14 极度不推荐长驻的后台同步服务。
Q4:如果让你负责把 App 升级到最新 targetSdk,你会怎么做? 答:我会按“风险盘点 → 依赖升级 → 灰度验证 → 指标观察 → 降级预案”推进。先列出权限、存储、通知、后台任务、Intent、第三方 SDK 等风险点;再用自动化测试和真机矩阵覆盖主路径;上线时按系统版本和渠道分批灰度,重点看 crash、ANR、权限拒绝率和业务转化。不能只在本地跑通就直接全量。
易错点 / 追问
- TargetSDK 与 CompileSDK 混淆:CompileSDK 是编译时环境,TargetSDK 是运行时的兼容模式开关。只有提升了 TargetSDK,系统才会用新的限制策略约束你。
- 第三方 SDK 拖后腿:自己业务适配了,但旧版第三方 SDK 仍用老 API 导致崩溃。追问解决方案时,可以说“推动 SDK 更新、使用 ASM 字节码插桩拦截修改、或寻找替代方案”。
- 忽略条件判断:在调用新 API 时没有包裹
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU),导致在老机器上发生NoSuchMethodError崩溃。