在运行时更改应用资源的取值

运行时资源叠加层 (RRO) 是一种在运行时更改目标软件包资源值的软件包。例如,安装在系统映像上的应用可能会根据资源的取值更改其行为。与其在构建时硬编码资源值,不如使用安装在不同分区上的 RRO 在运行时更改应用资源的取值。

RRO 可以启用或停用。您可以编程方式设置启用/停用状态,以切换 RRO 更改资源值的能力。RRO 默认处于停用状态(但是,静态 RRO 默认处于启用状态)。

叠加层资源

覆盖层的工作原理是将覆盖包中定义的资源映射到目标包中定义的资源。当应用尝试解析目标包中资源的值时,将返回映射到的覆盖层资源的值。

设置清单

如果软件包包含作为 <manifest> 标记的子标记的 <overlay> 标记,则该软件包被视为 RRO 软件包。

  • 必需属性 android:targetPackage 的值指定了 RRO 计划覆盖的软件包的名称。

  • 可选属性 android:targetName 的值指定了 RRO 计划覆盖的目标软件包的可覆盖资源子集的名称。如果目标未定义可覆盖资源集,则不应存在此属性。

以下代码显示了一个示例覆盖层 AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.overlay">
    <application android:hasCode="false" />
    <overlay android:targetPackage="com.example.target"
                   android:targetName="OverlayableResources"/>
</manifest>

覆盖层无法覆盖代码,因此它们不能具有 DEX 文件。此外,清单中 <application> 标记的 android:hasCode 属性必须设置为 false

定义资源映射

在 Android 11 或更高版本中,定义覆盖层资源映射的推荐机制是在覆盖层软件包的 res/xml 目录中创建一个文件,枚举应覆盖的目标资源及其替换值,然后将 <overlay> 清单标记的 android:resourcesMap 属性的值设置为对资源映射文件的引用。

以下代码显示了一个示例 res/xml/overlays.xml 文件。

<?xml version="1.0" encoding="utf-8"?>
<overlay xmlns:android="http://schemas.android.com/apk/res/android" >
    <!-- Overlays string/config1 and string/config2 with the same resource. -->
    <item target="string/config1" value="@string/overlay1" />
    <item target="string/config2" value="@string/overlay1" />

    <!-- Overlays string/config3 with the string "yes". -->
    <item target="string/config3" value="@android:string/yes" />

    <!-- Overlays string/config4 with the string "Hardcoded string". -->
    <item target="string/config4" value="Hardcoded string" />

    <!-- Overlays integer/config5 with the integer "42". -->
    <item target="integer/config5" value="42" />
</overlay>

以下代码显示了一个示例覆盖层清单。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.overlay">
    <application android:hasCode="false" />
    <overlay android:targetPackage="com.example.target"
                   android:targetName="OverlayableResources"
                   android:resourcesMap="@xml/overlays"/>
</manifest>

构建软件包

Android 11 或更高版本支持用于覆盖层的 Soong 构建规则,该规则可防止 Android Asset Packaging Tool 2 (AAPT2) 尝试删除具有相同值的资源配置 (--no-resource-deduping) 以及删除没有默认配置的资源 (--no-resource-removal)。以下代码显示了一个示例 Android.bp 文件。

runtime_resource_overlay {
    name: "ExampleOverlay",
    sdk_version: "current",
}

解析资源

如果目标资源或覆盖层资源为正在查询的资源定义了多个配置,则资源运行时会返回与设备配置最匹配的配置的值。要确定哪个配置是最佳匹配配置,请将覆盖层资源配置集合并到目标资源配置集中,然后遵循常规资源解析流程(有关详细信息,请参阅Android 如何查找最佳匹配资源)。

例如,如果覆盖层为 drawable-en 配置定义了一个值,而目标为 drawable-en-port 定义了一个值,则 drawable-en-port 具有更好的匹配项,因此在运行时选择目标配置 drawable-en-port 的值。要覆盖所有 drawable-en 配置,覆盖层必须为目标定义的每个 drawable-en 配置定义一个值。

覆盖层可以引用其自身的资源,但在 Android 版本之间行为有所不同。

  • 在 Android 11 或更高版本中,每个覆盖层都有其自己的保留资源 ID 空间,该空间不与目标资源 ID 空间或其他覆盖层资源 ID 空间重叠,因此覆盖层引用其自身的资源可以按预期工作。

  • 在 Android 10 或更低版本中,覆盖层和目标软件包共享相同的资源 ID 空间,当它们尝试使用 @type/name 语法引用其自身的资源时,可能会导致冲突和意外行为。

启用/停用覆盖层

可以手动和以编程方式启用/停用覆盖层。

手动停用或启用覆盖层

要手动启用和验证 RRO,请运行

adb shell cmd overlay enable --user current com.example.carrro
adb shell cmd overlay list --user current | grep -i com.example com.example.carrro

这将为拥有 SystemUI 的系统用户 (userId = 0) 启用 RRO。此指令不影响前台用户 (userId = 10) 启动的应用。要为前台用户启用 RRO,请使用参数 -–user 10

adb shell cmd overlay enable --user 10 com.example.carrro

以编程方式启用或停用覆盖层

使用 OverlayManager API 启用和停用可变覆盖层(使用 Context#getSystemService(Context.OVERLAY_SERVICE) 检索 API 接口)。覆盖层只能由其目标软件包或具有 android.permission.CHANGE_OVERLAY_PACKAGES 权限的软件包启用。当覆盖层被启用或停用时,配置更改事件会传播到目标软件包,并且目标 Activity 会重新启动。

限制可覆盖资源

在 Android 10 或更高版本中,<overlayable> XML 标记公开了一组允许 RRO 覆盖的资源。在以下示例 res/values/overlayable.xml 文件中,string/foointeger/bar 是用于设置设备外观主题的资源;要覆盖这些资源,覆盖层必须按名称显式地以可覆盖资源集合为目标。

<!-- The collection of resources for theming the appearance of the device -->
<overlayable name="ThemeResources">
       <policy type="public">
               <item type="string" name="foo/" />
               <item type="integer" name="bar/" />
       </policy>
       ...
</overlayable>

一个 APK 可以定义多个 <overlayable> 标记,但每个标记在软件包中必须具有唯一的名称。例如,以下情况是可以接受的

  • 两个不同的软件包都定义 <overlayable name="foo"> 是可以接受的。

  • 一个 APK 具有两个 <overlayable name="foo"> 代码块是不可接受的。

以下代码显示了 AndroidManifest.xml 文件中覆盖层的示例。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
       package="com.my.theme.overlay">
       <application android:hasCode="false" />
       <!-- This overlay will override the ThemeResources resources -->
       <overlay android:targetPackage="android" android:targetName="ThemeResources">
</manifest>

当应用定义 <overlayable> 标记时,以该应用为目标的覆盖层

  • 必须指定 targetName

  • 只能覆盖 <overlayable> 标记中列出的资源。

  • 只能以一个 <overlayable> 名称为目标。

您无法启用以公开可覆盖资源但不使用 android:targetName 以特定 <overlayable> 标记为目标的软件包为目标的覆盖层。

限制政策

使用 <policy> 标记对可覆盖资源强制实施限制。type 属性指定覆盖层必须满足哪些政策才能覆盖包含的资源。支持的类型包括以下类型。

  • public。任何覆盖层都可以覆盖该资源。
  • system。系统分区上的任何覆盖层都可以覆盖资源。
  • vendor。供应商分区上的任何覆盖层都可以覆盖资源。
  • product。产品分区上的任何覆盖层都可以覆盖资源。
  • oem。oem 分区上的任何覆盖层都可以覆盖资源。
  • odm。odm 分区上的任何覆盖层都可以覆盖资源。
  • signature。任何使用与目标 APK 相同签名签名的覆盖层都可以覆盖资源。
  • actor。任何使用与执行程序 APK 相同签名签名的覆盖层都可以覆盖资源。执行程序在系统配置的 named-actor 标记中声明。
  • config_signature。任何使用与 overlay-config apk 相同签名签名的覆盖层都可以覆盖资源。overlay-config 在系统配置的 overlay-config-signature 标记中声明。

以下代码显示了 res/values/overlayable.xml 文件中 <policy> 标记的示例。

<overlayable name="ThemeResources">
   <policy type="vendor" >
       <item type="string" name="foo" />
   </policy>
   <policy type="product|signature"  >
       <item type="string" name="bar" />
       <item type="string" name="baz" />
   </policy>
</overlayable>

要指定多个政策,请使用竖线 (|) 作为分隔符。当指定多个政策时,覆盖层只需满足一个政策即可覆盖 <policy> 标记中列出的资源。

配置覆盖层

Android 支持不同的机制来配置覆盖层的可变性、默认状态和优先级,具体取决于 Android 版本。

  • 运行 Android 11 或更高版本的设备可以使用 OverlayConfig 文件 (config.xml) 而不是清单属性。使用覆盖层文件是覆盖层的推荐方法。

  • 所有设备都可以使用清单属性 (android:isStaticandroid:priority) 来配置静态 RRO。

使用 OverlayConfig

在 Android 11 或更高版本中,您可以使用 OverlayConfig 配置覆盖层的可变性、默认状态和优先级。要配置覆盖层,请创建或修改位于 partition/overlay/config/config.xml 的文件,其中 partition 是要配置的覆盖层所在的分区。要进行配置,覆盖层必须位于配置它的分区的 overlay/ 目录中。以下代码显示了一个示例 product/overlay/config/config.xml

<config>
    <merge path="OEM-common-rros-config.xml" />
    <overlay package="com.oem.overlay.device" mutable="false" enabled="true" />
    <overlay package="com.oem.green.theme" enabled="true" />
</config>"

<overlay> 标记需要一个 package 属性,用于指示正在配置哪个覆盖层软件包。可选的 enabled 属性控制是否默认启用覆盖层(默认为 false)。可选的 mutable 属性控制覆盖层是否可变,并且可以在运行时以编程方式更改其启用状态(默认为 true)。未在配置文件中列出的覆盖层是可变的,并且默认情况下处于停用状态。

覆盖层优先级

当多个覆盖层覆盖相同的资源时,覆盖层的顺序非常重要。覆盖层的优先级高于配置在其自身配置之前的覆盖层。不同分区中覆盖层的优先级顺序(从最低优先级到最高优先级)如下所示。

  • system
  • vendor
  • odm
  • oem
  • product
  • system_ext

合并文件

使用 <merge> 标记允许在配置文件的指定位置合并其他配置文件。标记的 path 属性表示要合并的文件相对于包含覆盖层配置文件的目录的路径。

使用清单属性/静态 RRO

在 Android 10 或更低版本中,覆盖层的不可变性和优先级是使用以下清单属性配置的。

  • android:isStatic。当此布尔属性的值设置为 true 时,默认情况下启用覆盖层并且覆盖层是不可变的,这会阻止覆盖层被停用。

  • android:priority。此数字属性(仅影响静态覆盖层)的值配置当多个静态覆盖层以相同的资源值为目标时覆盖层的优先级。数字越高表示优先级越高。

以下代码显示了一个示例 AndroidManifest.xml

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.overlay">
    <application android:hasCode="false" />
    <overlay android:targetPackage="com.example.target"
                   android:isStatic="true"
                   android:priority="5"/>
</manifest>

Android 11 中的变更

在 Android 11 或更高版本中,如果配置文件位于 partition/overlay/config/config.xml 中,则使用该文件配置覆盖层,并且 android:isStaticandroid:priority 对位于该分区中的覆盖层没有影响。在任何分区中定义覆盖层配置文件都会强制执行覆盖层分区优先级。

此外,Android 11 或更高版本移除了使用静态覆盖层影响在软件包安装期间读取的资源值的功能。对于使用静态覆盖层更改配置组件启用状态的布尔值的常见用例,请使用 <component-override> SystemConfig 标记(Android 11 中的新增功能)。

调试覆盖层

要手动启用、停用和转储覆盖层,请使用以下 overlay manager shell 命令。

adb shell cmd overlay

使用 enable 而不指定用户会影响当前用户,即拥有 System UI 的系统用户 (userId = 0)。这不会影响拥有应用的前台用户 (userId = 10)。要为前台用户启用 RRO,请使用参数 –-user 10

adb shell cmd overlay enable --user 10 com.example.carrro

OverlayManagerService 使用 idmap2 将目标软件包中的资源 ID 映射到覆盖层软件包中的资源 ID。生成的 ID 映射存储在 /data/resource-cache/ 中。如果您的覆盖层无法正常工作,请在 /data/resource-cache/ 中找到与您的覆盖层对应的 idmap 文件,然后运行以下命令。

adb shell idmap2 dump --idmap-path [file]

此命令会打印资源映射,如下所示。

[target res id] - > [overlay res id] [resource name]
0x01040151 -> 0x01050001 string/config_dozeComponent
0x01040152 -> 0x01050002 string/config_dozeDoubleTapSensorType
0x01040153 -> 0x01050003 string/config_dozeLongPressSensorType