1. 获取 Android 17
谷歌发布时间表
Android 17 预计在 2026 年第二季度(26Q2)发布正式版本。目前处于开发阶段,部分特性已通过预览版向开发者开放。
在 Google Pixel 设备上获取 Android 17
开发者持有 Pixel 系列的机器可以直接 OTA 升级,或者下载镜像升级。具体可参考:
设置 Android 模拟器
请参考 设置 Android 模拟器。
设置 Android 17 SDK
请参考 设置 Android 17 SDK。
2.影响所有应用的行为变更
安全
usesCleartextTraffic 弃用预告
Google 官方文档:行为变更:所有应用 - usesClearTraffic 弃用计划
一、特性背景android:usesCleartextTraffic 是 AndroidManifest 中用于控制应用是否允许明文(HTTP)网络流量的属性。Android 平台计划在未来版本中弃用该属性,届时即使设置 usesCleartextTraffic="true",系统也将完全忽略该值,明文流量将被平台网络栈默认拒绝。
替代方案为网络安全配置文件(Network Security Config),它提供更精细的域名级别控制能力,支持仅对特定域名放行的明文流量,是当前官方推荐的配置方式(API 24 起支持)。
二、适用范围
| 维度 | 说明 |
| 正式生效版本 | 预计 Android 18(以谷歌官方文档为准) |
| 受影响应用 | 依赖 usesCleartextTraffic="true" 允许 HTTP 流量、且未配置 Network Security Config 的应用 |
| 不受影响 | 已使用 Network Security Config 文件的应用;targetSdk <= 37 的应用 |
三、特性内容
Android 17 在 ManifestConfigSource.java 中新增了 CompatChange DEPRECATE_USES_CLEARTEXT_TRAFFIC(ID: 415007211,当前 @Disabled)和 aconfig flag deprecate_uses_cleartext_traffic2。当两者同时启用时,系统将强制忽略 usesCleartextTraffic 属性值。
同时,R.attr.usesCleartextTraffic 已在公开 API 中标注为 @Deprecated @FlaggedApi。
四、应用适配
建议现在迁移,避免未来 Android 18(以谷歌官方文档为准) 正式生效时被动修改。
第一步:检查是否受影响
在 AndroidManifest.xml 中搜索:
android:usesCleartextTraffic="true"若存在且未配置 android:networkSecurityConfig,则需要迁移。
第二步:按 minSdkVersion 选择迁移方案
情况一:minSdkVersion < 24(需兼容 Android 7 以下)
同时保留 usesCleartextTraffic="true" 和 Network Security Config,两者并存:
<!-- AndroidManifest.xml -->
<application
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
... ><!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- 仅对需要 HTTP 的域名单独放行 -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">api.example.com</domain>
</domain-config>
<!-- 其他域名默认禁止明文流量 -->
<base-config cleartextTrafficPermitted="false" />
</network-security-config>情况二:minSdkVersion >= 24(仅支持 Android 7 及以上)
直接使用 Network Security Config,无需 usesCleartextTraffic:
<!-- AndroidManifest.xml -->
<application
android:networkSecurityConfig="@xml/network_security_config"
... ><!-- res/xml/network_security_config.xml(同上) -->限制隐式 URI 授权
Google 官方文档:行为变更:所有应用 - 限制隐式 URI 授权
一、特性背景
Android 系统此前对 ACTION_SEND、ACTION_SEND_MULTIPLE、ACTION_IMAGE_CAPTURE 等包含 URI 的 Intent 自动授予读写权限(隐式 URI 授权),存在安全隐患。Android 17 引入限制框架和 StrictMode 检测,Android 18(API 38)将正式废止隐式授权。
⚠️ Android 17 当前行为与 Android 16 完全相同,隐式授权仍然发生。 三个限制 Aconfig 标志默认关闭。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | StrictMode 自动检测:targetSdk > 36;限制本身:所有应用(Aconfig 标志控制) |
| 当前状态 | 限制标志默认关闭,隐式授权仍发生;Android 18 预计正式废止 |
三、特性内容
Android 17 在 Intent.migrateExtraStreamToClipData() 中对三类 Action 添加了条件分支:
ACTION_SEND:自动读取授权受 Aconfig 标志控制,标志开启时不再自动添加FLAG_GRANT_READ_URI_PERMISSIONACTION_SEND_MULTIPLE、ACTION_IMAGE_CAPTURE:同理,读写权限的自动授予受 Aconfig 标志控制
当前所有限制标志默认关闭,隐式授权仍然发生。logcat 中会输出 ERROR 日志提示 "discontinued from Android 18 onwards"。
StrictMode 新增 CompatChange DETECT_IMPLICIT_URI_PERMISSION_GRANT(ID: 460838111),对 targetSdk > 36 的应用自动启用 StrictMode 检测。
四、应用适配
建议现在迁移,避免 Android 18 正式废止时出现 SecurityException。
发送方应用修复(显式添加授权标志)
// ACTION_SEND 修复
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType("image/jpeg");
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // 显式添加
startActivity(Intent.createChooser(shareIntent, "分享"));
// ACTION_IMAGE_CAPTURE 修复
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, outputUri);
cameraIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
startActivityForResult(cameraIntent, REQUEST_IMAGE_CAPTURE);开发阶段检测(targetSdkVersion >= 37,且 strictModeViolationForImplicitUriGrantsEnabled 标志开启时):
if (BuildConfig.DEBUG) {
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectImplicitUriPermissionGrant()
.penaltyLog()
.build());
}Keystore 密钥数量限制
Google 官方文档:行为变更:所有应用 - 按应用密钥库限制
一、特性背景
Android 17 在 keystore2 守护进程中引入按 UID 的密钥数量上限:targetSdk >= 37 的非系统应用上限 50,000 个,其他应用 200,000 个。超出时抛出 KeyStoreException(targetSdk >= 37 错误码为 ERROR_TOO_MANY_KEYS)。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有非系统应用;targetSdk >= 37 上限更低(50,000 vs 200,000),且获得专属错误码 ERROR_TOO_MANY_KEYS |
| 当前状态 | 已生效 |
三、特性内容
密钥数量上限(由 keystore2 守护进程实现):
| 应用类型 | targetSdk | 上限 |
| 非系统应用 | >= 37 | 50,000 个 |
| 非系统应用 | < 37 | 200,000 个 |
| 系统应用 | 任意 | 200,000 个 |
四、应用适配
崩溃/行为特征
java.security.ProviderException: Keystore key generation failed
Caused by: android.security.KeyStoreException: Too many keys (errorCode: 29 或 30)捕获并处理超限异常
try {
KeyGenerator keyGen = KeyGenerator.getInstance("AES", "AndroidKeyStore");
keyGen.init(spec);
SecretKey key = keyGen.generateKey();
} catch (ProviderException e) {
Throwable cause = e.getCause();
if (cause instanceof KeyStoreException) {
KeyStoreException kse = (KeyStoreException) cause;
if (kse.getNumericErrorCode() == KeyStoreException.ERROR_TOO_MANY_KEYS
|| kse.getNumericErrorCode() == KeyStoreException.ERROR_INCORRECT_USAGE) {
// 清理旧密钥后重试
cleanupUnusedKeys();
}
}
}适配建议
- 为密钥设置有效期(
setKeyValidityEnd),并定期清理不再使用的密钥(KeyStore.deleteEntry(alias)) - 避免为每次操作创建独立密钥,改为复用已有密钥
- 升级 targetSdk 至 37 前,确认应用密钥总数不超过 50,000(通过枚举
KeyStore.aliases()检查)
用户体验和系统界面
旋转后恢复 IME 可见性
Google 官方文档:行为变更:所有应用 - 在旋转后恢复默认 IME 可见性
一、特性背景
Android 17 改变了旋转后 IME 恢复逻辑:不再依据 mLastImeShown 标志自动恢复 IME,改为读取新窗口的 requestedVisibleTypes(默认不含 IME)。旋转后 IME 不再自动出现,需应用显式请求。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(由 Aconfig Flag 控制) |
| 当前状态 | 由 Aconfig Flag disable_ime_restore_on_activity_create 控制 |
三、特性内容
Android 17 通过 Aconfig Flag disable_ime_restore_on_activity_create 改变了 IME 恢复判断逻辑:
- Flag 关闭(旧行为):系统检查
ActivityRecord.mLastImeShown,若旋转前 IME 可见则自动恢复 - Flag 开启(新行为):系统改为检查新窗口的
requestedVisibleTypes,Activity 重建后新窗口该字段默认不含 IME,因此 IME 不再自动恢复,需应用显式请求
四、应用适配
受影响场景:应用未处理旋转(Activity 重建),且旋转前 IME 通过用户操作打开(非 stateAlwaysVisible)。Flag 开启后,旋转后 IME 不再自动出现。
不受影响:windowSoftInputMode=stateAlwaysVisible;应用自行处理 configChanges(Activity 不重建);应用已在 onCreate() 中显式请求 IME。
适配方案
方案一:在 Manifest 中设置 stateAlwaysVisible
<activity
android:name=".MyActivity"
android:windowSoftInputMode="stateAlwaysVisible|adjustResize" />方案二:在 onCreate() 中显式请求
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
editText.post(() -> {
editText.requestFocus();
InputMethodManager imm = getSystemService(InputMethodManager.class);
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
});
}方案三:自行处理配置变化(Activity 不重建,IME 状态保持)
<activity
android:name=".MyActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" />人工输入
触控板指针捕获默认传递相对事件
Google 官方文档:行为变更:所有应用 - 在指针捕获期间,触控板默认传递相对事件
一、特性背景
Android 17 将触控板在指针捕获期间的默认行为从 ABSOLUTE(原始绝对坐标)改为 RELATIVE(相对移动,与鼠标一致)。通过两个 Aconfig Flag 控制,不受 targetSdkVersion 影响。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(由 Aconfig Flag 控制) |
| 当前状态 | 双 Flag 控制:pointer_capture_modes + relative_capture_mode_by_default(两个均需开启) |
三、特性内容
Android 17 新增三个指针捕获模式常量(View.java):
| 常量 | 值 | 说明 |
| POINTER_CAPTURE_MODE_UNCAPTURED | 0 | 未捕获 |
| POINTER_CAPTURE_MODE_ABSOLUTE | 1 | 旧行为:绝对坐标 |
| POINTER_CAPTURE_MODE_RELATIVE | 2 | 新默认:相对坐标(鼠标风格) |
新增 requestPointerCapture(int mode) 重载,允许应用显式指定模式。无参 requestPointerCapture() 在两个 Flag 均开启时默认使用 RELATIVE 模式。
四、应用适配
兼容性问题
| 问题 | 原因 |
| 多触点绝对坐标处理失效 | RELATIVE 模式不暴露多触点数据,双指捏合变为 ACTION_SCROLL |
| 事件源判断逻辑失效 | getSource() == SOURCE_TOUCHPAD 不再匹配,RELATIVE 模式返回 SOURCE_MOUSE_RELATIVE |
| getX()/getY() 语义变化 | RELATIVE 模式下返回相对位移量,而非绝对坐标 |
迁移方案
若应用依赖触控板多触点绝对坐标(如自定义手势识别),需显式指定 ABSOLUTE 模式:
// 在 Flag 开启的 Android 17+ 设备上,显式请求 ABSOLUTE 模式(保持旧行为)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.CINNAMON_BUN
&& com.android.hardware.input.Flags.pointerCaptureModes()) {
view.requestPointerCapture(View.POINTER_CAPTURE_MODE_ABSOLUTE);
} else {
view.requestPointerCapture();
}若应用只需要"鼠标风格的移动+滚轮"(如 FPS 游戏),无需修改代码,新默认行为已自动处理触控板。
媒体
后台音频操作强化(Background Audio Hardening)
Google 官方文档:行为变更:所有应用 - 后台音频强化 | 详细指南
一、特性背景
Android 平台要求音频操作只能由前台应用或持有 WIU(While-In-Use)能力的前台服务执行。Android 16 仅记录日志,Android 17 通过 aconfig flag 正式开启强制阻断模式:后台调用音量 API 静默失败,后台请求焦点返回 AUDIOFOCUS_REQUEST_FAILED,后台播放被系统静默。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(由 Aconfig Flag 控制) |
| 当前状态 | 由多个 aconfig flag 控制:hardening_strict(核心 flag)、hardening_partial_volume(音量方法)、ro_foreground_audio_control(FGS WIU 能力) |
三、特性内容
Android 17 将音频强化从 Android 16 的 warning 模式升级为 strict 强制模式,在 aconfig flag 开启后:
- 音量方法(
adjustStreamVolume、setStreamVolume等):后台调用时静默 no-op,直接 return,不抛异常 - 音频焦点(
requestAudioFocus):后台调用时返回AUDIOFOCUS_REQUEST_FAILED(0) - 音频播放(
AudioTrack.write()/ AAudio / OpenSL ES):后台播放被系统静默(MUTED_BY_OP_CONTROL_AUDIO)
核心通过三个 AppOps 实现:OP_CONTROL_AUDIO、OP_CONTROL_AUDIO_PARTIAL、OP_TAKE_AUDIO_FOCUS,结合 PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL 进程能力判断应用是否有权执行音频操作。
四、应用适配
受影响 API
后台调用以下 API 时静默失败(no-op,无异常):adjustStreamVolume、setStreamVolume、setStreamMute、adjustVolume、adjustSuggestedStreamVolume、setRingerMode。requestAudioFocus 返回 AUDIOFOCUS_REQUEST_FAILED(0)。AudioTrack.write() / AAudio / OpenSL ES 后台播放被系统静默。
豁免情况:应用有可见 Activity(前台)、应用可见时启动的非 SHORT_SERVICE 类型 FGS、system-server 委托启动的 FGS(如 Telecom)、持有 MODIFY_AUDIO_SETTINGS_PRIVILEGED / MODIFY_AUDIO_ROUTING / MODIFY_PHONE_STATE 权限。⚠️ 不豁免:从后台启动的 FGS(BFSL,如 BOOT_COMPLETE 触发)。
WIU 能力关键:FGS 获得 WIU 的关键在于启动时应用是否可见。从后台启动的 FGS(如 BOOT_COMPLETE)无法获得 WIU。VOIP 应用通常已通过 Telecom Jetpack API 满足要求,不太可能受影响。
修复方案
方案一(推荐):在应用可见时启动 mediaPlayback 前台服务,再执行音频操作。短暂故障时保持 FGS 有效(< 10 分钟),收到永久性结束信号时才停止 FGS。
方案二:确保在前台 Activity 中调用音频 API,避免在 onStop() 后触发。
方案三:处理静默失败(检查 requestAudioFocus 返回值是否为 AUDIOFOCUS_REQUEST_FAILED)。
logcat 识别关键词:AudioHardening volume control ... ignored、AudioHardening focus request ... ignored、AudioHardening background playback muted
本地验证命令:
adb shell cmd audio set-enable-hardening 1 # 启用
adb shell cmd audio set-enable-hardening 0 # 停用连接
蓝牙绑定丢失的自主重新配对
Google 官方文档:行为变更:所有应用 - 针对蓝牙绑定丢失的自主重新配对
一、特性背景
Android 16 及之前,蓝牙设备丢失配对信息时每次都广播 ACTION_KEY_MISSING。Android 17 引入自主重新配对机制:系统可在后台静默恢复 bond key,成功时不再广播。同时 ACTION_KEY_MISSING 升级为受保护广播。
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(由 Aconfig Flag 控制) |
| 当前状态 | 由 autonomous_repairing_initiation + bluetooth_pairing_hardening 两个 flag 控制;ACTION_KEY_MISSING 受保护广播已无条件生效 |
三、特性内容
Android 17 的蓝牙自主重新配对特性包含以下核心变化:
- 自主重配对机制:系统可在后台静默完成 bond key 恢复,成功时不再广播
ACTION_KEY_MISSING,仅在自主重配对失败时才广播 - 受保护广播:
ACTION_KEY_MISSING升级为<protected-broadcast>,第三方应用无法再伪造该广播 - Settings UI:key missing 时连接摘要优先显示 "Can't connect"
四、应用适配ACTION_KEY_MISSING 广播接收行为变化
| 场景 | Android 16 | Android 17(flag 开启后) |
| 系统自主重配对成功 | 广播发送 | 不发送广播 |
| 系统自主重配对失败 | 广播发送 | 广播发送 |
| 第三方应用发送此广播 | 允许 | 被系统拦截(受保护广播) |
适配建议
- 不可单独依赖
ACTION_KEY_MISSING;同时监听ACTION_BOND_STATE_CHANGED(BOND_NONE)感知 bond 丢失 - 监听
ACTION_PAIRING_REQUEST时,检查EXTRA_PAIRING_CONTEXT区分自主重配对与手动配对
// 推荐:同时监听 bond 状态
IntentFilter filter = new IntentFilter();
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
filter.addAction(BluetoothDevice.ACTION_KEY_MISSING);// 仍可监听,但不再保证每次 key missing 都收到
registerReceiver(receiver, filter);
// 在 BroadcastReceiver 中
int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1);
if (bondState == BluetoothDevice.BOND_NONE) {
// bond 完全解除,可提示用户重新配对
}核心功能
已回收 Parcel 对象的访问限制
一、特性背景Parcel 通过对象池(obtain() / recycle())复用。Android 16 中 recycle() 后继续读取仅打印警告。Android 17 新增 assertNotRecycled() 检测,检测到 use-after-recycle 时直接抛出 BadParcelableException。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(无 CompatChange,无 targetSdk 门槛) |
| 当前状态 | 已生效,当前仅在 readParcelableInternal() 入口处调用 |
三、特性内容
Android 17 在 Parcel.java 中新增 assertNotRecycled() 检测,目前被插入 readParcelableInternal() 入口处。当检测到 Parcel 回收后继续被读取时,直接抛出 BadParcelableException(Android 16 及之前仅打印 Log.wtf 警告,不抛异常)。
注意:当前仅 readParcelable() 路径触发此检查;后续 Android 版本将逐步扩展到更多读写方法。
四、应用适配
如何判断是否受影响:搜索 recycle() 后仍对同一 Parcel 调用读取方法的用法,或多线程未同步复用 Parcel 的场景。
崩溃/异常特征
android.os.BadParcelableException: Parcel used while recycled. ...
at android.os.Parcel.errorUsedWhileRecycling(Parcel.java:656)
at android.os.Parcel.assertNotRecycled(Parcel.java:662)
at android.os.Parcel.readParcelableInternal(Parcel.java:5261)
at android.os.Parcel.readParcelable(Parcel.java:...)
at com.example.app.MyClass.someMethod(MyClass.java:...)修复/迁移方案
典型错误模式及修复:
// 错误:recycle() 后继续读取
Parcel parcel = Parcel.obtain();
parcel.setDataPosition(0);
SomeData data = parcel.readParcelable(SomeData.class.getClassLoader());
parcel.recycle();
// 错误:以下调用将触发 BadParcelableException(Android 17)
SomeData data2 = parcel.readParcelable(SomeData.class.getClassLoader());
// 修复:recycle() 前完成所有读取操作,recycle() 后不再访问该对象
Parcel parcel = Parcel.obtain();
parcel.setDataPosition(0);
SomeData data = parcel.readParcelable(SomeData.class.getClassLoader());
SomeData data2 = parcel.readParcelable(SomeData.class.getClassLoader());
// 在 recycle 前完成 parcel.recycle();
// parcel 不再使用多线程场景下需自行记录 recycle 状态(isRecycled() 非公开 API),确保 recycle() 后不再访问该对象。
Parcel 序列化大小校验(failOnParcelSizeMismatch)
一、特性背景Parcel.writeValue() 序列化 Parcelable 等对象时写入 length prefix,readValue() 反序列化后校验字节数是否一致。若 writeToParcel() 与 createFromParcel() 字段不对称,会产生大小不一致。Android 16 仅打印警告,Android 17 通过 aconfig flag fail_on_parcel_size_mismatch 控制:flag 开启后直接抛出 BadParcelableException。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(由 aconfig flag 全局控制) |
| 当前状态 | 已生效(flag fail_on_parcel_size_mismatch 在所有发布配置中均为 ENABLED) |
三、特性内容Parcel.writeValue() 在序列化"带长度前缀"类型(Parcelable、Map、List、SparseArray、Serializable 等)时,会在对象内容之前写入字节数作为 length prefix。readValue() 在反序列化后比较实际消耗字节数与 length prefix 是否一致。
Android 17 通过 aconfig flag fail_on_parcel_size_mismatch 控制不一致时的行为:
- Flag 开启(Android 17 默认):抛出
BadParcelableException - Flag 关闭:仅记录
Slog.wtfStack日志,继续执行(Android 16 行为)
四、应用适配
如何判断是否受影响
搜索所有 Parcelable 自定义类,检查 writeToParcel() 与 createFromParcel() 是否严格对称(字段数量和顺序完全一致)。常见错误:遗漏读取、条件分支不对称、版本迭代添加新字段忘记对应读取。
崩溃特征
android.os.BadParcelableException: Unparcelling of <YourClass> of type PARCELABLE
consumed <actual> bytes, but <expected> expected. [throwing]
at android.os.Parcel.readValue(Parcel.java:4761)
at android.os.Parcel.readValue(Parcel.java:4736)
at android.os.BaseBundle.getValue(BaseBundle.java:...)
at android.os.Bundle.getParcelable(Bundle.java:...)
at com.example.app.YourClass$CREATOR.createFromParcel(YourClass.java:...)触发路径:通过 Bundle.getParcelable()、Parcel.readValue() 等方法反序列化自定义 Parcelable 时,若检测到字节数不一致则抛出该异常。
修复方案
确保 writeToParcel() 与 createFromParcel() 严格对称:
// 错误示例:writeToParcel 写入了额外字段,createFromParcel 未对应读取
public class MyData implements Parcelable {
public int value;
public String name;
public boolean flag; // 新增字段
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(value);
out.writeString(name);
out.writeBoolean(flag); // 写入了 flag }
protected MyData(Parcel in) {
value = in.readInt();
name = in.readString();
// 错误:遗漏了 in.readBoolean(),导致字节数不一致
}
}
// 修复:createFromParcel 必须与 writeToParcel 完全对称
protected MyData(Parcel in) {
value = in.readInt();
name = in.readString();
flag = in.readBoolean();
// 补充读取,与写入对称
}如果需要兼容旧版本序列化数据,建议通过版本号字段管理序列化格式(在 writeToParcel 头部写入版本号,createFromParcel 按版本号读取对应字段)。
相关配置变更不再重启 Activity
一、特性背景
Android 17 引入 SKIP_ACTIVITY_RECREATION_ON_CONFIG_CHANGE:当键盘、导航、触控屏、色彩模式等"低优先级"配置变更发生时,系统默认不再重启 Activity,转而调用 onConfigurationChanged() 回调。对所有应用生效。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(@Overridable) |
| 当前状态 | flag 开启后对所有应用生效 |
三、特性内容
受影响的配置变更类型:
| Manifest 声明值 | 说明 |
| keyboard | 键盘类型变化 |
| keyboardHidden | 软键盘弹出/收起 |
| navigation | 导航方式变化 |
| touchscreen | 触控屏配置变化 |
| colorMode | 色彩模式变化(HDR 等) |
| UI_MODE | 仅限进入/离开 UI_MODE_TYPE_DESK 时 |
新增 manifest 属性 android:recreateOnConfigChanges,允许应用逐 Activity 声明哪些配置变更仍需重建:
<!-- 支持的标志值:touchscreen | keyboard | keyboardHidden | navigation | colorMode -->
<activity
android:name=".YourActivity"
android:recreateOnConfigChanges="keyboard|keyboardHidden|navigation|touchscreen|colorMode" />四、应用适配
受影响场景
若应用依赖 Activity 重建来重新加载这些配置对应的资源(例如在 onCreate 中根据键盘状态初始化布局,但未实现 onConfigurationChanged),则在 flag 开启后,配置变更不再触发重建,布局可能无法正确响应。
方法一(推荐):实现 onConfigurationChanged() 主动处理
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 主动响应 keyboard / keyboardHidden / navigation / touchscreen / colorMode 变化
updateLayoutForConfig(newConfig);
}方法二:通过 manifest 属性恢复特定配置变更时的重建行为
<activity
android:name=".YourActivity"
android:recreateOnConfigChanges="keyboard|keyboardHidden|navigation|touchscreen|colorMode" />android:recreateOnConfigChanges 是 Android 17 新增属性,在旧版本设备上系统会忽略该未知属性,无副作用。
文件操作模式严格校验
一、特性背景ParcelFileDescriptor.parseMode(String) 用于将文件打开模式转换为 POSIX 标志位。旧实现采用逐字符扫描,对未文档化的模式(如 rwa、ra、rt)会静默接受。Android 17 改为显式枚举 switch 语句,不在白名单中的模式直接抛出 IllegalArgumentException。对所有应用生效,无 CompatChange 保护。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(无 CompatChange 保护) |
| 当前状态 | 已无条件生效 |
三、特性内容
Android 17 将 FileUtils.translateModeStringToPosix() 从逐字符扫描改为显式枚举的 switch 语句,任何不在白名单中的模式均直接抛出 IllegalArgumentException。
合法模式白名单(Android 17)
| 模式 | 等价 POSIX 标志 | 说明 |
| r | O_RDONLY | 只读 |
| w | O_WRONLY | O_CREAT | 只写,不存在则创建 |
| wt 或 tw | O_WRONLY | O_CREAT | O_TRUNC | 只写,截断 |
| wa 或 aw | O_WRONLY | O_CREAT | O_APPEND | 只写,追加 |
| rw 或 wr | O_RDWR | O_CREAT | 读写 |
| rwt/rtw/wrt/wtr/trw/twr | O_RDWR | O_CREAT | O_TRUNC | 读写,截断 |
四、应用适配
判断是否受影响
搜索代码中 parseMode()、openFileDescriptor(uri, mode) 等调用,重点检查 "rwa"、"ra"、"rt" 等矛盾模式(任何包含 a 但基础模式为 r 的组合均为非法)。
崩溃特征
java.lang.IllegalArgumentException: Bad mode: rwa
at android.os.FileUtils.translateModeStringToPosix(FileUtils.java:1552)
at android.os.ParcelFileDescriptor.parseMode(ParcelFileDescriptor.java:679)修复方案
将非法/矛盾模式替换为文档明确支持的合法模式:
// 错误:rwa 在 Android 17 中抛异常
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file,
ParcelFileDescriptor.parseMode("rwa"));
// 修复:根据实际需求选择合法模式
// 若需要读写 + 追加,使用 "rw" 打开后通过 seek 到末尾实现
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file,
ParcelFileDescriptor.parseMode("rw"));
// 或者,若仅需追加写入,使用 "wa"
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(file,
ParcelFileDescriptor.parseMode("wa"));ContentResolver.openFileDescriptor(uri, mode) 同理,将 "rwa" 替换为 "rw" 或 "wa"。
线程优先级越界强制校验
一、特性背景Process.setThreadPriority() 接受 Linux nice 值(-20 到 19)。Android 16 及之前越界参数由内核静默 clamp。Android 17 改为 Java 层校验,传入越界值直接抛出 IllegalArgumentException。对所有应用生效。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(无 CompatChange 保护) |
| 当前状态 | 已生效 |
三、特性内容
Android 17 将 Process.setThreadPriority() 的两个重载从 native 方法改写为 Java 方法,在调用底层实现之前先进行参数校验:传入超出 [-20, 19] 范围的值将直接抛出 IllegalArgumentException(Android 16 及之前由内核静默 clamp 到合法范围)。
合法范围:-20(最高优先级)到 19(THREAD_PRIORITY_LOWEST,最低优先级)。
四、应用适配
如何判断是否受影响
搜索 setThreadPriority 调用,检查参数是否可能超出 [-20, 19]。重点排查:硬编码越界值、外部来源未校验的值、对 THREAD_PRIORITY_LOWEST(19)做算术运算(如 +1 = 20)。
修复/迁移方案
在调用前将 priority 值 clamp 到合法范围,或直接使用 Process 类预定义的常量:
// 方案一:调用前主动 clamp,复现 Android 16 的静默截断行为
int safePriority = Math.max(-20, Math.min(Process.THREAD_PRIORITY_LOWEST, priority));
Process.setThreadPriority(safePriority);
// 方案二:使用预定义常量,避免硬编码越界数值 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // = 10
Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); // = 0
Process.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND); // = -2
Process.setThreadPriority(Process.THREAD_PRIORITY_URGENT_AUDIO); // = -19崩溃/异常特征
java.lang.IllegalArgumentException: Priority/niceness 20 is invalid
at android.os.Process.setThreadPriority(Process.java:1388)
at com.example.app.MyWorkerThread.run(MyWorkerThread.java:xx)3.影响 targetSdkVersion >= 37 应用的变更
核心功能
MessageQueue 新无锁实现
Google 官方文档:行为变更:targetSdk 37+ - MessageQueue 的新无锁实现 | 详细指南
一、特性背景
Android 17 引入全新的 DeliQueue 无锁消息队列实现,替代旧的 synchronized 有序链表,通过 CAS 原子操作消除锁竞争。由 CompatChange USE_NEW_MESSAGEQUEUE(ID: 421623328)控制,targetSdk >= 37 自动激活。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37 |
| 当前状态 | USE_NEW_MESSAGEQUEUE(ID: 421623328) |
三、特性内容
最关键的破坏性变更:当 DeliQueue 路径激活时(targetSdk >= 37),mMessages 字段恒为 null,消息实际存储在新的 MessageStack(Treiber Stack)中。mMessages 新增 @UnsupportedAppUsage(maxTargetSdk = BAKLAVA) 限制。
四、应用适配
最高风险:通过反射访问 mMessages 字段
// 常见于 APM SDK、性能监控框架
Field f = MessageQueue.class.getDeclaredField("mMessages");
f.setAccessible(true); Message msg = (Message) f.get(looper.getQueue());
// ★ Android 17(targetSdk >= 37):msg 永远为 null!消息在 mStack 中其他受影响的私有 API
| 私有 API | Android 17 变化 |
| mMessages 字段 | DeliQueue 路径下恒为 null |
| next() 方法 | 新增 maxTargetSdk = BAKLAVA 废弃声明 |
| synchronized(this) | 不再保护 DeliQueue 内部状态 |
| 消息对象复用 | 禁用对象池(防止 CAS ABA 问题),Message.obtain() 直接 new Message() |
推荐替代方案
| 废弃用途 | 官方替代 |
| 测试中操作消息队列(反射 mMessages) | TestLooperManager(Android 9+) |
| 监控消息派发(hook next()) | Looper.setMessageLogging() 或 Handler.Callback |
| 检查消息是否存在(遍历 mMessages) | Handler.hasMessages() |
测试框架升级要求
- Espresso 需升级到 3.7.0+(该版本使用 Android 16 引入的新
TestLooperManagerAPI 与 Looper 安全交互) - Robolectric 需升级到 4.17+ 并迁移到
@LooperMode(PAUSED)
本地验证命令:
adb shell am compat enable USE_NEW_MESSAGEQUEUE <package>
adb shell am compat disable USE_NEW_MESSAGEQUEUE <package>static final 字段不可修改
Google 官方文档:行为变更:targetSdk 37+ - 静态 final 字段现在不可修改
一、特性背景
Android 17 ART 强制禁止运行时修改 static final 字段(targetSdk >= 37)。反射修改抛 IllegalAccessException;JNI 修改导致进程崩溃。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37 |
| 当前状态 | 已生效 |
三、特性内容
ART 运行时在 ArtField::IsUnmodifiable() 中根据 targetSdkVersion 决定是否允许修改 static final 字段。以 Google 官方文档为准:targetSdk >= 37 的应用已受限制。
两条路径的行为差异:
- 反射路径(
Field.set()):抛出IllegalAccessException,可捕获 - JNI 路径(
SetStaticXxxField()):直接LOG(FATAL),进程崩溃,无 Java 异常可捕获
四、应用适配
崩溃/异常特征
反射路径:
java.lang.IllegalAccessException: Cannot set static final int field com.example.Config.MAX_RETRY
at java.lang.reflect.Field.set(Field.java:...)
at com.example.app.SomeClass.init(SomeClass.java:xx)JNI 路径(无 Java 异常,进程直接终止):
A/art: art/runtime/jni/jni_internal.cc:1651] Cannot set static final int field com.example.Config.MAX_RETRY
A/art: runtime aborting...如何判断是否受影响:搜索通过反射 setAccessible(true) 或 JNI SetStaticXxxField 修改 static final 字段的用法(常见于配置覆盖、Mockito/PowerMock 测试 Mock)。
修复方案
- 移除对
static final字段的运行时修改:改为使用非 final 字段、配置文件或依赖注入 - 测试框架:升级 Mockito(4.x+ 默认不修改 final 字段)、移除 PowerMock 依赖
- 若必须兼容:将 targetSdk 保持在 36 以下可暂时回避,但不建议长期依赖
通知自定义视图大小限制
一、特性背景
Android 17 对 targetSdk >= 37 的应用加强自定义通知视图内存限制:新增 SystemUI 侧 lightenPayload() 机制,超限时将自定义视图降级为标准通知模板(而非直接丢弃)。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37 |
| 当前状态 | 已定义,flag + CompatChange 双重门控 |
三、特性内容
分为两个层级的限制:
NMS 层(发送时):与 Android 16 相同——inflate 后 View 内存 > 2MB 警告,>= 5MB 移除 custom view。
SystemUI 层(显示时,Android 17 新增):CompatChange CHECK_SIZE_OF_INFLATED_CUSTOM_VIEWS(ID: 270553691),当 flag 开启且 targetSdk >= 37 时,若自定义 RemoteViews 超过阈值,lightenPayload() 将自定义视图降级为标准通知模板(标题/文本保留,自定义布局丢失)。
受影响的 Notification.Builder 方法:setCustomContentView()、setCustomBigContentView()、setCustomHeadsUpContentView()。
四、应用适配
受影响场景
- 自定义通知布局中包含从网络加载的高分辨率 Bitmap(封面图、用户头像等)
- 自定义通知 layout 嵌套了复杂视图层级,inflate 后超过 2MB
检测方式
# 观察 NMS 层警告日志
adb logcat | grep -i "custom.*view\|inflat.*notif"适配建议
- 图片降采样:设置 Bitmap 前先降采样,通知图片建议不超过 1024×1024px
- 使用
DecoratedCustomViewStyle:用Notification.Builder.setStyle(new DecoratedCustomViewStyle())包装,系统会进行额外内存管理 - 简化视图层级:减少 RemoteViews layout 嵌套深度
- 设置 fallback 文本:确保
setContentTitle()和setContentText()有有意义的文本,即使自定义视图被降级,通知内容依然可读
NPU 直接访问需声明硬件功能
一、特性背景
Android 17 引入 NpuManager 统一调度 NPU 资源。targetSdk >= 37 的应用若未声明 android.hardware.npu 硬件特性,NPU 直接访问将被拒绝。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37,且设备搭载 NPU 硬件 |
| 当前状态 | npumanager_block_missing_feature flag 已 ENABLED |
三、特性内容NpuManager 在调度应用时,检查 PackageInfo.reqFeatures 列表中是否声明了 android.hardware.npu。对 targetSdk >= 37 且未声明该 feature 的应用,flag 开启后将 hasDirectAccess 设为 false,NPU 硬件调度器拒绝其直接推理请求。应用更新 Manifest 声明后,系统会动态解除阻断。
四、应用适配
受影响场景:直接使用 NNAPI、TFLite、ML Kit 底层 NPU 路径的应用,targetSdk 升级到 37 后未声明 feature。无 NPU 硬件的设备不受影响。
失败表现:推理不抛异常,静默回退 CPU 执行(性能下降)。logcat:NPU access is blocked.
修复方法(一行 Manifest 声明)
<!-- 推荐 required="false",确保能安装到无 NPU 的设备 -->
<uses-feature android:name="android.hardware.npu" android:required="false"/>使用 required="false" 而非 required="true",以保证应用能安装在无 NPU 的设备上,同时在有 NPU 的 Android 17+ 设备上获得直接访问权限。
无障碍
复杂 IME 实体键盘输入的无障碍支持
Google 官方文档:行为变更:targetSdk 37+ - 复杂 IME 实体键盘输入的无障碍支持
一、特性背景
CJKV 语言 IME 输入经历多个阶段,屏幕阅读器无法区分,导致中间状态频繁播报。Android 17 新增 AccessibilityEvent 文本变更类型 API 和 TextAttribute.isTextSuggestionSelected(),让屏幕阅读器在撰写阶段静默、提交时才播报。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 所有应用(可选增强,不破坏现有行为) |
| 当前状态 | 由 Aconfig Flag 控制 |
三、特性内容AccessibilityEvent 新增文本变更类型常量:
| 常量 | 说明 |
| TEXT_CHANGE_TYPE_UNDEFINED | 默认/未定义 |
| TEXT_CHANGE_TYPE_IN_COMPOSITION | IME 撰写进行中(中间状态,未提交) |
| TEXT_CHANGE_TYPE_COMMITTED_BY_IME | IME 提交操作触发(文本已定稿) |
| TEXT_CHANGE_TYPE_CONVERSION_SUGGESTION_SELECTED_BY_IME | IME 候选词选中操作触发 |
TextAttribute 新增 isTextSuggestionSelected() 方法和 Builder.setTextSuggestionSelected() setter,用于传递候选词选择状态。
四、应用适配
本次变更属于可选增强,不破坏现有行为,Flag 未启用时行为与 Android 16 完全一致。
CJKV IME 应用适配建议:在调用 InputConnection.setComposingText() 时,通过 TextAttribute 传递候选词选择状态:
if (Flags.a11yTextChangeTypesApi()) {
TextAttribute textAttr = new TextAttribute.Builder()
.setTextConversionSuggestions(suggestions)
.setTextSuggestionSelected(true) // 候选词选择中
.build();
inputConnection.setComposingText(composingText, 1, textAttr);
}无障碍服务适配建议:根据事件类型调整播报策略:
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
&& Flags.a11yTextChangeTypesApi()) {
int types = event.getTextChangeTypes();
if ((types & AccessibilityEvent.TEXT_CHANGE_TYPE_IN_COMPOSITION) != 0) {
// 撰写中,可跳过或延迟朗读
} else if ((types & AccessibilityEvent.TEXT_CHANGE_TYPE_COMMITTED_BY_IME) != 0) {
// IME 提交,触发最终朗读
}
}隐私权
机会性启用 ECH(加密客户端 Hello)
Google 官方文档:行为变更:targetSdk 37+ - 机会性地启用 ECH
一、特性背景
ECH(Encrypted Client Hello)通过加密 TLS 握手中的 SNI 增强隐私保护。Android 17 对 targetSdk >= 37 的应用默认以 Opportunistic 模式启用 ECH(服务器支持时自动使用,不支持时正常握手),新增 <domainEncryption> XML 元素允许细粒度控制。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37 时默认启用 OPPORTUNISTIC 模式 |
| 当前状态 | 双重门控:CompatChange + Aconfig Flag 同时满足才启用 |
三、特性内容
ECH 通过双重门控启用:CompatChange ENABLE_DEFAULT_ENCRYPTED_CLIENT_HELLO(targetSdk >= 37)+ Aconfig Flag 同时满足时,默认以 OPPORTUNISTIC 模式启用。
新增 Network Security Config XML 元素 <domainEncryption>,可在 <base-config> 或 <domain-config> 中使用:
| mode | 说明 |
| disabled | 禁用 ECH |
| opportunistic | 服务器支持时启用 ECH,否则正常握手(targetSdk >= 37 默认值) |
| enabled | 服务器支持时 ECH,不支持时使用 GREASE |
四、应用适配
默认行为(无需修改代码):targetSdk >= 37 的应用在 Aconfig 标志开启的设备上,ECH 自动以 OPPORTUNISTIC 模式启用,应用无需任何改动即可受益。
如需禁用 ECH(如企业网络代理不兼容 ECH 加密 SNI):
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<domainEncryption mode="disabled" />
</base-config>
</network-security-config>高风险场景:企业防火墙、DPI 设备依赖 TLS SNI 进行流量管控,ECH 加密 SNI 后可能导致连接被拒或降级。受影响时在 Network Security Config 中为相关域名设置 mode="disabled"。
访问本地网络需要权限
Google 官方文档:行为变更:targetSdk 37+ - 以 Android 17 为目标平台的应用需要本地网络权限
一、特性背景
Android 17 引入 ACCESS_LOCAL_NETWORK 运行时危险权限(NEARBY_DEVICES 权限组),通过内核 BPF 强制执行。targetSdk < 37 通过 split-permission 自动兼容;targetSdk >= 37 必须显式声明并请求。
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37 需显式声明和请求;targetSdk < 37 自动兼容 |
| 当前状态 | 已生效 |
三、特性内容
ACCESS_LOCAL_NETWORK权限定义为dangerous级别,属于NEARBY_DEVICES权限组- 向后兼容:targetSdk < 37 的应用通过
split-permission机制(INTERNET → ACCESS_LOCAL_NETWORK)自动获得该权限 - BPF 强制执行:权限通过内核 BPF 层面强制,未授权时网络包被静默丢弃
四、应用适配
需要权限的典型场景(targetSdk >= 37):LAN 直连 Socket、mDNS/DNS-SD 服务发现、Wi-Fi Direct、本地 HTTP 请求、UDP 广播/多播、智能家居设备控制。
适配步骤
第一步:在 AndroidManifest.xml 中声明:
<uses-permission android:name="android.permission.ACCESS_LOCAL_NETWORK" />第二步:运行时请求(与其他危险权限一致):
if (checkSelfPermission(Manifest.permission.ACCESS_LOCAL_NETWORK)
!= PackageManager.PERMISSION_GRANTED) {
requestPermissions(
new String[]{Manifest.permission.ACCESS_LOCAL_NETWORK},
REQUEST_CODE_LOCAL_NETWORK);
}失败特征:Socket 连接超时或 Operation not permitted;BPF 层静默丢包(无 Java 异常);mDNS 发现无设备。
实体设备隐藏密码
Google 官方文档:行为变更:targetSdk 37+ - 在实体设备上隐藏密码
一、特性背景
Android 17 将密码可见性拆分为触屏和实体键盘两个独立设置。对 targetSdk >= 37 的应用,实体键盘输入密码时默认不再显示最后输入字符(TEXT_SHOW_PASSWORD_PHYSICAL 默认 0)。触屏行为不变。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37 |
三、特性内容
Android 17 新增 ShowSecretsSetting 类,将密码可见性拆分为两个独立设置:
| 设置 | 默认值 | 说明 |
| Settings.Secure.TEXT_SHOW_PASSWORD_TOUCH | 1(显示) | 触屏/软键盘输入,与旧版行为一致 |
| Settings.Secure.TEXT_SHOW_PASSWORD_PHYSICAL | 0(隐藏) | 实体键盘输入,这是行为变化 |
四、应用适配
行为变化:targetSdk >= 37 的应用,用户通过外接键盘(蓝牙键盘、USB 键盘等)在密码字段输入时,所有字符立即显示为 •,最后输入字符不再短暂显示。触屏/软键盘输入不受影响。
大多数应用无需修改:该变更由系统框架层(PasswordTransformationMethod)自动处理。
若应用需读取密码可见性设置(需 Flag 开启):
boolean showTouch = ShowSecretsSetting.shouldShowTouchInput(context);
boolean showPhysical = ShowSecretsSetting.shouldShowPhysicalInput(context);若应用自定义了 TransformationMethod:PhysicalInputSpan 是临时 span,在 onTextChanged 回调期间存在,之后立即移除,自定义实现可通过检查此 span 区分输入来源。
短信 OTP 保护
一、特性背景
Android 17 对 targetSdk >= 37 的应用引入 OTP 短信过滤:含 OTP 的短信只有受信任应用能通过 SMS_RECEIVED 广播接收;其他应用需迁移到 SMS Retriever API 或 SMS User Consent API。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37 |
| CompatChange | FILTER_GENERIC_OTP(ID: 437043173) |
三、特性内容
系统对 OTP 短信使用双 broadcast 机制:向 targetSdk < 37 的应用维持旧行为,向 targetSdk >= 37 的应用加 FILTER_GENERIC_OTP 约束,只有受信任应用才能接收。
受信任应用条件(满足任一即可接收 OTP 广播):系统应用、默认短信/助手/拨号应用(ROLE_SMS 等)、具有运营商特权的应用、通过 CompanionDeviceManager 关联的应用、或被授予 READ_OTP_SMS AppOp 的应用。普通第三方应用无法获得 RECEIVE_SENSITIVE_NOTIFICATIONS 权限。
四、应用适配
受影响场景
应用通过 SMS_RECEIVED 广播自行解析短信内容来提取 OTP 验证码,且 targetSdk >= 37。
检测方式
搜索代码中 SMS_RECEIVED、SMS_DELIVER 广播接收,以及解析短信内容提取数字验证码的逻辑。
迁移方案一:SMS Retriever API(推荐,无需任何 SMS 权限)
// 启动 SMS Retriever,有效期 5 分钟
SmsRetrieverClient client = SmsRetriever.getClient(context);
Task<Void> task = client.startSmsRetriever();
task.addOnSuccessListener(aVoid -> {
// 注册 BroadcastReceiver 等待系统回调
});
// BroadcastReceiver 中接收 OTP 短信
String message = intent.getStringExtra(SmsRetriever.EXTRA_SMS_MESSAGE);
// 从 message 中提取 OTPSMS Retriever API 要求短信末尾嵌入应用签名 Hash(格式:<#> Your OTP is 123456\nAB12CD34EF5)。
迁移方案二:SMS User Consent API(无需修改短信格式)
// 展示系统弹窗,让用户确认分享短信
SmsUserConsentClient client = SmsRetriever.getSmsUserConsentClient(context);
Task<Void> task = client.startSmsUserConsent(/* senderPhoneNumber */ null);
task.addOnSuccessListener(aVoid -> {
// 注册 BroadcastReceiver 接收 SEND_PERMISSION 结果
});安全
活动安全性增强(BAL 强化)
Google 官方文档:行为变更:targetSdk 37+ - 活动安全性
一、特性背景
Android 17 在 BAL(后台 Activity 启动)方面有三项变化:(1)旧 BAL 常量 @Deprecated,三个新常量成为稳定 API;(2)IntentSender.sendIntent() 不再自动豁免 BAL 检查(targetSdk >= 37);(3)StrictMode BAL 检测对 targetSdk > 35 自动启用。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 编译警告:所有应用;IntentSender BAL 限制:targetSdk >= 37 |
| CompatChange | COVER_INTENT_SENDER(ID: 405995292) |
三、特性内容
MODE_BACKGROUND_ACTIVITY_START_ALLOWED废弃:新增三个替代常量(已成为稳定 API):MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS(3)MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE(4)——推荐MODE_BACKGROUND_ACTIVITY_START_DENIED(2)- IntentSender BAL 限制:CompatChange
COVER_INTENT_SENDER(ID: 405995292),targetSdk >= 37 且 Flag 开启时,IntentSender.sendIntent()不再自动豁免 BAL 检查 - StrictMode 自动检测:
detectBlockedBackgroundActivityLaunch()对 targetSdk > 35 的应用在detectAll()中自动启用
四、应用适配
场景 1:替换废弃常量(编译警告,低风险)
// 旧代码(编译警告)
ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
// 推荐:大多数场景使用 ALLOW_IF_VISIBLE(仅应用可见时允许启动)
ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)场景 2:通过 IntentSender 启动 Activity(targetSdk > 36,高风险)
当 bal_cover_intent_sender Flag 开启时,IntentSender.sendIntent() 不再自动豁免 BAL 检查,若调用方无 BAL 特权,启动将被阻断。修复方案:
// 旧代码(在 targetSdk > 36 时可能被阻断)
intentSender.sendIntent(context, 0, intent, onFinished, handler);
// 新代码:显式指定 BAL 模式
Bundle options = ActivityOptions.makeBasic()
.setPendingIntentBackgroundActivityStartMode(
ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_IF_VISIBLE)
.toBundle();
intentSender.sendIntent(context, 0, intent, null, options, executor, onFinished);场景 3:使用 StrictMode 检测 BAL 问题(targetSdk > 35 时 detectAll() 自动包含):
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll() // Android 17 对 targetSdk > 35 自动包含 BAL 检测
.penaltyLog()
.build());CT 证书透明度默认启用
Google 官方文档:行为变更:targetSdk 37+ - 默认启用 CT
一、特性背景
CT(证书透明度)要求 CA 将证书记录到公开日志。Android 16 默认关闭,Android 17 对 targetSdk >= 37 的应用默认开启。服务端证书若未提交至 CT 日志,HTTPS 握手将以 SSLHandshakeException 失败。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37 |
| 当前状态 | 双重门控:CompatChange + Aconfig flag 同时满足才启用 |
三、特性内容
CT 通过双重门控启用:CompatChange DEFAULT_ENABLE_CERTIFICATE_TRANSPARENCY(ID: 407952621,targetSdk > 36)+ Aconfig flag certificate_transparency_default_enabled 同时满足时生效。
自动禁用 CT 的场景:NSC 中使用 src="user"(用户证书)或内联证书(src="@raw/xxx")时,CT 自动关闭;连接 localhost 时同样自动跳过。
四、应用适配
崩溃特征
javax.net.ssl.SSLHandshakeException: Certificate transparency verification failed
at com.android.org.conscrypt.TrustManagerImpl.checkServerTrusted(...)
at android.security.net.config.NetworkSecurityTrustManager.checkServerTrusted(...)区分方式:错误消息含 certificate transparency,且仅在 targetSdk >= 37 的设备上复现。
不受影响:localhost、NSC 中使用 src="user" 或内联证书、显式声明 <certificateTransparency enabled="false" />、targetSdk <= 36。
修复方案
方案一(推荐):确保服务端证书已提交 CT 日志(主流 CA 默认支持)。
方案二:NSC 中对特定域名关闭 CT:
<network-security-config>
<domain-config>
<domain includeSubdomains="true">internal.company.com</domain>
<certificateTransparency enabled="false" />
</domain-config>
</network-security-config>方案三:NSC 中使用 src="user" 信任用户证书库(CT 自动禁用)。
加载可写原生库抛 UnsatisfiedLinkError(原生 DCL 加强)
Google 官方文档:行为变更:targetSdk 37+ - 更安全的原生 DCL
一、特性背景
Android 17 将 Safer DCL 保护扩展到原生库:通过 System.load() 加载可写的 .so 文件时将抛出 UnsatisfiedLinkError(targetSdk >= 37,双重开关控制)。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | targetSdk >= 37 |
三、特性内容
Android 17 在 Runtime.load0() 中新增可写性检测:加载 .so 文件前检查 file.canWrite(),双重开关(ART 服务端 flag + CompatChange THROW_ERROR_FOR_WRITABLE_DCL)同时满足时抛出 UnsatisfiedLinkError。
UID 豁免:root(0)、system(1000)、adb shell(2000) 跳过检查;只读文件系统上的文件也跳过检查。
四、应用适配
触发条件(全部满足才崩溃)
- ART 服务端 flag
read_only_dynamic_code_load_throw_exception已开启 - 应用 targetSdkVersion >= 37
- 加载的
.so文件在文件系统上可写(file.canWrite() == true) - 运行进程 UID 不是 root(0)、system(1000)、adb shell(2000)
- 文件所在文件系统不是只读挂载
崩溃特征
java.lang.UnsatisfiedLinkError: Attempt to load writable file: /data/app/com.example/.../libfoo.so
at java.lang.Runtime.load0(Runtime.java:958)
at java.lang.System.load(System.java:xxx)高风险场景
| 场景 | 原因 |
| 动态下载并加载插件 .so | 下载后未设置文件为只读 |
| 热更新框架直接写入并加载 .so | 覆盖写入后文件可写 |
| 从 /data/local/tmp 加载调试库 | 该目录文件默认可写 |
修复方案:加载前将文件设为只读
File soFile = new File(soPath);
soFile.setWritable(false, false); // 所有用户均不可写
System.load(soFile.getAbsolutePath());或使用 POSIX 权限:
Os.chmod(soPath, 0444);
System.load(soPath);设备类型
大屏设备忽略屏幕方向和尺寸调整限制
Google 官方文档:行为变更:targetSdk 37+ - 大屏设备忽略限制 | 详细指南
一、特性背景
Android 16 已对大屏(sw >= 600dp)上 targetSdk >= 36 的应用强制忽略方向声明,但允许 opt-out。Android 17 封堵 opt-out:targetSdk >= 37 时 PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY 完全失效,且相关 aconfig flag 已删除(行为无条件生效)。游戏类应用(appCategory="game")始终豁免。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 方向忽略:targetSdk >= 36;opt-out 封堵:targetSdk >= 37 |
| 当前状态 | 已生效(相关 flag 已删除,行为无条件生效) |
三、特性内容
Android 17 的大屏策略分两层:
- 大屏方向忽略(targetSdk >= 36):
sw >= 600dp的设备无条件忽略方向请求 - 封堵 opt-out(targetSdk >= 37):
PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY属性完全失效 - 游戏豁免:声明
appCategory="game"的应用始终豁免
四、应用适配
受影响条件:sw >= 600dp 设备 + targetSdk >= 36 + 声明了被忽略的方向/尺寸属性(screenOrientation portrait/landscape 类值、resizableActivity=false、minAspectRatio/maxAspectRatio、setRequestedOrientation() API)。
Android 17 额外影响:targetSdk >= 37 时 PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY opt-out 完全失效。
不受影响:游戏类应用(appCategory="game")、手机形态设备(sw < 600dp)。
视觉问题特征:宽幅黑边(方向被忽略)、布局拉伸(无自适应布局)、方向死循环(反复调用 setRequestedOrientation() 被忽略)。
长期适配方案(推荐)
<!-- 1. 放宽方向限制 -->
<activity android:screenOrientation="fullUser" .../>
<!-- 2. 声明可调整大小 -->
<application android:resizableActivity="true" .../>// 3. 响应方向变化而非阻止
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 适配横屏布局
}
}游戏应用豁免(唯一 manifest 级豁免手段):
<application android:appCategory="game" .../>4.新功能和 API
本章节介绍 Android 17 引入的新功能和 API,供开发者主动集成使用,不属于强制性行为变更。涉及特性均由 aconfig flag 动态控制,flag 开启后应用可获得相应能力。
核心功能
AlarmManager 精确闹钟 OnAlarmListener 变体
一、特性背景
Android 17 为 AlarmManager.setExactAndAllowWhileIdle() 新增公开的 OnAlarmListener 重载,无需 SCHEDULE_EXACT_ALARM 权限,在 Doze 模式下以独立的触发配额执行回调,适合运行前台服务的长期运行应用。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 无要求(纯新增 API) |
| 权限要求 | 不需要 SCHEDULE_EXACT_ALARM |
| 当前状态 | @FlaggedApi,flag 默认关闭 |
三、特性内容
新增方法:setExactAndAllowWhileIdle(int type, long triggerAtMillis, String tag, Executor executor, OnAlarmListener listener)。
关键差异:无需权限、进程内直接回调、但绑定到调用进程(进程进入 cached 状态时闹钟会被丢弃)。
四、应用适配
适合场景:即时通讯心跳、媒体精确触发、在前台服务运行期间使用,无需 SCHEDULE_EXACT_ALARM 权限。
使用示例
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
Executor executor = Executors.newSingleThreadExecutor();
// 必须保存引用,以便后续取消
AlarmManager.OnAlarmListener heartbeatListener = () -> {
// 在 executor 线程执行,进程内直接回调
doHeartbeat();
};
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.ELAPSED_REALTIME_WAKEUP,
SystemClock.elapsedRealtime() + INTERVAL_MS,
"MyApp:heartbeat",
// tag:必须非空,用于调试和电量归因
executor,
heartbeatListener);
// 在组件销毁时取消,防止内存泄漏
// alarmManager.cancel(heartbeatListener);注意:
- 必须保持进程活跃(前台服务)
- 进程进入 cached 状态后闹钟自动丢弃
- 销毁时调用
cancel(listener)避免泄漏。
时区偏移量变化广播(ACTION_TIMEZONE_OFFSET_CHANGED)
一、特性背景
Android 17 新增 ACTION_TIMEZONE_OFFSET_CHANGED 广播,在 DST 切换(时区 ID 不变但 UTC 偏移量变化)时触发,携带新旧偏移量。此前系统没有针对 DST 切换的专用广播。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 无限制(所有应用均可注册接收) |
| 当前状态 | @FlaggedApi,受保护广播 |
三、特性内容
新增广播 ACTION_TIMEZONE_OFFSET_CHANGED(受保护广播),携带两个附加字段:EXTRA_NEW_TIMEZONE_OFFSET(新偏移量,秒)和 EXTRA_OLD_TIMEZONE_OFFSET(旧偏移量,秒)。系统通过 TimeZoneOffsetHelper 预调度一个 RTC 精确闹钟,在 DST 过渡时刻到来时触发该广播
各场景广播触发情况:
| 场景 | ACTION_TIMEZONE_CHANGED | ACTION_TIMEZONE_OFFSET_CHANGED |
| 用户切换时区(ID 改变) | 触发 | 不触发 |
| DST 夏令时/冬令时切换(ID 不变) | 不触发 | 触发 |
| Android 16 中 DST 切换 | 不触发 | 不存在 |
四、应用适配
现有代码无需修改:ACTION_TIMEZONE_CHANGED 行为不变。日历/闹钟/调度类应用可主动感知 DST 过渡:
// 注册接收 DST 偏移变化广播
IntentFilter filter = new IntentFilter(Intent.ACTION_TIMEZONE_OFFSET_CHANGED);
registerReceiver(receiver, filter);
// 在 BroadcastReceiver 中处理
int oldOffset = intent.getIntExtra(Intent.EXTRA_OLD_TIMEZONE_OFFSET, 0);// 单位:秒
int newOffset = intent.getIntExtra(Intent.EXTRA_NEW_TIMEZONE_OFFSET, 0); // 单位:秒
// 重新计算/刷新时区相关 UI兼容注意:该广播由 aconfig flag 控制,不保证所有 Android 17 设备均触发。对于时间准确性要求高的场景,建议额外维持 AlarmManager 定时校准作为备选方案。
隐私权
联系人选择器(Contact Picker)
Google 官方文档:联系人选择器
一、特性背景
Android 17 引入系统级联系人选择器,应用通过 Intent.ACTION_PICK_CONTACTS 让用户选择要分享的联系人,仅获得一次性会话 URI 权限,无需 READ_CONTACTS 权限。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 无限制(新功能) |
| 核心类 | ContactsPickerSessionContract |
| 当前状态 | 由服务端 flag 动态控制 |
三、特性内容
核心 API(ContactsPickerSessionContract):
| API | 说明 |
| ACTION_PICK_CONTACTS | 启动联系人选择器的 Intent Action |
| EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS | 必填:指定需要的联系人数据字段(MIME 类型列表,如 Phone.CONTENT_ITEM_TYPE、Email.CONTENT_ITEM_TYPE 等) |
| EXTRA_PICK_CONTACTS_MATCH_ALL_DATA_FIELDS | 可选:要求联系人同时具备所有指定字段(默认 false) |
| EXTRA_PICK_CONTACTS_SELECTION_LIMIT | 可选:最多可选联系人数量(默认 50,最大 100) |
| CONTENT_URI | 会话结果 URI 基础路径 |
四、应用适配
方案一(推荐):迁移到新 API,消除 READ_CONTACTS 权限依赖
// 1. 构建 Intent
val requestedFields = arrayListOf(
ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE,
ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE
)
val pickIntent = Intent(ContactsPickerSessionContract.ACTION_PICK_CONTACTS).apply {
putStringArrayListExtra(
ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_REQUESTED_DATA_FIELDS,
requestedFields
)
// 可选:允许多选
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
// 可选:限制最多选 10 个
putExtra(ContactsPickerSessionContract.EXTRA_PICK_CONTACTS_SELECTION_LIMIT, 10)
}
// 2. 启动(推荐使用 ActivityResultLauncher)
val launcher = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val sessionUri = result.data?.data ?: return@registerForActivityResult
// 3. 用 ContentResolver 查询会话 URI(在后台线程执行)
contentResolver.query(sessionUri, null, null, null, null)?.use { cursor ->
while (cursor.moveToNext()) {
val mimeType = cursor.getString(cursor.getColumnIndex(ContactsContract.Data.MIMETYPE))
// 根据 mimeType 处理对应字段
}
}
}
}
launcher.launch(pickIntent)注意事项
- 无需 READ_CONTACTS 权限,选择器通过会话 URI 授予一次性访问权限
- 结果 URI 为临时会话 URI,进程终止后失效,需立即持久化数据
SELECTION_LIMIT超过 100 会抛IllegalArgumentException- flag 未开启时
ACTION_PICK_CONTACTS无法解析,应做好回退处理 - 兼容测试:targetSdk < 37 的应用可通过
putExtra(Intent.EXTRA_USE_SYSTEM_CONTACTS_PICKER, true)在 Android 17 设备上测试新选择器行为
安全
Android 高级保护模式(AAPM)
一、特性背景
AAPM 是用户可开启的设备级安全加固模式。Android 17 将 AdvancedProtectionManager 核心 API 正式升级为稳定 API,新增 AccessibilityService 限制:AAPM 开启后,非工具型 AccessibilityService 被强制禁用。
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 无(主动查询) |
| 权限要求 | QUERY_ADVANCED_PROTECTION_MODE(normal 级别) |
| 当前状态 | 核心 API 已稳定 |
三、特性内容
公开 API(core/api/current.txt 已收录,需 QUERY_ADVANCED_PROTECTION_MODE normal 权限):
AdvancedProtectionManager.isAdvancedProtectionEnabled():查询当前状态registerAdvancedProtectionCallback(executor, callback):监听状态变化(注册时立即回调一次)unregisterAdvancedProtectionCallback(callback):取消监听
AAPM 开启后的影响:禁止侧载安装 APK、限制 USB 数据传输、强制 Play Protect 扫描、非工具型 AccessibilityService 被强制禁用。
四、应用适配
场景一:根据 AAPM 状态调整功能
<uses-permission android:name="android.permission.QUERY_ADVANCED_PROTECTION_MODE" />AdvancedProtectionManager mgr = context.getSystemService(AdvancedProtectionManager.class);
if (mgr.isAdvancedProtectionEnabled()) {
disableRiskyFeatures();
}
// 监听状态变化(避免轮询)
mgr.registerAdvancedProtectionCallback(mainExecutor, enabled -> {
if (enabled) disableRiskyFeatures();
else enableRiskyFeatures();
});场景二:AccessibilityService 应用(重点适配项)
若应用的 AccessibilityService 是辅助功能工具,必须声明 isAccessibilityTool="true",否则 AAPM 模式下服务将被强制关闭:
<accessibility-service android:isAccessibilityTool="true" ... />PQC APK 混合签名(ML-DSA)
一、特性背景
Android 17 引入 APK V3.2 签名方案(Hybrid Block),支持经典密钥(RSA/EC)与 ML-DSA(PQC)配对混合签名;旧版设备仅验证经典签名块,Android 17+ 设备同时验证 Hybrid 块;使用 Google Play 签名的应用无需操作,自行管理密钥的应用需更新。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37) |
| targetSdk 要求 | 无(签名方案对所有应用适用)查询) |
| 权限要求 | 仅影响自管理签名密钥的应用 |
| 当前状态 | @FlaggedApi |
三、特性内容
Android 17 新增 APK V3.2 签名方案(Hybrid Block,签名块 ID 0x70e1c89f,最低 SDK = API 37),支持 ML-DSA-65(推荐)和 ML-DSA-87 两种 PQC 变体。AndroidKeyStore 同步新增 KEY_ALGORITHM_ML_DSA / KEY_ALGORITHM_ML_DSA_65 / KEY_ALGORITHM_ML_DSA_87 常量。
apksigner 命令行新增参数:
# --hybrid-signer-role classical —— 指定该签名者为 hybrid 中的经典密钥
# --hybrid-signer-role pqc —— 指定该签名者为 hybrid 中的 ML-DSA 密钥
# --hybrid-min-sdk-version <N> —— hybrid 块生效的最低 SDK 版本(默认 37)注意:V4 签名当前不支持 PQC 签名。
四、应用适配
使用 Google Play 签名(Play App Signing)的应用
无需任何操作。Google Play 将自动为应用添加 PQC 混合签名,开发者只需等待 Play 推出相关选项。
使用自行管理签名密钥的应用
1.生成 ML-DSA 密钥对:使用更新后的 Android build 工具(apksigner)或 Java KeyPairGenerator 生成 ML-DSA-65 密钥对及自签名证书
2.更新签名命令:
apksigner sign \
--ks keystore.jks --ks-key-alias classical_key \
--hybrid-signer-role classical \
--next-signer \
--ks keystore.jks --ks-key-alias mldsa65_key \
--hybrid-signer-role pqc \
--hybrid-min-sdk-version 37 \
output.apk3.密钥约束:必须创建新的经典密钥,不能复用旧的经典密钥
4.向后兼容:旧版 Android 设备仍验证 V3.0/V3.1 经典签名
Android 开发者验证(Developer Verification)
Google 官方文档:Android 开发者验证
一、特性背景
Android 17 引入开发者验证(Developer Verification)框架,要求所有应用都必须由经过验证的开发者注册,用户才能在已获认证的Android设备上安装。验证逻辑由设备上的验证代理(如 Google Play Services)实现。
Google 计划 2026 年 9 月起在部分地区强制执行,2027 年后全球推广。
关键信息: Google Play 分发的应用 98% 自动注册;ADB 安装始终绕过;企业托管设备豁免;未注册应用侧载需经过高级确认流程。
二、适用范围
| 维度 | 说明 |
| 平台版本 | Android 17(API 37)引入框架 |
| targetSdk 要求 | 无(由验证代理策略决定) |
| 当前状态 | 框架已就绪,实际执行取决于设备上的验证代理 |
三、特性内容
Android 17 新增 Developer Verification 子系统(core/java/android/content/pm/verify/developer/,全新目录),采用可插拔的 Verification Agent 架构:
- 核心流程:应用安装时,
PackageInstaller调用设备上的验证代理(如 Google Play Services),验证代理查询开发者注册库、校验签名密钥,返回验证结果 - ADB 安装始终绕过验证:(用于开发/测试)
- 默认验证超时:10 秒(可配置),最大延长 10 分钟
安装失败时,PackageInstaller 会根据验证结果向用户展示对应提示(开发者未通过验证、网络不可用、验证不可用等)。
新增安装失败原因常量:DEVELOPER_VERIFICATION_FAILED_REASON_UNKNOWN(0)、DEVELOPER_VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE(1)、DEVELOPER_VERIFICATION_FAILED_REASON_DEVELOPER_BLOCKED(2)。
四、应用适配
受影响场景
通过非 Google Play 渠道分发的应用(APK 直接下载、企业内部分发、第三方应用商店),在认证 Android 设备上安装时,若开发者未注册且设备策略启用验证,安装将被阻止或弹出警告提示。
不受影响:Google Play 安装、ADB 安装(开发调试)、企业托管设备、策略为 POLICY_NONE 的设备。
适配建议
第一步:确认分发渠道
- 仅 Google Play 分发:无需操作(98% 应用自动注册)
- Google Play + 其他渠道:在 Google Play 管理中心 确认注册状态
- 仅非 Play 渠道:需在 Android Developer Console 注册
第二步:注册应用(非 Play 渠道开发者)
- 访问 Android Developer Console 创建账号
- 完成身份验证(个人开发者需政府签发的身份证明;组织开发者额外需要 DUNS 号码)
- 注册应用包名(
applicationId)和 APK 签名证书的 SHA-256 指纹 - 通过签名验证证明包名所有权(使用注册密钥签署包含指定代码片段的 APK)
第三步(面向安装器应用):处理安装结果中新增的 EXTRA_DEVELOPER_VERIFICATION_FAILURE_REASON Extra(DEVELOPER_BLOCKED、NETWORK_UNAVAILABLE、UNKNOWN 三种原因)。
时间线
| 时间 | 事件 |
| 2026 年 3 月 | 注册开放(Full Distribution) |
| 2026 年 6 月 | Limited Distribution 早期注册 |
| 2026 年 9 月 | 部分地区强制执行(巴西、印尼、新加坡、泰国) |
| 2027 年 + | 全球推广 |
5.迁移指南
每次发布新的 Android 版本时,我们都会推出一些全新的功能并引入一些行为变更,目的就在于提高 Android 的实用性、安全性和性能。在许多情况下,您的应用都可以直接使用并完全按预期运行;而在其他的一些情况下,您可能需要对应用进行更新以适应这些平台变更。
源代码发布到 AOSP(Android 开源项目)后,用户随之就可能开始使用新平台。因此,应用必须做好准备,让用户能够正常使用,最好还能利用新的功能和 API 发挥新平台的最大优势。
典型的迁移包含两个阶段,这两个阶段可以同时进行:
- 确保应用兼容性(在 Android 17 最终发布前)
- 针对新平台的功能和 API 调整应用(最终发布后尽快进行)
确保与 Android 17 兼容
您必须测试现有应用在 Android 17 上的运行情况,以确保更新到最新版 Android 的用户获得良好的体验。有些平台变更可能会影响应用的行为方式,因此,必须尽早进行全面测试并对应用进行任何必要的调整。
您通常可以调整应用并发布更新,而无需更改应用的 targetSdkVersion。同样,您应该也不需要使用新的 API 或更改应用的 compileSdkVersion,不过这一点可能要取决于应用的构建方式及其所使用的平台功能。
在开始测试之前,请务必熟悉适用于所有应用的行为变更。即使您不更改应用的 targetSdkVersion,这些变更也可能会影响您的应用。

测试应用在 Android 17 上的兼容性
在大多数情况下,测试与 Android 17 的兼容性与普通的应用测试类似。这时有必要回顾一下核心应用质量指南和测试最佳实践。
如要进行测试,请在搭载 Android 17 的设备上安装您当前发布的应用,并完成所有流程和功能,以排查问题。为帮助您确定测试重点,请查看 Android 17 中引入的适用于所有应用的行为变更,这些变更会影响应用的功能或导致应用崩溃。
此外,请务必查看并测试受限非 SDK 接口的使用。您应使用应用公共 SDK 或 NDK 等效项替换应用使用的任何受限接口。留意突出显示这些访问权限的 logcat 警告,并使用 StrictMode 方法 detectNonSdkApiUsage() 以程序化地捕获它们。
最后,请务必完整测试应用中的库和 SDK,确保它们在 Android 17 上按预期运行,并遵循隐私权、性能、用户体验、数据处理和权限方面的最佳实践。如果您遇到问题,请尝试更新到最新版本的 SDK,或联系 SDK 开发者寻求帮助。
当您完成测试并进行更新后,我们建议您立即发布兼容的应用。这样可以尽早让您的用户测试应用,并帮助用户顺利过渡到 Android 17。
更新应用的目标平台并使用新 API 进行构建
发布应用的兼容版本后,下一步是通过更新 targetSdkVersion 并利用 Android 17 的新 API 和功能来添加对 Android 17 的全面支持。准备就绪后,您即可开始进行这些更新,请注意以新平台为目标平台的 Google Play 要求。
当您计划全面支持 Android 17 时,请查看影响以 Android 17 为目标平台的应用的行为变更。这些针对性的行为变更可能会导致需要解决的功能问题。在某些情况下,这些变更需要进行大量开发工作,因此我们建议您尽早了解并解决这些问题。为帮助确定影响您的应用的具体行为变更,请使用兼容性切换开关来测试已启用所选变更的应用。
以下步骤介绍了如何全面支持 Android 17。

获取 SDK,更改目标平台,使用新 API 进行构建
如需开始针对 Android 17 全面支持进行测试,请使用最新预览版的 Android Studio 下载 Android 17 SDK,以及所需的任何其他工具。接下来,更新应用的 targetSdkVersion 和 compileSdkVersion,然后重新编译应用。如需了解详情,请参阅 SDK 设置指南。
测试 Android 17 应用
编译应用并将其安装到搭载 Android 17 的设备上后,请开始测试,确保应用能够在 Android 17 上正常运行。某些行为变更仅在应用以新平台为目标平台时才适用,因此您需要在开始之前查看这些变更。
与基本兼容性测试一样,完成所有流程和功能以查找问题。将测试重点放在以 Android 17 为目标平台的应用的行为变更上。您还可以根据核心应用质量指南和测试最佳实践检查您的应用。
请务必查看并测试可能适用的受限非 SDK 接口的使用。留意突出显示这些访问权限的 logcat 警告,并使用 StrictMode 方法 detectNonSdkApiUsage() 以编程方式捕获它们。
最后,请务必完整测试应用中的库和 SDK,确保它们在 Android 17 上按预期运行,并遵循隐私权、性能、用户体验、数据处理和权限方面的最佳实践。如果您遇到问题,请尝试更新到最新版本的 SDK,或联系 SDK 开发者寻求帮助。
使用应用兼容性切换开关进行测试
Android 17 包含兼容性切换开关,可让您更轻松地在应用中测试针对性的行为变更。对于可调试的应用,切换开关可让您:
- 在不实际更改应用的 targetSdkVersion 的情况下测试针对性的变更。您可以使用切换开关强制启用特定的针对性行为变更,以评估对现有应用的影响。
- 仅针对特定变更进行测试。您可以使用切换开关停用除要测试的变更之外的所有针对性变更,而不必一次处理所有针对性变更。
- 通过 adb 管理切换开关。您可以使用 adb 命令在自动测试环境中启用和停用可切换的变更。
- 使用标准变更 ID 更快地进行调试。每个可切换的变更都具有唯一 ID 和名称,可用于在日志输出中快速调试根本原因。
在您准备更改应用的目标平台时,或者在您积极开发以便支持 Android 17 时,切换开关将十分有用。如需了解详情,请参阅兼容性框架变更 (Android 17)。
6.开发者支持
若您在适配过程中遇到任何问题,都可以通过参考此文档反馈给我们,我们的兼容性工程师团队会尽快为您解决。联系我们