Android 17 开发者适配文档

一、概览

类别影响范围适配优先级变更/新特性
安全所有应用usesClearTraffic 弃用计划
核心功能所有应用后台音频强化
安全所有应用限制隐式 URI 授权
安全所有应用密钥库限制
用户体验和系统界面所有应用在旋转后恢复默认 IME 可见性
人工输入所有应用触控板指针捕获模式变更
核心功能所有应用线程优先级设置范围强制校验
安全所有应用已回收 Parcel 对象的访问限制
安全所有应用文件操作模式严格校验
界面和窗口所有应用以下config变化不会触发activity relaunch
隐私权所有应用短信动态密码保护
安全所有应用Android developer verification
隐私权targetSdk 37+本地网络权限保护
核心功能targetSdk 37+新的 MessageQueue 无锁实现
安全targetSdk 37+本地主机保护
安全targetSdk 37+更安全的本地动态代码加载
设备规格targetSdk 37+大屏自适应
安全targetSdk 37+Activity安全性增强
安全targetSdk 37+默认启用证书透明度
安全targetSdk 37+Parcel 数据加固
核心功能targetSdk 37+静态 final 字段现在不可修改
隐私权targetSdk 37+NPU权限声明
无障碍targetSdk 37+复杂 IME 实体键盘输入的无障碍支持
核心功能所有应用新的 ProfilingManager 触发器
核心功能所有应用JobDebugInfo API


二、应用适配 -> 所有应用

2.1 usesClearTraffic 弃用计划

说明:

自 Android 6.0 引入、Android 9.0 默认关闭明文传输以来,android:usesCleartextTraffic 属性一直是开发者规避 HTTPS 强制要求的常用手段。

从 Android 17 开始,系统将计划弃用Manifest 中的 usesCleartextTraffic 属性。这意味着该声明在 Android 17 及更高版本的系统上将不再生效,系统将转而强制执行更为严格的网络安全校验。

应用若需建立 HTTP 明文连接,必须通过 Network Security Config 文件显式指定允许通信的特定域名(Domain)。系统将不再支持通过这个全局属性开关开启应用范围内的所有 HTTP 连接,未在白名单配置文件中列出的明文流量将被系统直接拦截。

影响:

若应用未及时适配,在 Android 17及以上版本设备上运行将面临以下风险:

应用仅通过 Manifest 声明(android:usesCleartextTraffic=true)而未配置相应的网络配置 XML 文件,在升级到 Android 17+ 设备上运行时,所有 HTTP 请求将被系统拦截。

适配建议:

除测试环境或极少数无法升级的旧接口外,建议尽可能将所有网络通信迁移至 HTTPS,从根本上符合 Android 的安全演进方向。如无法完全迁移,需按以下步骤配置域名白名单:

1. 根据 minSdkVersion 确定适配方案

由于网络安全配置文件是在 Android 7.0(API 24)引入的,适配逻辑如下:

  • 若 minSdkVersion < 24: 由于旧系统不识别 XML 配置文件,仍需在 Manifest 中保留 android:usesCleartextTraffic=”true” 以兼容旧设备。同时,必须增加网络安全配置文件以适配 Android 17+。
  • 若 minSdkVersion ≥ 24: 应直接使用网络安全配置文件,并建议从 Manifest 中移除 android:usesCleartextTraffic 属性。

2. 配置网络安全文件

  • 在 res/xml/ 下创建 network_security_config.xml。
  • 通过 <domain-config> 精确授权特定域名。

示例代码:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">api.legacy-service.com</domain>
        <domain includeSubdomains="false">192.168.1.100</domain>
    </domain-config>
    <base-config cleartextTrafficPermitted="false" />
</network-security-config>
  • 在 AndroidManifest.xml 中关联配置:
<application
    android:networkSecurityConfig="@xml/network_security_config"
    android:usesCleartextTraffic="true" 
    ...>
</application>

重要提示:尽管 Google 目前尚未明确该特性的强制执行时间计划,但其潜在风险极大。一旦后续版本强制启用该策略,未适配的应用将面临HTTP明文流量被拦截的严重后果。请开发者务必尽快完成网络安全配置适配。


2.2 后台音频强化

说明:

在 Android 17 之前,应用请求音频焦点或播放音频的限制相对宽松。但从 Android 17 开始,系统将强制执行严格的音频访问生命周期校验。

核心准则:应用必须处于前台可见状态,或拥有具备 “使用中(While-In-Use, WIU)” 能力的 mediaPlayback 类型前台服务(FGS),才能获得并维持音频输出权限。

  • WIU 能力的 FGS 启动规则
FGS启动方式是否授予WIU能力说明
应用前台时启动推荐方式,用于延续用户主动触发的音频操作
后台启动(BFSL)否(特例除外)仅在响应通知点击、组件交互、媒体按键等用户显式操作时授予 WIU
系统委托启动(如 Telecom 库)系统级授权,适用于通话类场景
  • 受影响的音频 API 清单
操作类型失效行为具体受影响API
音频播放静默无声(无报错)AudioTrack.write()、AAudioStream_write(NDK)、OpenSL ES、Media3/ExoPlayer/Oboe
音频焦点请求返回 AUDIOFOCUS_REQUEST_FAILEDAudioManager.requestAudioFocus()
音量 / 铃声模式调整静默失效(无报错)setStreamVolume()、setStreamMute()、adjustStreamVolume()、setRingerMode() 等

影响:

如果应用未按规范配置具备 WIU 能力的 mediaPlayback FGS,将面临以下风险:

  • “空跑”风险:应用切后台或息屏后,正在播放的音频将变为“无声播放”。这意味着进度条仍在走,但用户听不到声音。当用户重新切换回前台时,会发现音频已播放到非预期位置,严重影响体验。
  • 后台控制失效:在后台尝试调整音量、响铃模式或启动新音频播放的行为将全部失效,且由于接口不返回异常,开发者难以在代码层面直接捕获此类失败。
  • 特定触发场景受阻:例如在后台定时任务中播放提醒音,或在响应 BOOT_COMPLETED(开机广播)时尝试直接播放音频或调节音量等操作。若未按规范配置 FGS 声明权限,这些音频请求都将被拦截。

适配建议:

核心原则:后台音频(如音乐 / 播客播放、熄屏继续播放)必须依赖带 WIU 能力的 FGS,且 FGS 需在用户主动触发音频操作时启动。

1. 优先采用 Media3 框架

采用 MediaSessionService 组件管理后台音频,该库已封装 FGS 生命周期和 WIU 能力管理,无需手动适配;

2. 手动实现 FGS(未使用 Media3 时)

若因特殊需求无法使用 Media3,开发者需手动管理 mediaPlayback 类型的 FGS,并严格遵守以下准则:

  • 启动时机:必须在应用处于前台(可见)时,或响应明确的用户操作(如媒体键点击、通知栏交互)时启动 FGS。
  • FGS 保活:短暂异常(如网络缓冲、AUDIOFOCUS_LOSS_TRANSIENT)时,FGS 需保活 ≤10 分钟;
  • 生命周期终止:当播放内容结束(无下一首)、用户主动暂停或发生不可恢复的错误时,必须及时释放音频资源并关闭 FGS。
  • 恢复播放:仅在用户显式操作(如点击播放按钮、媒体按键)时,重新启动 FGS 并恢复播放。

3. 处理静默失败

音频播放、响铃模式和音量调节API调用不会提示任何异常或失败信息,因此无法捕捉异常。但音频焦点切换(AudioManager.requestAudioFocus())失败会返回AUDIOFOCUS_REQUEST_FAILED,可以通过检查返回值来确认。


2.3 限制隐式 URI 授权

说明:

系统将在 Android 18 中停止对 Send、SendMultiple、ImageCapture 等操作自动授予 URI 读写权限,要求应用显式使用 FLAG_GRANT_READ_URI_PERMISSION 和 FLAG_GRANT_WRITE_URI_PERMISSION 来授权。Android 17 作为过渡版本,建议开发者提前适配此变更。

1. 旧版(Android 18 之前)的行为

当前 Android 系统中,若 App A 发送包含 URI 的 Intent,且 Intent 动作是 ACTION_SEND/ACTION_SEND_MULTIPLE/ACTION_IMAGE_CAPTURE 时,系统会自动、隐式地给接收该 Intent 的目标 App B 授予该 URI 的「读 / 写权限」—— 无需 App A 显式声明授权,也无需用户确认。

  • 例:App A 通过 ACTION_SEND 分享一张图片(URI 为 content://xxx/photo.jpg)给 App B,系统会自动让 App B 获得这张图片的读写权限;
  • 问题:这种 “自动授权” 存在安全漏洞 —— 恶意 App 可通过监听这类 Intent 窃取 URI 对应的敏感数据,或篡改数据,且开发者无法自主管控权限范围、有效期。

2. Android 18 的新行为(计划变更)

  • 核心规则:系统不再为上述 Intent 动作自动授予 URI 读 / 写权限。若 App A 希望目标 App B 访问该 URI,必须显式调用 API 授予权限,否则 App B 访问 URI 时会抛出 SecurityException(权限拒绝)。
  • 目标:将 URI 权限的管控权完全交还给开发者,遵循 “最小权限 + 显式授权” 原则,提升跨 App 数据交互的安全性。

影响:

若应用未能及时完成显式授权适配,以下场景可能会受到影响:

  • 应用通过 ACTION_SEND(单文件分享)、ACTION_SEND_MULTIPLE(多文件分享)、ACTION_IMAGE_CAPTURE(拍照 / 截图保存)发送包含 URI 的 Intent;
  • 依赖系统自动授权,未显式授予 URI 权限的跨 App 数据交互(如分享图片、文件、视频给其他应用);

适配建议:

  • 方案 1:基础显式授权(推荐,适配所有场景)

发送 Intent 前,通过 grantUriPermission() 给目标 App 显式授予 URI 权限,可指定权限类型(读 / 写)和有效期:

// 示例:分享图片时,显式授予URI权限
fun shareImageToApp(context: Context, targetPackage: String, imageUri: Uri) {
    // 1. 显式授予URI的读权限(可根据需求添加写权限:Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
    context.grantUriPermission(
        targetPackage, // 目标App的包名(精准授权,避免泛授权)
        imageUri,
        Intent.FLAG_GRANT_READ_URI_PERMISSION // 权限类型:读
    )
    // 2. 构建分享Intent(ACTION_SEND)
    val shareIntent = Intent(Intent.ACTION_SEND).apply {
        putExtra(Intent.EXTRA_STREAM, imageUri)
        type = "image/jpeg"
        setPackage(targetPackage) // 指定目标App,避免跳转到其他应用
        // 可选:添加权限标记(强化授权,兼容低版本)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }
    // 3. 启动Intent
    context.startActivity(shareIntent)
}
  • 方案 2:临时授权(通过 Intent Flag,简化版)

若不确定目标 App 包名(如分享到 “任意应用”),可通过 Intent Flag 授予临时权限(权限有效期至 Intent 处理完成):

val shareIntent = Intent(Intent.ACTION_SEND).apply {
    putExtra(Intent.EXTRA_STREAM, imageUri)
    type = "image/jpeg"
    // 授予所有接收该Intent的App临时读权限(有效期短,相对安全)
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
context.startActivity(Intent.createChooser(shareIntent, "分享图片"))


2.4 密钥库限制

说明:

Android 密钥库(Android Keystore)是设备上所有应用共享的关键系统资源。为了防止单一应用过度占用资源导致系统不稳定,从 Android 17 开始,系统将对单个应用可创建的密钥总数实施强制配额限制。如果应用尝试创建超出限制的密钥,则创建会失败并抛异常 KeyStoreException。

  • 密钥数量限制对比
应用类型Android 16及之前Android 17+
非系统应用(targetSdk ≥ 37)✅ 无限制❌ 50,000 个密钥上限
非系统应用(targetSdk < 37)✅ 无限制⚠️ 200,000 个密钥上限
系统应用(任意 targetSdk)✅ 无限制⚠️ 200,000 个密钥上限
  • 错误码差异

超限时,KeyStoreException 的 getNumericErrorCode() 返回值因 targetSdk 而异:

应用目标版本getNumericErrorCode() 返回值含义
targetSdk ≥ 37ERROR_TOO_MANY_KEYS(新值)明确告知密钥过多
targetSdk < 37ERROR_INCORRECT_USAGE通用错误码(兼容旧版)

影响:

如果应用在业务逻辑中存在大量生成密钥的行为(如为每个用户、每个文件甚至每次会话生成独立密钥),将面临以下风险:

  • 创建操作失败:当达到配额上限时,后续所有生成新密钥的请求都将抛出异常,导致加密、解密或签名功能瘫痪。
  • 错误处理差异化:系统根据应用的 targetSdkVersion 返回不同的错误代码,若代码层未做针对性适配,可能导致异常捕获逻辑失效:
    • Target API ≥ 37 的应用:调用 getNumericErrorCode() 返回新定义的 ERROR_TOO_MANY_KEYS。
    • 其他应用:调用 getNumericErrorCode() 返回 ERROR_INCORRECT_USAGE。

适配建议:

1. 优化密钥管理策略

  • 审查应用架构,避免为大量琐碎的数据条目分别创建独立密钥。建议通过主密钥(Master Key)+ 派生密钥(Key Derivation)的方案来降低 Keystore 的实际条目数。
  • 在安全性允许的范围内,尽可能复用现有的安全密钥,而不是频繁生成新密钥。

2. 建立密钥清理机制

定期清理已失效、已过期或用户已注销的相关密钥条目。开发者应通过 KeyStore.deleteEntry(alias) 主动回收配额,确保应用始终运行在安全水位线内。


2.5 在旋转后恢复默认 IME 可见性

说明:

在 Android 17 之前,当用户旋转屏幕触发 Activity 重建时,系统默认会尝试自动恢复软键盘(IME)的显示状态。

从 Android 17 开始,系统将取消这一逻辑。如果应用未声明自行处理配置更改(即 Activity 会经历完整的销毁与重建过程),系统将不再主动恢复旋转前的键盘可见性。这意味着,除非应用显式请求,否则重建后的页面默认不弹出键盘。

影响:

该变更仅影响「未处理配置变更」的应用场景,判断标准:

  • 受影响的场景:
    • 应用未在 AndroidManifest.xml 中给 Activity 声明 android:configChanges 处理旋转等配置变更;
    • 应用未重写 Activity.onConfigurationChanged() 方法;
    • 配置变更后系统自动重建 Activity(如旋转、语言切换、屏幕尺寸变化)。
  • 不受影响的场景:
    • 应用已通过 android:configChanges 声明处理旋转(如android:configChanges=”orientation|screenSize”),配置变更时 Activity 不重建;
    • 应用主动调用 hideSoftInputFromWindow() 隐藏键盘,或 showSoftInput() 显示键盘的逻辑。

适配建议:

若需在旋转后维持键盘的显示状态,建议开发者根据业务逻辑选择以下适配方案:

方案 1:静态配置

在 AndroidManifest.xml 的对应 Activity 节点中设置:

<activity
    android:name=".EditActivity"
    <!-- 核心配置:始终保持软键盘可见 -->
    android:windowSoftInputMode="stateAlwaysVisible|adjustResize">
</activity>
  • 优点:无需写代码,适配成本最低;
  • 缺点:该 Activity 启动(获取焦点)时键盘也会默认弹出(若有输入框),适合 “以输入为核心” 的页面(如编辑器、聊天页);

方案 2:动态配置

适合仅需在 “配置变更后” 显示键盘,而非启动时就显示的场景,有两种实现方式:

  • 方式 A:在 onCreate () 中主动请求键盘
class EditActivity : AppCompatActivity() {
    private lateinit var editText: EditText
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_edit)
        editText = findViewById(R.id.et_content)
        // 配置变更后(Activity 重建),显式请求弹出键盘
        if (savedInstanceState != null) { // 判读是否是配置变更导致的重建
            editText.post { // 延迟执行,确保View已加载
                val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
                imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
            }
        }
    }
}
  • 方式 B:重写 onConfigurationChanged () 处理

若应用已声明处理配置变更(android:configChanges),可在该方法中恢复键盘:

// 1. 先在Manifest中声明处理配置变更
<activity
    android:name=".EditActivity"
    android:configChanges="orientation|screenSize|keyboardHidden">
</activity>
// 2. 重写onConfigurationChanged()
override fun onConfigurationChanged(newConfig: Configuration) {
    super.onConfigurationChanged(newConfig)
    // 配置变更后,检查输入框是否有焦点,有则恢复键盘
    val editText = findViewById<EditText>(R.id.et_content)
    if (editText.hasFocus()) {
        val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
        imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
    }
}

需要做好兼容:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.SEVENTEEN) {
    // Android 17+ 显式请求键盘
    imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}


2.6 触控板指针捕获模式变更

说明:

这是一项针对 Android 大屏设备(平板、折叠屏及 ChromeOS)交互体验的底层优化,旨在统一触控板(Trackpad)与鼠标在“指针捕获”模式下的行为表现。

  • 在 Android 17 之前,当应用请求指针捕获时,触控板的行为类似于“触摸屏”(上报绝对坐标)。从 Android 17 开始,触控板在捕获模式下默认模拟鼠标行为,传递“相对位移”事件。
  • 调用 View.requestPointerCapture() 后,触控板的移动和滚动手势将不再上报绝对位置,而是转换为与鼠标一致的相对偏移量(Delta)。
  • 系统新增了 View.requestPointerCapture(int) 方法,允许开发者在 RELATIVE(相对,默认)和 ABSOLUTE(绝对,旧版行为)两种模式间手动切换。

影响:

该变更主要影响对坐标精度有特殊要求的应用:

  • 如果应用(如专业绘图、手写板类软件)原本依赖触控板的“绝对坐标”来定位笔触,升级到 Android 17 后,默认的相对模式会导致画笔无法准确定位,产生类似鼠标操作的漂移感。
  • 对于大多数游戏或 3D 协作类应用,此变更有助于直接复用鼠标逻辑;但对于强依赖绝对定位的应用,必须显式适配新 API。

适配建议:

  • 普通应用/游戏:默认情况下无需修改。应用将自动获得与鼠标一致的触控板操作体验,且开发者无需再为触控板编写复杂的坐标转换逻辑。
  • 特殊业务场景:若业务逻辑强依赖触控板的原始绝对坐标,需在请求捕获时改用显式调用:
requestPointerCapture(POINTER_CAPTURE_MODE_ABSOLUTE)


2.7 线程优先级设置范围强制校验

说明:

在 Android 17 之前,系统对 Process.setThreadPriority(int priority) 执行的是“兼容性容错策略”。

  • 内核机制:Android 线程优先级基于 Linux 内核的 niceness 机制。其标准取值范围严格限定在 [-20, 19] 之间。数值越小,线程获取 CPU 时间片的权重越高(-20 为最高优先级,19 为最低优先级)。
  • Android 16 及其以下:系统底层在接收到参数后,会先进行一次数值合法性预处理。如果开发者传入了超出法定区间的数值(如 -50 或 100),系统并不会报错,而是自动将其修正为该区间的临界值(即:小于 -20 的统一按 -20 处理,大于 19 的统一按 19 处理)。这种做法虽然保证了调用的成功率,但也导致开发者难以察觉代码中的逻辑边界错误。
  • Android 17:为了规范 API 的标准调用并确保系统调度逻辑的严谨性,Android 17 彻底废弃了这种“范围自动修正”机制。系统现在要求传入的参数必须严格落在合法区间内。任何越界参数都会被视为非法指令,直接由系统抛出异常进行拦截。

影响:

由于该变更会直接触发运行时异常(RuntimeException),如果应用在调用时未进行 try-catch 捕获,将直接导致应用闪退(Crash)。

适配建议:

  • 规范参数传入:在调用接口前,务必对优先级数值进行前置逻辑判断,确保数值在合法区间内。
  • 增加异常捕获机制:对于所有调用 Process.setThreadPriority 的地方,增加显式的异常捕获,防止因非预期数值导致进程崩溃。


2.8 已回收 Parcel 对象的访问限制

说明:

Parcel 是 Android 系统中 IPC(进程间通信)数据的容器。为了提高内存复用效率,系统设计了 recycle() 机制将不再使用的 Parcel 对象回收到缓存池中。在 Native 层,调用 recycle() 会清空 Parcel 内部持有的内存指针和数据状态。

  • Android 16 及其以下:当一个 Parcel 对象被执行 recycle() 后,如果应用层逻辑错误地再次调用 readParcelable 等读取方法,系统通常会由于内存引用失效而返回一个 null 对象。
  • Android 17:为了强化内存安全规范并防止潜在的内存破坏问题,Android 17 引入了状态一致性检查。一旦 Parcel 被标记为已回收(Recycled),任何后续的读取尝试都会被直接拦截,并由系统强制抛出 BadParcelableException。

影响:

应用如果存在“先回收、后读取”的逻辑问题,或者在多线程环境下对同一个 Parcel 对象进行竞争操作,将直接导致应用进程崩溃。

适配建议:

核心原则: Parcel 对象一旦调用 recycle() ,即视作该对象已销毁。严禁在 recycle() 之后存在任何针对该实例的引用操作。

  • 严格收拢回收时机:避免在业务逻辑中间杂乱调用 recycle()。推荐使用 try-finally 结构,确保数据读取完全闭环后,再在 finally 块中统一释放资源。
  • 异步场景的引用安全:在跨线程(如 Handler 或 Thread)传递数据时,严禁直接传递 Parcel 对象。错误做法: 在主线程回收 Parcel,在子线程读取 Parcel。正确做法: 在主线程(或发送端)完成 Parcel 数据的完整解析,将解析后的数据传递给异步线程。


2.9 文件操作模式严格校验

说明:

在 Android 系统中,FileUtils.translateModeStringToPosix 接口常用于将字符串格式的文件操作模式(如 “rw”)转换为底层 Linux 系统的文件打开标志(Posix Flags)。Android 17 上对该接口进行了安全性增强,该变更旨在通过严格的模式校验,防止应用在执行文件读取操作时,因参数组合不当意外触发“写入、截断或追加”等破坏性行为。

  • 标志说明:t (Truncate):截断模式,打开即清空原文件内容。a (Append):追加模式,写入时定位至文件末尾。
  • Android 16 及其以下:系统对模式组合较为宽容。例如,允许 rt(只读+截断)、ra(只读+追加)这种逻辑矛盾的组合存在。这可能导致开发者原本只想“读取”文件,却因为误传了 t 导致文件内容被意外清空。
  • Android 17:引入了“读写权限隔离”的安全校验。强制要求 t(截断)和 a(追加)必须配合 w(写入)模式同时出现。如果试图在纯读取(r)模式下组合 t 或 a,系统将视其为高危非法操作,直接抛出异常。

影响:

由于该变更会强制触发 RuntimeException,应用若未做参数预校验或异常捕获,将直接导致 应用闪退 (以下组合会在Android 17上抛异常:ra、rt、rwa)。

适配建议:

检查所有调用 FileUtils.translateModeStringToPosix 或底层涉及文件模式转换的地方。

  • 如果业务只需要读取,请严格使用 “r”。
  • 如果需要读写并追加,应拆分为两次操作,或根据业务需求修正为合规组合(如 “rw”,且在写入逻辑中处理偏移)。


2.10 以下config变化不会触发activity relaunch

说明:

为防止状态丢失,系统默认情况下不再针对以下特定配置更改重启 Activity:

配置常量含义典型触发场景
CONFIG_COLOR_MODE颜色模式变化HDR/宽色域模式切换
CONFIG_KEYBOARD键盘类型变化外接/断开物理键盘
CONFIG_KEYBOARD_HIDDEN键盘可见性变化翻盖键盘打开/关闭
CONFIG_NAVIGATION导航方式变化外接/断开轨迹球、方向键设备
CONFIG_TOUCHSCREEN触摸屏类型变化连接/断开触控设备
CONFIG_UI_MODE(部分)界面模式变化仅限进入/退出桌面模式(UI_MODE_TYPE_DESK)

影响:

维度说明
影响面所有在 Android 17 设备上运行的应用(不限 targetSdk )
触发条件发生上述 6 类配置变更事件时
高频场景大屏设备/ChromeOS 上的键盘接入断开、DeX 桌面模式切换
低频场景手机上很少触发这些配置变更

适配建议:

建议关注(Priority: Medium)

  • 大多数应用:这是正面优化,无需任何操作
  • 需要关注的应用:依赖 Activity 重建来重新加载键盘类型、导航方式、颜色模式、桌面模式相关资源的应用


2.11 短信动态密码保护

说明:

所有在 Android 17 设备上运行的应用(不限 targetSdk),系统会对包含动态密码(OTP)的短信消息延迟 3 小时后才允许应用通过程序化方式(如 READ_SMS 权限读取收件箱)访问,目的是为了防止 OTP 短信被恶意应用劫持。使用 SMS Retriever API 或 SMS User Consent API 的应用不受此延迟限制。

  • 受影响的 API 和访问方式
访问方式是否受延迟影响说明
ContentResolver 查询 content://sms❌ 受影响通过 READ_SMS 权限直接读取收件箱
BroadcastReceiver 监听 SMS_RECEIVED❌ 受影响广播中的 OTP 消息延迟投递
通知访问(NotificationListenerService)❌ 受影响OTP 短信的通知也可能被延迟或脱敏
SMS Retriever API✅ 不受影响Google Play 服务专用通道,立即送达
SMS User Consent API✅ 不受影响用户授权通道,不受延迟限制

影响:

  • 影响矩阵
应用类型影响程度说明
使用 SMS Retriever API 的应用⚪无已走专用通道,不受延迟限制
用 SMS User Consent API 的应用⚪无用户授权通道,不受延迟限制
通过 READ_SMS 读取 OTP 的应用🔴高OTP 短信延迟 3 小时,验证流程将失败
使用 NotificationListenerService 截取 OTP 的应用🔴高可能无法及时获取 OTP 通知
使用 Accessibility Service 读取 OTP 的应用🟡中具体行为取决于实现,可能受限
  • 典型受影响场景

场景一:银行/金融 App 自动填充验证码

现状:部分银行 App 通过 READ_SMS 权限监听短信,自动提取验证码填充。

影响:在 Android 17 上,OTP 短信被延迟 3 小时,自动填充功能失效,用户需手动输入。

建议:迁移到 SMS Retriever API。

场景二:第三方验证码自动填充工具

现状:部分工具类 App(如密码管理器)通过通知监听或短信读取来自动填充验证码。

影响:所有基于 READ_SMS 的自动填充将延迟 3 小时,实质上等于功能不可用。

建议:引导用户使用系统原生自动填充框架。

场景三:企业安全 App 的短信审计

现状:MDM(Mobile Device Management)解决方案可能监控短信以进行安全审计。

影响:OTP 类短信的审计日志将延迟 3 小时。

建议:评估是否真正需要实时审计 OTP 短信。

适配建议:

优先级:高(必须在 Android 17 正式发布前完成)

  • 步骤 1:审计当前 OTP 获取方式
// 检查是否在使用以下方式读取 OTP:
// ❌ ContentResolver 查询短信
getContentResolver().query(Uri.parse("content://sms/inbox"), ...);
// ❌ 广播接收器监听
// <receiver android:name=".SmsReceiver">
//     <intent-filter>
//         <action android:name="android.provider.Telephony.SMS_RECEIVED"/>
//     </intent-filter>
// </receiver>
// ❌ NotificationListenerService 截取
// onNotificationPosted(StatusBarNotification sbn) { ... }
  • 步骤 2:接入 SMS Retriever API(推荐)
// 1. 添加依赖
// implementation 'com.google.android.gms:play-services-auth:21.5.1'
// implementation 'com.google.android.gms:play-services-auth-api-phone:18.3.0'
// 2. 启动 SMS Retriever
SmsRetrieverClient client = SmsRetriever.getClient(context);
Task<Void> task = client.startSmsRetriever();
task.addOnSuccessListener(aVoid -> {
    // 开始监听,等待匹配短信
});
// 3. 注册 BroadcastReceiver
public class MySMSBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (SmsRetriever.SMS_RETRIEVED_ACTION.equals(intent.getAction())) {
            Bundle extras = intent.getExtras();
            Status status = (Status) extras.get(SmsRetriever.EXTRA_STATUS);
            if (status.getStatusCode() == CommonStatusCodes.SUCCESS) {
                String message = extras.getString(SmsRetriever.EXTRA_SMS_MESSAGE);
                // 从 message 中提取验证码
            }
        }
    }
}
// 4. 配合后端发送符合格式的短信:
// <#> 您的验证码是 123456
// FA+9qCX9VSu  (11位 App Hash)
  • 步骤 3:备选方案 – SMS User Consent API
// 适用于无法控制短信格式的场景
SmsRetrieverClient client = SmsRetriever.getClient(context);
client.startSmsUserConsent(null /* 或指定发送方号码 */);
// 系统会弹出对话框让用户确认,用户点击"允许"后
// 通过 onActivityResult 接收短信内容


2.12 Android developer verification

说明:

ADI (Android Developer Verification) 是 Google 为了增强安卓生态信任度、打击恶意仿冒应用而推出的强制性开发者身份验证机制。它要求将现实世界的实体(个人或公司)与应用包名、签名进行强绑定。

  • 核心逻辑:验证通过:允许安装。验证失败/未注册:系统弹出安全警告,拦截安装流程。ADB 豁免:为了不影响开发调试,通过 adb install 安装的应用不受此限制。
  • 强制执行时间:2026 年 Q3 起部分地区正式强制执行。

影响:

  • 海外设备(GMS 环境):高风险。所有搭载 Android 17 及以上系统的海外 GMS 设备必须强制执行(未注册ADI的应用将无法安装)。
  • 国内设备(中国区):无影响。国内版暂不实施该特性。

适配建议

开发者应及时完成合规性注册:

  • 开发者需在 Google Play Console 或相关 Google 平台完成个人或企业的法定身份验证。
  • 确保所有发布的 APK(包括不在 Play 商店上架的 APK)的包名和签名已在验证后的开发者账号下完成备案。
  • 提前准备法人身份证、营业执照、地址证明等文件,海外审核周期可能较长。


三、应用适配 -> targetSdk 37

3.1 本地网络权限保护

说明:

在 Android 16 之前,INTERNET 权限同时涵盖了对公网和局域网(LAN)的访问权。由于局域网内的设备信息(如 MAC 地址、设备列表)常被用于用户指纹追踪和精准定位,Google 决定将这两项访问权进行解耦。

核心变更点:

  • 权限拆分:引入全新的运行时权限 ACCESS_LOCAL_NETWORK。
  • 强制执行时间线:Android 16:选择性加入(Opt-in)阶段。仅针对主动开启兼容性测试的应用生效。Android 17:强制执行。所有 targetSdkVersion ≥ 37 的应用必须适配,否则系统将默认拦截其局域网访问请求。
  • 底层实现与影响范围:该限制在系统网络堆栈底层实现,适用于所有网络 API,包括原始套接字(Raw Sockets)、网络库(如 OkHttp、Cronet)以及所有基于这些库实现的 API。此外,解析具有 .local 后缀的本地服务同样需要此权限。
  • 豁免场景:DNS 流量:若设备的 DNS 服务器位于本地网络,进出该服务器(端口 53)的流量不受此权限限制。系统输出切换器:使用系统原生“输出源切换器”作为设备选择器的应用无需申请此权限。

影响:

如果应用通过以下方式访问局域网,在未获得权限的情况下将面临连接失败:

  • 直接或通过第三方库在本地地址(如 mDNS、SSDP 协议)上使用原始套接字。
  • 使用可访问本地网络的系统框架类(如 NsdManager)。
  • 尝试建立连接或解析本地服务时,系统将直接屏蔽流量,且可能导致相关网络库抛出连接超时或拒绝访问的异常。

适配建议:

针对不同的业务场景,建议选择以下两种路径之一进行适配:

路径 A:使用隐私受保护的选择器(推荐用于偶尔连接)

如果应用仅需偶尔连接特定设备(如投屏或打印),无需请求完整权限,应改用系统提供的 UI 选择器:

  • 媒体流传输:调用系统输出切换器 (Output Switcher)。
  • 常规设备连接 (mDNS):使用 NsdManager 的新标志位 FLAG_SHOW_PICKER。

优势:由系统弹出设备列表供用户选择,应用仅获得所选设备的访问权。此路径无需在清单中声明权限,用户信任度更高。

路径 B:请求运行时权限(适用于持续访问)

对于需要广泛、持续访问本地网络的情形(如智能家居控制、物联网管理),需遵循标准权限流程:

  • 清单声明:添加<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />
  • 动态请求:在访问局域网前,必须调用 Activity.requestPermission() 触发系统提示。
  • 权限组逻辑:ACCESS_LOCAL_NETWORK 属于 “附近设备(NEARBY_DEVICES)” 权限组。若用户已授予该组内的其他权限(如蓝牙权限),系统可能不会再次提示。
  • 容错处理:应用必须以适当方式处理用户拒绝请求或日后在系统设置中撤消权限的情况。


3.2 新的 MessageQueue 无锁实现

说明:

从 Android 17 开始,针对目标平台(targetSdkVersion)为 37 及以上应用,系统将默认启用 android.os.MessageQueue 的全新无锁化实现。

背景与改进:

自 Android 首个版本发布以来,MessageQueue 一直依赖于“单一锁(Single Lock)”机制来管理主线程任务队列。这种设计在多线程高频交互下极易引发锁争用(Lock Contention)——即后台线程在提交任务时可能意外阻塞主线程,导致 UI 掉帧和卡顿。

Android 17 通过重写底层逻辑,实现了无锁化的任务管理,旨在从根本上提升系统性能并显著减少界面丢帧。

影响:

新实现虽然大幅提升了性能,但由于重构了内部结构,会破坏依赖 MessageQueue 私有字段或方法的客户端逻辑:

许多应用为了监控卡顿或分析任务堆栈,会通过反射访问 MessageQueue.mMessages。在 Android 17 中,系统为了兼容性虽保留了该字段,但其返回值将始终为 null。

任何依赖 mMessages 或 mIdleHandlers 等私有成员获取任务详情的逻辑,在 Android 17 上都将面临逻辑异常或空指针崩溃风险。

适配建议:

1. 移除私有字段反射

全面排查代码中对 MessageQueue 私有成员变量(如 mMessages, mIdleHandlers)的反射操作。建议使用官方推荐的性能监控工具(如 Choreographer.FrameCallback 或 OnCommitContentListener)替代。

2. 更新自动化测试工具库

若项目中使用了相关测试框架,请务必升级至兼容新版 MessageQueue 的版本:

Espresso:升级至 3.7.0+。此版本已采用 TestLooperManager API(Android 16 引入)来安全地与 Looper 交互,不再依赖内部实现。

Robolectric:升级至 4.17+。若目前仍在使用 @LooperMode(LEGACY),需尽快迁移至新的 @LooperMode(PAUSED)。


3.3 本地主机保护

说明:

Android 17 引入了一个全新的安装时权限:USE_LOOPBACK_INTERFACE。该权限旨在限制通过本地回环地址(Loopback)进行的跨应用通信(Inter-app Communication)。

  • 涉及地址:包括 127.0.0.1、localhost 以及 IPv6 的 ::1。在 Android 开发中,这些地址常用于本地 HTTP 服务、Socket 通信、代理服务以及 WebView 与本地服务的桥接。
  • 历史行为:在 Android 17 之前,应用只需声明 INTERNET 权限,即可通过 localhost 与本机上的其他应用建立连接,系统不做额外限制。
  • Android 17 变更:系统默认禁止跨应用的本地回环通信。 只有当通信双方(服务端应用与客户端应用)均声明了 USE_LOOPBACK_INTERFACE 权限时,连接才能成功建立。否则,Socket 连接将失败,通常返回错误码 EPERM (Operation not permitted)。

影响:

同一应用内的不同进程间通过 loopback 通信依然受 INTERNET 权限保护,无需额外适配。

跨应用通信受阻:

  • 本地 HTTP 服务:应用 A 开启本地 Server,应用 B 通过 localhost 访问。
  • 代理/拦截类应用:如网络抓包工具、VPN 代理类应用。
  • 混合开发架构:WebView 通过本地桥接服务(Local Bridge)获取宿主应用外其他应用的数据。

适配建议:

1. 针对 TargetSDK ≥ 37 的应用

不论作为客户端还是服务端,应用都必须在AndroidManifest.xml显式声明 USE_LOOPBACK_INTERFACE 权限,否则所有涉及跨应用的 localhost Socket 访问都将失败。

2. 针对 TargetSDK ≤ 36 的应用

不论作为客户端还是服务端,应用都无需改动,声明 INTERNET 即自动获得权限。

跨应用通信规则:双方都必须都有权限,任一方缺失都会导致连接失败。低 targetSdk 只是”自动获得权限”,不是”豁免检查”。


3.4 更安全的本地动态代码加载

说明:

动态代码加载(DCL)是 App 运行时加载外部代码(如 .so/.dex/jar)的行为,本身存在高安全风险:

1. 恶意攻击者可通过「代码注入 / 篡改」替换或修改可写的 .so 文件,植入恶意逻辑(如窃取数据、执行非法操作);

2. Android 14 先管控了 DEX/JAR 文件,Android 17 进一步覆盖原生库(Native Libraries,如 .so 文件),本质是封堵动态加载的安全漏洞,提升 App 安全性。

3. 当 App 目标版本(targetSdkVersion)≥ 37 时,所有通过 System.load() 加载的 .so 文件,必须被标记为只读(read-only);若未标记为只读,系统会直接抛出 UnsatisfiedLinkError 异常,导致原生库加载失败。

影响:

1. 直接受影响的场景

  • App 目标版本设置为 Android 17 及以上;
  • App 中通过 System.load()/System.loadLibrary() 加载 .so 文件(包括自研 .so、第三方 SDK 内置 .so);
  • 加载的 .so 文件未被标记为只读(如 App 运行时修改了 .so 文件权限,或下载的 .so 文件默认是可写的)。

2. 不受影响的场景

  • App 目标版本 < Android 17(系统会兼容,但建议提前适配);
  • 仅加载 App 安装包内自带的 .so 文件(默认被系统标记为只读,无需额外处理);
  • 不使用动态加载原生库的 App(纯 Java/Kotlin 开发,无 .so 依赖)。

适配建议:

1. 核心原则:确保加载的 .so 文件为只读

  • 场景 1:加载安装包内的 .so 文件(最常见)无需额外操作!App 安装时,系统会自动将 lib/ 目录下的 .so 文件标记为只读(权限通常为 -rwxr-xr-x 或 -rw-r–r–);禁止在运行时修改这些 .so 文件的权限(如通过 chmod 改为可写),否则加载时会抛异常。
  • 场景 2:加载外部下载的 .so 文件(高风险,不推荐)如果 App 必须从网络 / 本地存储加载外部 .so 文件(如插件化、热更新场景),需做以下操作:
// 示例:加载外部 .so 文件前,设置为只读权限
fun loadExternalSo(soPath: String) {
    try {
        // 1. 获取文件对象
        val soFile = File(soPath)
        if (!soFile.exists()) {
            throw FileNotFoundException("SO file not found: $soPath")
        }
        // 2. 设置文件为只读(Linux 权限:所有者读/写,其他只读 → 0644)
        // 也可设置为 0755(所有者读/写/执行,其他读/执行),核心是移除可写权限
        val isReadOnly = soFile.setReadable(true, false) // 所有用户可读
        val isWritable = soFile.setWritable(false, false) // 所有用户不可写
        val isExecutable = soFile.setExecutable(true, false) // 所有用户可执行(加载 .so 需执行权限)
        if (!isReadOnly || isWritable) {
            throw SecurityException("Failed to set SO file as read-only")
        }
        // 3. 加载 .so 文件
        System.load(soFile.absolutePath)
    } catch (e: UnsatisfiedLinkError) {
        // 处理加载失败异常
        Log.e("NativeDCL", "Load SO failed: ${e.message}")
    } catch (e: Exception) {
        Log.e("NativeDCL", "SO file operation failed: ${e.message}")
    }
}

2. 推荐方案:尽量避免动态加载原生库

Google 明确建议「尽可能避免动态加载代码」,原因是:

动态加载大幅提升代码注入 / 篡改风险,且易触发系统安全管控;

替代方案:

  • 将 .so 文件打包进 App 安装包,通过 System.loadLibrary() 加载(最安全);
  • 插件化 / 热更新场景,优先使用 Google 官方支持的方案(如 App Bundle 动态功能模块),而非自定义动态加载。


3.5 大屏自适应

说明:

此特性在Android 16.0提出,应用的targetsdk等于36且运行在Android 16设备上生效。此特性主要是对最小宽度大于等于600dp的显示屏上生效,即在大屏设备上,系统会忽略应用对屏幕方向、尺寸可调整性和宽高比限制。因此当应用升级targetsdk到36后,针对大屏设备必须要适配全屏显示和横竖屏显示,系统会忽略应用固定比例显示和固定屏幕方向显示。

不过对于targetSdk 36的应用开发者可以选择停用这些变更,但对于以 Android 17 或更高版本为目标平台的应用,此停用选项将不再可用,会被强制生效。

影响:

忽略屏幕方向、可调整大小性和宽高比限制可能会影响应用在某些设备上的界面,尤其是那些专为锁定为纵向的小布局设计的元素。例如,应用可能会出现布局拉伸、动画和组件超出屏幕等问题。您对宽高比或屏幕方向做出的任何假设都可能会导致应用出现视觉问题。详细了解如何避免这些问题并改进应用的自适应行为。

在横屏可折叠设备上或在多窗口、桌面窗口化或连接的显示屏等场景中进行宽高比计算时,常见的问题是相机预览画面出现拉伸、旋转或裁剪。此问题通常发生在大屏设备和可折叠设备上,因为应用假定相机功能(例如宽高比和传感器方向)与设备功能(例如设备屏幕方向和自然屏幕方向)之间存在固定关系。详细了解如何管理相机预览

允许设备旋转会导致更多 activity 重新创建,如果未正确保留,可能会导致用户状态丢失。如需了解如何正确保存界面状态,请参阅保存界面状态

在大屏设备上,以下清单属性和运行时 API 在全屏或多窗口模式下将不起作用:

同时对于 screenOrientation、setRequestedOrientation() 和 getRequestedOrientation(),系统会忽略以下值:

  • portrait
  • reversePortrait
  • sensorPortrait
  • userPortrait
  • landscape
  • reverseLandscape
  • sensorLandscape
  • userLandscape

另外对于显示是否支持可调整大小来说,android:resizeableActivity=”false”、android:minAspectRatio 和 android:maxAspectRatio 都不会产生任何效果。

在以下情况下,Android 17 的屏幕方向、尺寸调整和宽高比限制不再适用:

  • 游戏(基于 android:appCategory标志)
  • 用户在设备的宽高比设置中明确选择启用应用的默认行为
  • 最小宽度小于 sw600dp 的屏幕

适配建议:

1. 应用升级targetSdk到36+后,扫描代码里面是否有minAspectRatio和maxAspectRati关键字,若有请在折叠屏内屏或平板上全链路点检下页面是否可以正常全屏显示,不会出现布局错乱和操作异常等问题,有异常后建议使用Compose响应式布局方案进行适配。

2. 应用升级targetSdk到36+后,扫描代码里面是否有portrait、landscape关键字,若有请在折叠屏内屏或平板上全链路点检下页面横竖屏切换后是否存在布局显示异常、操作异常和相机方向是否正常等问题。有异常后建议使用Compose响应式布局方案进行适配。

3. 布局优化建议使用Compose响应式布局方案进行适配,以便适配不同的屏幕尺寸和方向。适配可以参考如下链接:https://developer.android.com/develop/ui/compose/layouts/adaptive?hl=zh-cn

4. 针对 targetSdk 36 的过渡期:如某个页面短期内无法完成适配,可通过声明 PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY=”true” 临时停用,但需注意,升级到 targetSdk 37 后该属性将被系统完全忽略,因此应用仍需在此之前完成最终适配。


3.6 Activity安全性增强

说明:

Android 17 进一步强化了后台 Activity 启动(Background Activity Launch, BAL) 的限制,旨在防止恶意应用通过静默启动界面进行钓鱼攻击或界面劫持。

核心变更点:

  • 限制范围从直接启动扩展至 IntentSender。这意味着应用无法再通过间接方式(如通过挂起的 Intent)绕过现有的后台启动限制。
  • 系统正式废弃了传统的 MODE_BACKGROUND_ACTIVITY_START_ALLOWED 模式。
  • 推荐开发者切换至 MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE 等更严谨的策略。该模式仅允许在应用对用户处于可见状态时触发 Activity 启动,从而显著降低用户在无感状态下被劫持的风险。

影响:

后台启动受阻:若应用尝试在 Service、BroadcastReceiver 等后台组件中直接拉起 Activity,该操作将被系统拦截。

旧版标志位失效:如果应用仍在使用已废弃的 MODE_BACKGROUND_ACTIVITY_START_ALLOWED,一旦 targetSdkVersion 升级至 37 或更高,该标志位将不再生效,导致原本预期的后台跳转逻辑失败。

适配建议:

1. 开发者应避免从后台组件直接启动 Activity,建议通过通知或用户触发的方式拉起界面。

2. 对于需要通过代码显式声明启动权限的场景(如 PendingIntent 适配),应将模式更新为更精细的选项:MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE。

3. 全面核查应用中涉及 IntentSender 或 PendingIntent 的逻辑,确保这些间接启动方式符合 Android 17 的安全约束,避免因系统策略收紧导致的功能性中断。


3.7 默认启用证书透明度

说明:

证书透明度(Certificate Transparency, CT)是一种开放的审计框架,通过建立公开的日志系统,记录全球所有由证书颁发机构(CA)签发的数字证书,从而防止伪造或误发的证书被滥用。

  • 在 Android 16 及以前,CT 是一项可选功能,开发者必须通过网络安全配置(Network Security Configuration)主动声明才能开启。
  • 对于所有以 Android 17 或更高版本为目标平台(targetSdkVersion ≥ 37) 的应用,系统将默认开启全量 TLS/SSL 连接的证书透明度校验。如果服务器提供的证书没有包含合法的 SCT(Signed Certificate Timestamp)证明,连接将被系统视为不安全并直接拦截。

影响:

这一变更虽然提升了安全性,但也可能导致以下兼容性问题:

  • 若应用连接的服务器证书不符合 CT 标准(未包含有效的 SCT 信息),系统将抛出 SSLHandshakeException 并终止连接。
  • 企业内部使用的私有证书或自签名证书通常不符合 CT 标准。在 Android 17 设备上,这些原本正常的内网连接可能会默认失效。
  • 开发者在抓包调试时,如果使用的代理工具(如 Charles, Fiddler)生成的伪造证书未经过 CT 校验,可能导致调试流量被系统阻断。

适配建议:

1. 检查服务端生产环境,确保生产环境所使用的 SSL 证书符合 CT 规范(即包含 SCT 信息)。

2. 对于确实无法提供 CT 信息的内部域名或开发测试环境,可以通过 res/xml/network_security_config.xml 配置显式禁用 CT 校验,避免影响业务功能。

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config>
        <!-- 需豁免的域名 -->
        <domain includeSubdomains="true">your-test-domain.com</domain>
        <!-- 关闭该域名的CT校验 -->
        <certificate-transparency overridePins="true">
            <disabled />
        </certificate-transparency>
    </domain-config>
</network-security-config>

在AndroidManifest.xml中引用该配置:

<application
    ...
    android:networkSecurityConfig="@xml/network_security_config">
</application>


3.8 Parcel 数据加固

说明:

Android 17 增强了 Parcel.readValue() 的安全性,引入了严格的大小偏移校验机制。

  • 核心逻辑:当反序列化包含长度前缀的类型(如 VAL_PARCELABLE、VAL_LIST、VAL_MAP 等)时,系统会实时监控字节消费情况。若实际消费的字节数与记录的长度不匹配,系统将通过新增的 failOnParcelSizeMismatch 标志位触发 BadParcelableException。
  • 安全价值:该变更将原本可能被利用的“Parcel 不匹配漏洞”直接转化为可捕获、可感知的显性运行时异常,有效增强了跨进程通信的安全性。

影响:

这一变更主要影响所有涉及 Binder IPC 传递 Parcelable 对象的模块。

  • Android 16 及以前:readValue() 检测到大小不匹配时,仅会静默打印一条 wtfStack 日志,随后继续执行,可能导致逻辑异常或安全隐患。
  • Android 17:该行为被定义为致命错误,系统将直接抛出异常导致进程崩溃。
  • 受影响场景(targetSdk 37+):虽然整体触发频率较低,但对于 writeToParcel 与 createFromParcel 逻辑不一致的自定义实现(尤其是厂商定制的 Parcelable 类)存在极高的稳定性风险,可能导致应用在Android 17系统版本上出现兼容性崩溃。

适配建议:

1. 全面排查三方应用自定义及核心业务中的 Parcelable 实现。确保在所有条件分支下,writeToParcel 写入的数据总量与 createFromParcel(或相应的构造函数)读取的数据量严格保持一致。

2. 在升级 Android 17 之前,建议在现有的 Android 16 测试环境中搜索包含 “Parcel size mismatch” 关键字的 wtf 级别日志。这些在旧版本中被忽略的警告,在 Android 17 中均是潜在的崩溃点。


3.9 静态 final 字段现在不可修改

说明:

在 Android 17 或更高版本上运行且以 Android 17 或更高版本为目标平台的应用无法更改 static final 字段。如果应用尝试使用反射来更改 static final 字段,则会导致 IllegalAccessException。尝试通过 JNI API(例如 SetStaticLongField())修改这些字段中的任何一个都会导致应用崩溃。 

影响:

  • 所有通过反射修改 static final 字段的代码
  • 所有通过 JNI SetStatic*Field() 修改 static final 字段的 native 代码
  • 常见受影响场景:
    • 热修复框架(如 Tinker、Sophix)修改系统类的 static final 常量
    • 插件化框架 hack 系统 static final 字段
    • 单元测试中 mock static final 常量(如 Build.VERSION.SDK_INT)
    • 某些 SDK 通过反射修改配置常量
    • 自研框架中通过反射注入配置值到 static final 字段   

适配建议:

建议尽快排查所有反射修改 static final 的代码路径,尤其是第三方 SDK 和热修复框架,并同步推动三方SDK厂商确认适配计划。

  • // ❌ 旧代码(Android 17 上会崩溃)
fun hackStaticFinalField(clazz: Class<*>, fieldName: String, newValue: Any?) {
    val field = clazz.getDeclaredField(fieldName)
    field.isAccessible = true
    field.set(null, newValue) // Android 17+: IllegalAccessException!
  • // ✅ 适配方案1:改用非 final 的 static 字段(推荐)
class AppConfig {
    companion object {
        // 之前:val BASE_URL = "https://prod.api.com"  // static final
        // 之后:
        @JvmStatic
        var BASE_URL = "https://prod.api.com"  // static 非 final,可修改
    }
}
  • // ✅ 适配方案2:使用 AtomicReference 等线程安全容器
class AppConfig {
    companion object {
        @JvmStatic
        val BASE_URL = AtomicReference("https://prod.api.com")
    }
}
// 修改时:AppConfig.BASE_URL.set("https://debug.api.com")
// 读取时:AppConfig.BASE_URL.get()
  • // ✅ 适配方案3:对于测试场景,使用依赖注入替代反射 hack
interface SdkVersionProvider {
    fun getSdkInt(): Int
}
class RealSdkVersionProvider : SdkVersionProvider {
    override fun getSdkInt() = Build.VERSION.SDK_INT
}
class MockSdkVersionProvider(private val sdkInt: Int) : SdkVersionProvider {
    override fun getSdkInt() = sdkInt
}


3.10 NPU权限声明

说明:

以 Android 17 为目标平台的应用必须声明 FEATURE_NEURAL_PROCESSING_UNIT 硬件功能才能直接访问 NPU。

影响:

未声明 FEATURE_NEURAL_PROCESSING_UNIT 的应用,直接访问 NPU 会触发系统权限拒绝(如抛出 FeatureNotDeclaredException 异常)。

适配说明:

需要「直接访问 NPU 硬件」的应用且以Android 17+为目标平台的应用必须声明 FEATURE_NEURAL_PROCESSING_UNIT权限。


四、新功能和api

4.1 复杂 IME 实体键盘输入的无障碍支持

说明:

Android 17 针对无障碍功能的升级核心聚焦CJKV 语言(中文、日文、韩文、越南文)物理键盘输入场景,通过新增 API 细化文本输入的无障碍事件反馈,解决了此前屏幕阅读器对 CJKV 输入法(IME)复杂输入流程反馈不精准的问题,让视障用户使用物理键盘输入 CJKV 文字时,能获得更贴合输入状态的语音 / 触觉反馈,是安卓无障碍生态对东亚语言场景的针对性优化。

Android 17 正是通过新增 API,AccessibilityEvent  TextAttribute  让系统能精准识别 CJKV 输入的不同阶段,从而让无障碍服务提供精准反馈:

1. 输入法(IME):AppTextAttribute.Builder.setTextSuggestionSelected(boolean)标记当前文本变更是否来自 “候选词选择”(如用户选了 “你好” 这个候选词)

2. 带编辑框的应用: 

  • TextAttribute.isTextSuggestionSelected():读取 IME 标记的 “候选词选择” 状态;
  • AccessibilityEvent.setTextChangeTypes():给无障碍事件标记文本变更类型(如 “输入中”/“已提交”);

3. 无障碍服务(如屏幕阅读器):AccessibilityEvent.getTextChangeTypes()读取事件的文本变更类型,针对性调整反馈策略(如 “已选择候选词:你好”/“已提交文本:你好”)

影响:

  • 生效范围:仅对 targetSdkVersion ≥ Android 17 的应用生效;低版本应用使用标准 TextView/EditText 也会默认开启该能力,但自定义 InputConnection 需手动适配;
  • 非 CJKV 语言无影响:英文、西语等无候选词选择流程的语言,无需适配,API 调用后不会产生负面效果;
  • 兼容性测试:适配后需测试主流无障碍服务(如 TalkBack),确保反馈精准;

适配建议:

1. 输入法(IME)App 适配(核心适配方)

如果你的应用是 CJKV 输入法(如微信输入法、百度输入法),需在处理候选词选择时,通过 TextAttribute 标记选择状态:

// 示例:IME处理候选词选择时,构建TextAttribute并标记选择状态
fun onCandidateSelected(editField: InputConnection, candidateText: String) {
    // 1. 构建TextAttribute,标记“已选择候选词”
    val textAttribute = TextAttribute.Builder()
        .setTextSuggestionSelected(true) // true=选中了候选词;false=未选中(如仅输入拼音)
        .build()
    // 2. 向编辑框设置正在组合的文本,并附带TextAttribute
    val composingText = SpannedString(candidateText).apply {
        setSpan(textAttribute, 0, candidateText.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
    }
    editField.setComposingText(composingText, 1)
}

2. 带编辑框(EditField)的应用适配

  • 场景 1:使用标准 TextView/EditText(无需手动适配)targetSdkVersion ≥ Android 17 的应用,使用系统默认的 TextView/EditText 时,系统会自动处理:自动从 IME 读取 TextAttribute 中的候选词选择状态;自动给 TYPE_VIEW_TEXT_CHANGED 事件设置文本变更类型;开发者无需写额外代码,直接受益。
  • 场景 2:自定义 InputConnection(需手动适配)若应用自定义了 InputConnection(如自定义编辑框、富文本编辑器),需手动读取候选词状态并标记事件类型:
// 示例:自定义InputConnection处理文本变更时,发送精准的无障碍事件
override fun setComposingText(text: CharSequence, newCursorPosition: Int): Boolean {
    val result = super.setComposingText(text, newCursorPosition)
    // 1. 从文本中读取TextAttribute,判断是否选中了候选词
    val textAttribute = TextAttribute.getFirstTextAttribute(text)
    val isCandidateSelected = textAttribute?.isTextSuggestionSelected() ?: false
    // 2. 构建无障碍事件,标记文本变更类型
    val accessibilityEvent = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED)
    accessibilityEvent.textChangeTypes = if (isCandidateSelected) {
        // 标记为“候选词选择导致的文本变更”
        AccessibilityEvent.TEXT_CHANGE_TYPE_COMPOSITION_SELECTION
    } else {
        // 标记为“普通输入导致的文本变更”
        AccessibilityEvent.TEXT_CHANGE_TYPE_COMPOSITION
    }
    // 3. 发送无障碍事件
    sendAccessibilityEventUnchecked(accessibilityEvent)
    return result
}

3. 无障碍服务(如屏幕阅读器)适配

无障碍服务可读取文本变更类型,提供精准的语音反馈:

// 示例:无障碍服务处理文本变更事件
override fun onAccessibilityEvent(event: AccessibilityEvent) {
    if (event.eventType == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) {
        // 1. 获取文本变更类型
        val changeType = event.textChangeTypes
        val text = event.text.joinToString("")
        // 2. 根据类型给出不同反馈
        val feedback = when (changeType) {
            AccessibilityEvent.TEXT_CHANGE_TYPE_COMPOSITION_SELECTION -> "已选择候选词:$text"
            AccessibilityEvent.TEXT_CHANGE_TYPE_COMPOSITION -> "正在输入:$text"
            AccessibilityEvent.TEXT_CHANGE_TYPE_COMMIT -> "已提交文本:$text"
            else -> "文本已更改:$text"
        }
        // 3. 语音播报反馈
        speak(feedback)
    }
}


4.2 新的 ProfilingManager 触发器

说明:

Android 17 为 ProfilingManager 新增了三类核心系统触发器,核心目标是自动化采集应用性能问题的关键数据,无需开发者手动埋点或实时监控,就能精准定位冷启动、内存溢出、CPU 异常等高风险性能问题

新增触发器类型说明:

1. TRIGGER_TYPE_COLD_START: 在应用冷启动时自动触发,它会同时生成堆栈采样 (Stack Sample) 和 系统追踪 (System Trace),帮助分析启动耗时瓶颈。

2. TRIGGER_TYPE_OOM: 当应用抛出 OutOfMemoryError 时自动触发。系统会生成 Java 堆转储 (Heap Dump),这对于事后分析内存泄漏至关重要。

3. TRIGGER_TYPE_KILL_EXCESSIVE_CPU_USAGE: 当应用因 CPU 占用过高被系统强杀时触发,提供堆栈采样,帮助定位高负载代码段。

影响:

在应用监听注册监听,自动化采集应用性能问题的关键数据,协助分析冷启动,内存过高,CPU过高的问题分析,借助Profiler 工具、MAT(内存分析工具)等解析问题原因。

  • 数据获取与分析:触发器输出的原始数据(如堆转储、调用栈)需结合 Android Studio 的 Profiler 工具、MAT(内存分析工具)等解析;
  • 适用场景:主要用于测试 / 调试环境,线上环境建议仅在需要定位特定性能问题时临时开启(避免数据量过大);

适配建议:

对冷启动、内存、CPU要求高的业务场景建议接入。


4.3 JobDebugInfo API

说明:

在 Android 17 中,JobScheduler 的调试能力得到了显著增强。新增的 JobDebugInfo API 解决了开发者长期以来的痛点:“为什么我的任务还不执行?” 以及 “任务卡在某个状态多久了?”。

特别是 getPendingJobReasonStats() 方法,它将“原因”与“耗时”结合,为性能优化提供了量化数据。

在 Android 17 之前,我们只能通过 getPendingJobReasons() 获取当前不满足的约束,或者通过 getPendingJobReasonsHistory() 查看历史原因。

Android 17 的改进点:

1. getPendingJobReasonStats(): 返回一个 Map<Integer, Long>。

2. Key (Integer): 代表挂起的原因(例如 PENDING_JOB_REASON_CONSTRAINT_CHARGING)。

3. Value (Long): 代表该任务因为该原因处于挂起状态的累计毫秒数。

影响:

  • JobDebugInfo APIs 主要新增 getPendingJobReasonStats() 方法,整合了「任务挂起原因 + 累计挂起时长」的关联数据,解决了传统调试的零散性问题;
  • 能精准定位 JobScheduler 任务未执行的原因(如充电 / 网络约束、系统资源),并提供执行时长等关键指标,大幅降低调试成本;

适配建议:

使用JobScheduler 条件触发的业务场景可接入。


五、参考链接

1、Android Developers:https://developer.android.com/about/versions/17/features

阅读全文
下载说明:
1、本站所有资源均从互联网上收集整理而来,仅供学习交流之用,因此不包含技术服务请大家谅解!
2、本站不提供任何实质性的付费和支付资源,所有需要积分下载的资源均为网站运营赞助费用或者线下劳务费用!
3、本站所有资源仅用于学习及研究使用,您必须在下载后的24小时内删除所下载资源,切勿用于商业用途,否则由此引发的法律纠纷及连带责任本站和发布者概不承担!
4、本站站内提供的所有可下载资源,本站保证未做任何负面改动(不包含修复bug和完善功能等正面优化或二次开发),但本站不保证资源的准确性、安全性和完整性,用户下载后自行斟酌,我们以交流学习为目的,并不是所有的源码都100%无错或无bug!如有链接无法下载、失效或广告,请联系客服处理!
5、本站资源除标明原创外均来自网络整理,版权归原作者或本站特约原创作者所有,如侵犯到您的合法权益,请立即告知本站,本站将及时予与删除并致以最深的歉意!
6、如果您也有好的资源或教程,您可以投稿发布,成功分享后有站币奖励和额外收入!
7、如果您喜欢该资源,请支持官方正版资源,以得到更好的正版服务!
8、请您认真阅读上述内容,注册本站用户或下载本站资源即您同意上述内容!
原文链接:https://www.dandroid.cn/archives/22927,转载请注明出处。
0

评论0

显示验证码
没有账号?注册  忘记密码?