针对非 SDK 接口的限制

从 Android 9(API 级别 28)开始,此平台对应用能使用的非 SDK 接口实施了限制。只要应用引用非 SDK 接口或尝试使用反射或 JNI 来获取其句柄,这些限制就适用。这些限制旨在帮助提升用户体验和开发者体验,为用户降低应用发生崩溃的风险,同时为开发者降低紧急发布的风险。如需详细了解有关此限制的决定,请参阅通过减少非 SDK 接口的使用来提高稳定性

区分 SDK 接口和非 SDK 接口

一般而言,公共 SDK 接口是在 Android 框架软件包索引中记录的那些接口。非 SDK 接口的处理是 API 抽象出来的实现细节,因此这些接口可能会在不另行通知的情况下随时发生更改。

为了避免发生崩溃和意外行为,应用应仅使用 SDK 中经过正式记录的类。这也意味着当您的应用通过反射等机制与类互动时,不应访问 SDK 中未列出的方法或字段。

黑名单、灰名单和白名单

随着每个 Android 版本的发布,我们将进一步限制更多非 SDK 接口。我们知道这些限制会影响您的发布工作流,同时我们希望确保您拥有相关工具来检测非 SDK 接口的使用情况、有机会向我们提供反馈,并且有时间根据相应新政策做出规划和调整。

为最大程度地降低非 SDK 使用限制对开发工作流的影响,我们以列表的形式对非 SDK 接口做出了分类,这些列表界定了根据应用的目标 API 级别而定的 SDK 接口使用限制的严格程度。下表介绍了这些列表:

列表 说明
黑名单 无论您应用的目标 API 级别是什么,您都无法使用此列表中的非 SDK 接口。如果您的应用尝试访问其中任何一个接口,系统就会抛出错误
灰名单 只要在您应用的目标 API 级别不限制此列表中的非 SDK 接口,您就可以使用它们。

从 Android 9(API 级别 28)开始,我们在每个 API 级别分别会限制某些非 SDK 接口。如果您应用的目标 API 级别较低,您可以访问灰名单中的受限 API,但如果您的应用尝试访问在您的目标 API 级别受限的非 SDK 接口,系统就会假定此 API 已列入黑名单

注意:在 Android 9(API 级别 28)中,非受限灰名单中的非 SDK 接口称为浅灰名单,而受限灰名单中的非 SDK 接口称为深灰名单。

白名单 此列表中的接口已在 Android 框架软件包索引中正式记录,它们是受支持的接口,您可以自由使用。

尽管您目前仍可以使用灰名单中的某些非 SDK 接口(取决于您应用的目标 API 级别),但是如果您使用任何非 SDK 方法或字段,终归存在很可能会损坏应用的风险。如果您的应用依赖于非 SDK 接口,则应该开始计划迁移到 SDK 接口或其他替代方案。如果您无法为应用中的功能找到使用非 SDK 接口的替代方案,则应该请求新的公共 API

确定接口属于哪个列表

非 SDK 接口的列表是作为平台的一部分编译的。

对于 Android 9(API 级别 28),此文本文件包含已列入灰名单的非受限 API 的列表。黑名单和受限的灰名单是在编译时派生的列表。AOSP 上有一个用于生成相关列表的编译规则。尽管 AOSP 上的列表与 Android 9 上的列表不同,但重叠度相当高。您也可以自行生成黑名单,方法是下载 AOSP,然后运行以下命令:

make hiddenapi-aosp-blacklist
    

然后,您便可以在以下位置找到该文件:

out/target/common/obj/PACKAGING/hiddenapi-aosp-blacklist.txt
    

访问受限的非 SDK 接口时可能会出现的预期行为

下表说明了当您的应用尝试访问黑名单中的非 SDK 接口时可能会出现的预期行为。

访问方式 结果
Dalvik 指令引用某个字段 抛出 NoSuchFieldError
Dalvik 指令引用某个方法 抛出 NoSuchMethodError
通过 Class.getDeclaredField()Class.getField() 进行反射 抛出 NoSuchFieldException
通过 Class.getDeclaredMethod()Class.getMethod() 进行反射 抛出 NoSuchMethodException
通过 Class.getDeclaredFields()Class.getFields() 进行反射 结果中未获取到非 SDK 成员
通过 Class.getDeclaredMethods()Class.getMethods() 进行反射 结果中未获取到非 SDK 成员
通过 env->GetFieldID() 进行 JNI 调用 返回 NULL,抛出 NoSuchFieldError
通过 env->GetMethodID() 进行 JNI 调用 返回 NULL,抛出 NoSuchMethodError

测试您的应用是否使用非 SDK 接口

您可以通过多种方法来测试您的应用是否使用非 SDK 接口。

使用可调试的应用进行测试

您可以通过在搭载 Android 9(API 级别 28)或更高版本的设备或模拟器上构建和运行可调试应用来测试该应用是否使用非 SDK 接口。请确保您使用的设备或模拟器与您应用的目标 API 级别相匹配。

在您的应用上运行测试时,如果该应用访问了某些非 SDK 接口,系统就会输出一条日志消息。您可以检查应用的日志消息,查找以下详细信息:

  • 声明的类、名称和类型(采用 Android 运行时所使用的格式)。
  • 访问方式:链接、反射或 JNI
  • 所访问的非 SDK 接口属于哪个列表。

您可以使用 adb logcat 来查看这些日志消息,这些消息显示在所运行应用的 PID 下。举例而言,日志中可能包含如下条目:

Accessing hidden field Landroid/os/Message;->flags:I (light greylist, JNI)
    

使用 StrictMode API 进行测试

您还可以利用 StrictMode API 来测试您的应用是否使用非 SDK 接口。请使用 detectNonSdkApiUsage 方法来启用此 API。启用 StrictMode API 后,您可以使用 penaltyListener 来接收每次使用非 SDK 接口的行为所对应的回调,并且您可以在其中实现自定义处理。回调中提供的 Violation 对象派生自 Throwable,并且封闭式堆栈轨迹会提供相应使用行为的上下文。

使用 veridex 工具进行测试

您还可以在 APK 上运行静态分析工具 veridex。veridex 工具会扫描 APK 的整个代码库(包括所有第三方库),并报告发现的所有使用非 SDK 接口的行为。

veridex 工具存在以下局限性:

  • 它无法检测到通过 JNI 实现的调用。
  • 它只能检测到一部分通过反射实现的调用。
  • 它对非活动代码路径的分析仅限于 API 级别的检查。

请求新的公共 API

如果您无法为应用中的功能找到使用非 SDK 接口的替代方案,则可以创建功能请求以向 SDK 中添加新的公共 API。

创建功能请求时,请提供以下方面的信息:

  • 您目前使用的是灰名单中的哪些 API,包括 Accessing hidden ... logcat 消息中显示的完整描述符。
  • 为什么需要使用这些 API,包括与需要使用这些 API 的高级功能相关的详情,而不仅是关于相应低级功能的详情。
  • 为什么所有相关的公共 SDK API 都不足以满足您的需要。
  • 您尝试过的其他替代方案,以及这些方案都无效的原因。

如果您在功能请求中提供这些详细信息,我们批准添加新公共 API 的可能性就会更高。

其他问题

本部分针对开发者经常会提出的其他一些问题给出了解答:

常见问题

Google 如何确保通过 issuetracker 捕获所有应用的需求?

我们对应用进行静态分析,并以下面的方法作为补充,从而针对 Android 9(API 级别 28)创建了初始列表:

  • 对热门的 Play 应用和非 Play 应用进行手动测试
  • 查看内部报告
  • 自动在内部用户中收集数据
  • 查看开发者预览报告
  • 执行额外的静态分析,以基于谨慎原则包含更多假正例

在我们评估针对每个新版本的列表时,我们会考虑 API 使用情况以及开发者通过问题跟踪器提供的反馈。

如何允许访问非 SDK 接口?

您可以使用以下 adb 命令来更改 API 强制执行策略,从而允许在开发设备上访问非 SDK 接口:

adb shell settings put global hidden_api_policy_pre_p_apps  1
    adb shell settings put global hidden_api_policy_p_apps 1
    

要将 API 强制执行策略重置为默认设置,请使用以下命令:

adb shell settings delete global hidden_api_policy_pre_p_apps
    adb shell settings delete global hidden_api_policy_p_apps
    

这些命令无需设备启用 root 权限即可执行。

您可以将 API 强制执行策略中的整数设置为以下值之一:

  • 0:停用所有非 SDK 接口检测。如果使用此设置,系统会停止输出有关非 SDK 接口使用行为的所有日志消息,并阻止您使用 StrictMode API 测试应用。建议不要使用此设置。
  • 1:允许访问所有非 SDK 接口,但同时输出日志消息,并且在其中显示针对所有非 SDK 接口使用行为的警告。如果使用此设置,您仍可以使用 StrictMode API 来测试应用。
  • 2:禁止使用已列入黑名单或灰名单并且在您的目标 API 级别受限的非 SDK 接口。
  • 3:禁止使用已列入黑名单的非 SDK 接口,但允许使用已列入灰名单并且在您的目标 API 级别受限的接口。

有关非 SDK 接口列表的问题

在系统映像中的什么位置可以找到黑名单和灰名单?

它们是在平台 dex 文件中的字段和方法访问标记位中编码的。系统映像中不存在用于容纳这些列表的单独文件。

搭载同一 Android 版本的不同 OEM 设备上的黑名单和灰名单是否相同?

原始设备制造商 (OEM) 可以自行向黑名单中添加更多接口,但是无法从 AOSP 黑名单或灰名单中移除接口。CDD 会阻止此类更改,并且 CTS 测试会确保 Android 运行时强制执行相应列表。

对原生代码中的非 NDK 接口是否有任何限制?

Android SDK 包含 Java 接口。Android 平台从 Android 7(API 级别 26)开始限制访问原生 C/C++ 代码中的非 NDK 接口。如需了解详情,请参阅通过在 Android N 中限制私有 C/C++ 符号提升稳定性

Google 是否计划限制对 dex2oat 或 DEX 文件的操作?

我们没有限制对 dex2oat 二进制文件的访问的正式计划,但我们并不打算让 DEX 成为一种固定的文件格式,并且除了在 Dalvik 可执行文件格式中公开指定的部分之外,我们也不打算让它成为一个公共接口。我们保留随时修改或清除 dex2oat 和 DEX 格式的未指定部分的权利。另请注意,由 dex2oat 生成的派生文件都属于未指定的格式,例如 ODEX(也称为OAT)、VDEX 和 CDEX。

如果某个重要的第三方 SDK(例如混淆工具)无法避免使用非 SDK 接口,但确实承诺维持对未来 Android 版本的兼容性,该怎么办?在这种情况下,Android 可以免除其兼容性要求吗?

我们没有计划针对单个 SDK 免除兼容性要求。如果 SDK 开发者目前只能依赖于灰名单中的接口来维持兼容性,那么他们应该着手计划迁移到 SDK 接口或其他替代方案;倘若找不到使用非 SDK 接口的替代方案,则可请求新的公共 API

 

针对非 SDK 接口的限制是否适用于包括系统应用和第一方应用在内的所有应用,而不仅是第三方应用?

是,不过,我们对使用平台密钥进行签名的应用免除这项限制,同时为一些系统映像应用提供软件包白名单。请注意,免除限制的情况仅适用于系统映像应用(或更新的系统映像应用)。软件包白名单仅适用于针对私有平台 API(而不是 SDK API)构建的应用(其中 LOCAL_PRIVATE_PLATFORM_APIS := true)。