前言
Android 12 是 2021 年 10 月发布的最新正式版本,然而很多同学表示还没有适配。针对开发者在进行版本适配过程中遇到的问题,我们建立了 GitHub · AndroidPlatformWiki。我们希望站在开发者的视角,全面且深刻地解读每个 Android 版本更新,以此建立起一个体系化的 Android 系统适配手册。具体包括:
两个维度
根据内容相关度,我们将从 2 个维度解读:
- 基于时间线: 现阶段官方每年会发布一个新的版本,因此有必要以一个 Android 版本为单位,解读该版本涉及的新功能与行为变更。这样可以帮助开发同学了解新版本的更新内容,例如我们会通过一个文档解读 Android 13 版本的更新内容与适配自查表;
- 基于内容线: 通常一个系统功能模块会历经多个系统版本更新才会趋于稳定,因此有必要以一个功能为单位,解读该功能的主要能力以及不同版本的变更和差异。这样可以帮助开发同学了解该功能在不同版本上的差异,例如我们会通过一个文档单独解读系统通知。
三个等级
根据故障敏感性分级,我们将系统变更的兼容性划分为 3 个等级:
- 强制适配 ❗: 所有应用必须适配,否则会出现编译不通过、功能不可用或者用户体验受损等问题;
- 推荐适配 ⭐: 不强制要求适配,但适配的应用将获得更出色的用户体验或更安全的隐私保护等收益;
- 已适配: 应用不需要任何改动就已经兼容。
两类行为变更
系统行为变更通常属于以下两种类别之一:
- 面对所有应用的行为变更: 运行在该系统版本上的所有应用都会影响,而无论应用的 targetSDKVersion 为何。通常应该先针对这些变更进行适配和测试,这有助于用户在新版本系统上运行你的应用时,用户体验不会受损;
- 以特定 targetSDKVersion 为目标版本的行为变更: 只有 targetSDKVersion 高于或等于系统版本的应用会影响,通常是影响较大或适配工作量较大的变更,我们可以理解为一个 Google 留给开发者的适配缓冲。
Android 12 适配自查表
根据故障敏感性分级,我们将 Android 12 系统变更的兼容性划分为 3 个等级:
- 强制适配❗: 涉及该功能的所有应用必须适配的变更,不适配的应用会出现编译不通过、功能不可用,或者用户体验出现一定受损等问题;
- 推荐适配⭐: 不强制要求适配的变更,适配的应用具有更出色的用户体验或更安全的隐私保护等;
- 已适配: 应用不需要任何改动就可以兼容的变更。
以 Android 12 为目标版本的应用
类别 | 变更 | 兼容性 | 摘要 |
1. 用户体验 | 自定义通知外观模板统一 | 强制❗ | 自定义通知的内容区域缩小为自定义通知模板内的一块区域,不再完整覆盖通知区域 |
画中画 (PiP) 交互改进 | 推荐⭐ | 优化画中画 (PiP) 模式的用户交互 | |
Toast 视图改进 | 已适配 | 系统 Toast 视图文本最多可以显示两行,并且始终在文本旁边显示应用图标 | |
2. 安全和隐私设置 | 新蓝牙运行时权限(新) | 推荐⭐ | 引入一些新运行时权限,用于更好地管理应用于附近蓝牙设备的连接,而无需请求位置信息权限 |
传感器采样率限制 | 已适配 | 系统会限制某些移动传感器和位置传感器的数据的刷新率 | |
应用休眠改进 | 已适配 | 扩展应用休眠机制 | |
数据访问审核中的归因标记改进 | 强制❗ | 归因标记必须在 Manifest 文件中声明 | |
ADB 备份限制 | 已适配 | adb backup 导出的数据不再默认包含应用数据 | |
显式指定组件 exported 属性 | 强制❗ | 声明了 过滤器的组件必须显式设置 android:exported 属性 | |
显式指定 PendingIntent 可变性 | 强制❗ | PendingIntent 必须显式声明一个可变性标志 | |
检测不安全的嵌套 Intent 启动 | 已适配 | StrictMode 会检测不安全的嵌套 Intent 启动 | |
3. 性能和电池 | 精确的闹钟权限(新) | 强制❗ | 设置 AlarmManager 精准闹钟的应用必须在 Manifest 中声明权限 |
前台服务启动限制 | 强制❗ | 除了少数情况外,禁止应用从后台启动前台服务 | |
通知 trampoline 限制 | 强制❗ | 禁止从通知 trampoline 间接启动目标 Activity |
所有应用
类别 | 变更 | 兼容性 | 摘要 |
4. 用户体验 | Material You 设计语言(新) | 已适配 | 新的设计语言 |
富媒体内容插入(新) | 推荐⭐ | 应用可以从统一的位置接受任何来源(剪贴板粘贴、键盘输入或拖放操作)的内容 | |
支持 AVIF 图片(新) | 推荐⭐ | 支持 AVIF 格式图片 | |
应用启动动画 API SplashScreen(新) | 强制❗ | 支持定制应用启动转场动画 | |
Widget 桌面小部件改进 | 推荐⭐ | 改进 Widgets 外观和行为 | |
图形 API 改进 | 推荐⭐ | 新增图形效果 | |
OverScroll 过度滑动动画改进 | 已适配 | 过度滑动动画改为拉伸和反弹效果 | |
通知改进 | 推荐⭐ | 增加新的通知样式和安全保障 | |
HTTP 深度链接解析改进 | 已适配 | 调整了 HTTP Intent 的默认解析行为 | |
全屏模式的手势导航改进 | 推荐⭐ | 增加了一次交互即可执行手势导航的模式 | |
屏幕尺寸 API 变更 | 强制❗ | 针对适配每种设配上获取屏幕尺寸的需求,系统引入了新 API | |
多窗口模式标准化 | 强制❗ | 在大屏设备中,系统会为所有 Activity 启用多窗口模式 | |
延迟展示前台服务通知 | 已适配 | 除了特殊情况外,前台服务通知会延迟 10 s 显示 | |
activity 生命周期改进 | 已适配 | 修改根 Activity 的返回行为 | |
Surface 帧率切换改进 | 推荐⭐ | 引入强制切换帧率的 API | |
5. 安全和隐私设置 | 隐私信息中心(新功能) | 推荐⭐ | 隐私信息中心以一个时间轴的方式显示过去时间内所有应用对于敏感信息的访问情况 |
支持只授予粗略位置权限(新) | 强制❗ | 用户可以只授予应用模糊位置权限 | |
麦克风和摄像头切换开关(新) | 已适配 | 用户可以通过全局切换开关停用整台设备上的摄像头或麦克风权限 | |
麦克风和摄像头指示标示(新) | 已适配 | 应用使用麦克风或相机时,状态栏会有图标标记。 | |
剪贴板访问提示(新) | 已适配 | 应用首次从另一个应用访问剪辑数据时,会弹出一个消息框消息 | |
隐藏应用叠加窗口(新) | 推荐⭐ | 应用的窗口可见时可以隐藏所有可见的系统级悬浮窗口 | |
应用无法关闭系统对话框 | 强制❗ | 除了特殊情况外,禁止应用尝试关闭系统对话框 | |
屏蔽不信任的触摸事件 | 强制❗ | 屏蔽从不同应用的窗口传递的事件 | |
6. 性能和电池 | 应用待机分区改进 | 已适配 | 引入了一个新的受限待机分区 |
第 1~3 节介绍的是以 Android 12 为目标版本的应用行为变更和新功能更新,我将这部分更新总结为 3 部分:
- 1、用户体验(以 Android 12 为目标版本)
- 2、安全和隐私设置(以 Android 12 为目标版本)
- 3、性能和电池(以 Android 12 为目标版本)
1. 用户体验(以 Android 12 为目标版本)
1.1 自定义通知外观模板统一
Android 系统通知可以分为两类样式:标准通知 + 自定义通知
- 标准通知: 标准通知是指基于
NotificationCompat.Builder#setContentTitle()
等模板 API 构建通知,最终会按照系统预置的视图模板展示。例如:
- 自定义通知: 自定义通知是指基于
NotificationCompat.Builder#setCustomContentView()
等 API 构建的通知,最终会按照开发者自定义的的布局展示,而不会按照标准通知模板展示。
从 Android 12 系统开始,系统规范了自定义通知的外观和行为,自定义通知的内容区域缩小为自定义通知模板内的一块区域,不再完整覆盖通知区域。因此,如果你的应用使用了自定义通知,则需要进行必要的测试和调整:
- 布局调整: 由于内容区域缩小了,需要调整并测试通知布局;
- 设置展开式通知: 由于所有通知都是可展开的,所以需要调用
NotificationCompat.Builder#setCustomBigContentView()
设置展开后布局,确保展开和收起状态一致。
下图是统一的自定义通知模板:
可以看出,这次改动是 Google 希望自定义通知能够呈现相对一致的感观体验,以及减少不同设备上产生的兼容性问题。
1.2 画中画 (PiP) 交互改进
画中画模式是 Android 8.0 中引入的一种多窗口模式,最常用于视频播放 Activity,能够实现在视频播放过程中打开其他应用,而不退出中断当前视频。目前主流的音视频 App 都支持画中画模式,你可以在系统设置中搜索 “画中画” 查看。这次改动是 Google 对画中画模式的用户交互进行优化,具体参考资料:
- 对画中画的支持 —— 官方文档
- Android 12 画中画改进 —— 官方文档
1.3 Toast 视图改进
在 Android 12 中,系统 Toast 视图文本最多可以显示两行,并且始终在文本旁边显示应用图标。相关资料:消息框概览
2. 安全和隐私设置(以 Android 12 为目标版本)
2.1 新蓝牙运行时权限(新功能)
Android 12 系统引入了新的运行时权限 BLUETOOTH_SCAN、BLUETOOTH_ADVERTISE 和 BLUETOOTH_CONNECT 权限,用于更好地管理应用于附近蓝牙设备的连接。
在低版本中,应用与附近蓝牙设备连接需要用户授予 ACCESS_FINE_LOCATION
精确位置权限,这其实是不合理的设计,因为用户很难理解为什么蓝牙连接会跟位置信息有关。从 Android 12 系统开始,ACCESS_FINE_LOCATION 精确位置权限是可选项,只要应用不会通过蓝牙推导物理位置信息,就不再需要请求。如果不会,你需要在 Manifest 中显式做出 usesPermissionFlags
声明:
<manifest> <!-- Include "neverForLocation" only if you can strongly assert that your app never derives physical location from Bluetooth scan results. --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> ... </manifest> 复制代码
- 新蓝牙权限体系(以 Android 12 为目标版本):
- BLUETOOTH_SCAN:允许搜索附近蓝牙设备;
- BLUETOOTH_ADVERTISE:允许当前设备暴露给其他蓝牙设备;
- BLUETOOTH_CONNECT:允许当前设备连接其他蓝牙设备;
- ACCESS_FINE_LOCATION(可选):允许由蓝牙信息推导设备位置信息。
- 旧蓝牙权限体系:
- BLUETOOTH:允许与蓝牙相关的交互;
- ACCESS_FINE_LOCATION(必选):允许由蓝牙信息推导设备位置信息,在 Android 9 或以下版本,可以用 ACCESS_COARSE_LOCATION 替代。
另外,BLUETOOTH_SCAN 等权限是 NEARBY_DEVICES 附近设备权限组的一部分。请求该权限组的权限,权限授予对话框会提示用户批准访问附近的设备。
可以看出,这次的改动 Google 是希望连接蓝牙设备的权限授予能够给用户更精准的权限功能描述。
相关资料:
2.2 传感器采样率限制
大多数 Android 设备都有内置传感器,用来测量运动、屏幕方向和各种环境条件,这些传感器能够提供高度精确的原始数据。为了保护有关用户的潜在敏感信息,Android 12 系统会限制某些移动传感器和位置传感器的数据的刷新率。
相关资料:传感器概览 —— 官方文档
2.3 应用休眠改进
Android 11 引入了应用休眠机制,如果用户有几个月没有与应用交互,那么系统会将应用置于休眠 / 冬眠状态,Android 12 扩展了应用休眠机制:
- Android 11:重置已授予的运行时敏感权限;
- Android 12:重置已授予的运行时敏感权限;无法从后台运行任务;无法接收推送通知;应用缓存文件会被删除。
相关资料:应用休眠 —— 官方文档
2.4 数据访问审核中的归因标记改进
Android 11 引入了数据访问审核 API,开发者可以在应用访问用户隐私数据的代码位置增加归因标记,并通过注册 AppOpsManager.OnOpNotedCallback
监听。这个功能提供了对调用隐私数据的监听,无论是应用层还是依赖库中的代码,只要访问到私密数据(危险权限)都会回调。从 Android 12 系统开始,归因标记必须在 Manifest 文件中声明,例如:
<manifest ...> <!-- The value of "android:tag" must be a literal string, and the value of "android:label" must be a resource. The value of "android:label" should be user-readable. --> <attribution android:tag="sharePhotos" android:label="@string/share_photos_attribution_label" /> ... </manifest> 复制代码
相关资料:数据访问审核 —— 官方文档
2.5 ADB 备份限制
为了保护私有应用数据,Android 12 变更了 adb backup
命令的默认行为,adb backup 导出的数据不再默认包含应用数据。如果开发阶段需要依赖于 adb backup 导出的应用数据,可以将 Manifest 文件中将 android:debuggable 设置为 true 来导出应用数据。
2.6 显式指定组件 exported 属性
组件属性 android:exported
用于设置该组件是否支持其他应用交互,exported 为 false 表示不允许该组件被其他应用启动。一般 exported 属性默认为 false,除非组件声明了 <intent-filter>
过滤器(即支持隐式启动),则 exported 属性默认为 true。从 Android 12 系统开始,声明了 过滤器的组件必须显式设置 android:exported 属性。例如:
<service android:name="com.example.app.backgroundService" android:exported="false"> <intent-filter> <action android:name="com.example.app.START_BACKGROUND" /> </intent-filter> </service> 复制代码
否则,在编译应用时就会有报错:
Manifest merger failed : Apps targeting Android 12 and higher are required \ to specify an explicit value for android:exported when the corresponding \ component has an intent filter defined. 复制代码
如果使用低版本的 Android Gradle 插件虽然可以编译成功,但安装时会报错:
Installation did not succeed. The application could not be installed: INSTALL_FAILED_VERIFICATION_FAILURE 复制代码
可以看出,这次改动背后的理念是 “不要相信默认值”,因为不符合预期的默认值会产生更严重的风险。举个例子,由于开发者的疏忽,一个原本不允许外部应用启动的组件未显式声明 android:exported=“false”,而正好该组件声明了 过滤器,那么就因为默认值的影响的产生了一个安全风险。而强制开发者对声明 过滤器的组件显式声明 android:exported 的值,就可以避免了默认值的安全风险。同样的道理在对接外部系统时,也不要相信默认值,例如网络请求参数的默认值,能传的就传。
2.7 显式指定 PendingIntent 可变性
为了使 PendingIntent 的处理更加安全,Android 12 要求 PendingIntent 必须显式声明一个可变性标志即 FLAG_MUTABLE 或 FLAG_IMMUTABLE。在此之前,PendingIntent 默认是可变的。
2.8 检测不安全的嵌套 Intent 启动
Android 12 引入了一项 StrictMode
检查规则,用于检测不安全的嵌套 Intent 启动。StrictMode 模式大家很熟悉了,这里解释下为什么嵌套 Intent 启动是不安全的。
举个例子,开发者的预期效果是 Client App 请求 Provider App 的一个服务,并且希望在请求结束后回调到 Client App 的 ClientCallbackActivity。那么,最直接的方法是将启动 ClientCallbackActivity 的 Intent 当作参数嵌套到启动 ApiService 的 Intent 里。例如:
乍看起来没有问题,但其实这种实现方式存在两个隐蔽的安全风险:
- Client App: 由于 ClientCallbackActivity 是从另一个应用 Provider App 启动的,因此它必须暴露为 exported。这意味着除了 Provider App 外,设备上其他恶意的应用也可以启动 ClientCallbackActivity;
- Provider App: 由于嵌套的 Intent 是在 Provider App 的上下文中启动的,因此恶意应用 Attacker App 可以将 Provider App 的任何一个 Activity 嵌套其中,即使启动的是私有的非 exported 的 Activity,这让 Provider App 防不胜防。
解决方法是使用 PendingIntent 替代嵌套 Intent,PendingIntent 是 Intent 的包装容器,也类似于一个嵌套 Intent。但是,很多小伙伴简单地认为 PendingIntent 只是延迟待处理的 Intent,两者只有时间维度的区别,这是片面的。
PendingIntent 的最主要的作用是授权外部应用以本应用的身份执行使用嵌套的 Intent。有点拗口哈,在我们这个例子里,就是 Client App 将启动 ClientCallbackActivity 的 Intent 暴露给 Provider App 后,但 Provider App 在使用 PendingIntent 时,系统会以 Client App 的上下文身份来使用嵌套的 Intent。
PendingIntent pendingIntent = PendingIntent.getActivity(application, 0, resultIntent, 0); 复制代码
现在,我们再回顾下还有没有风险:
- Client App: 由于 PendingIntent 使用 Client App 的身份使用嵌套的 Intent,那么 ClientCallbackActivity 不再需要暴露为 exported;
- Provider App: 由于 PendingIntent 使用 Client App / Attacker App 的身份使用嵌套的 Intent,而它们是没有权限访问 Provider App 非 exported 的 ApiSensitiveActivity 的。
相关资料:Android 嵌套 Intent —— 官方博客文章