【车载 Android】应用换肤方案(一) - Runtime Resource Overlay

3,437 阅读16分钟

前言

手机应用的动态换肤功能目前已经使用的比较广泛了,也有许多文章分析了其中的原理,使用方式也大同小异,基本都需要在应用内集成一个三方的框架,或独立开发一个换肤框架。此类换肤方式需要长期维护一套换肤框架,对原始应用存在一定的侵入性,开发的复杂度、工作量都会比较大。车载应用出于稳定性的考虑,对于引入第三方框架会比较克制,所以目前车载系统应用少见有直接采用手机应用换肤方案的(当然也不是没有,博主也做过)。

那么车载应用要如何在降低复杂度、工作量的前提下实现应用换肤呢?

其实也不难,因为 Android 系统本身已经提供了一套动态资源替换机制(Runtime Resource Overlay),灵活运用动态资源替换就可以实现应用换肤的功能,那么本篇就来介绍一下这个 Runtime Resource Overlay 是如何使用的。

Runtime Resource Overlay 简介

Runtime Resource Overlay(RRO)是一种在运行时更改目标应用所使用资源值的机制。例如,我们可以将安装在系统镜像上的应用程序的资源值,动态修改为另一个资源包中定义的资源值,到达修改应用程序更改其界面或行为的目的。

安装在不同分区上的 RRO 可以在运行时更改应用程序资源的值,而不需要在构建时应用对资源值进行硬编码。不过RRO只可以替换res目录下的资源,assert目录下的资源不属于资源框架无法动态替换。RRO机制目前主要应用场景是厂商定制系统级的主题切换功能。

RRO工作原理简单来说,就是将资源包中定义的资源映射到应用层定义的资源上。当应用尝试解析应用资源值时,系统转而会返回资源包中的资源值。

RRO 换肤实践

Demo 请访问github仓库:github.com/linxu-link/…

首先准备一个需要被换肤的应用,这里我们随便新建一个APP为例,下面我们将新建的Target App工程称为『目标应用』,目标是利用RRO机制替换目标应用中颜色值。

第 1 步:新建一个APP工程,配置 AndroidManifest.xml

首先我们要新建一个独立的 app 工程,这个应用存在的意义就是放置『目标应用』的资源文件,下面我们将其称为『资源包』。它虽然是一个应用工程,但是不包含逻辑代码,只能用于存放资源文件。

注意到这一步,我们实际上新建了两个app工程,一个是目标应用,一个是资源包。

  • 【必需设定】android:targetPackage 用于指明 RRO 想要替换的『目标应用』。
  • 【必需设定】android:hasCode必须设定为 false。由于无法替换代码,因此 RRO 无法使用 DEX 文件。
  • 【非必需设定】android:targetName 用于指明 RRO 『目标应用』的可替换资源子集的名称。如果『目标应用』没有定义可替换资源集,此属性就不需要设定。
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.target.overlay">

    <overlay android:targetPackage="com.android.target" />

    <application android:hasCode="false"/>

</manifest>

如果某个APK 的AndroidManifest.xml中包含 <overlay> 标记作为 <manifest> 标记的子项,该APK将被视为『资源包』 。

第 2 步:定义资源映射

Android 10或以下版本

在 Android 10 或更低版本中,系统是根据资源的名称进行资源替换,所以我们只需要在资源包中将需要替换的资源定义好即可。

Android 11或以上版本

在 Android 11 或更高版本中,Google推荐在『资源包』的res/xml 目录中创建一个文件,枚举出应覆盖的『目标应用』的资源值及其替换值。

<?xml version="1.0" encoding="utf-8"?>
<overlay xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 更换颜色 -->
    <item target="color/purple_200" value="#000000"/>
    <item target="color/purple_500" value="#000000"/>
    <item target="color/purple_700" value="#000000"/>
    <item target="color/teal_200" value="#000000"/>
    <item target="color/teal_700" value="#000000"/>
    <item target="color/black" value="#000000"/>
    <item target="color/white" value="#000000"/>
</overlay>

注意,target标签中的color并没有带上@标记,他实际上仅仅是一个字符串而不是引用

然后在资源包的AndroidManifest.xml 中将android:resourcesMap 属性的值设置为资源映射文件。

<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:resourcesMap="@xml/overlays"/>
</manifest>

第 3 步:构建资源包

如果目标应用未做限制,构建的资源包即使与目标应用的签名不一致,也可以生效。如何限制使用统一签名才可以生效的方法,请参考 第5节-限制可替换资源

构建出的资源包,我们在调试时,可以使用Android Studio直接安装,如下所示。

由于『资源包』(resource_ten)没有activity所以也无法启动,我们只需看到data/app目录下又多了一个路径,即表示安装成功。『目标应用』(target)也是同样的安装方法。

正式发布时,『资源包』可以发布到vendor/overlay目录下,『目标应用』则可以根据需要可以灵活放置。之后重启Android系统即可。

第 4 步:启用/停用 RRO

使用OverlayManager提供的API可以启用、停用RRO。OverlayManager在公开的Android SDK中并没有提供,如果是使用Gradle构建工程,需要额外使用AOSP源码编译一个framework.jar并引入才可以使用,同时应用签名也需要使用Android系统签名,让应用成为系统级应用。

framework.jar和系统签名文件,可以访问GitHub仓库获取github.com/linxu-link/…

OverlayManager manager = MainActivity.this.getSystemService(OverlayManager.class);
OverlayInfo overlayInfo = manager.getOverlayInfo("com.android.target.overlay.ten", UserHandle.CURRENT_OR_SELF);
manager.setEnabled("com.android.target.overlay.ten", !overlayInfo.isEnabled(), UserHandle.CURRENT_OR_SELF);

在启用或停用RRO后,配置更改事件会传播到目标应用,并且目标应用的 activity 会重新启动,如下所示:

也可以使用下面的adb指令来开启、关闭RRO。

adb shell cmd overlay enable [com.android.target.overlay.ten] // 开启
adb shell cmd overlay disable [com.android.target.overlay.ten] // 关闭

如果目标应用或RRO资源为正在查询的资源定义了多个配置,RRO将返回与设备配置最匹配的配置值。 如需确定最匹配的配置,请将叠加层资源配置集合并到目标资源配置集中,然后遵循常规资源流程执行操作,如需了解详情,请参阅 Android 如何寻找最匹配的资源

以上就是 RRO 简单地使用方式,不过实际开发时我们一定会遇到各种各样的问题,接下来我们先来讲讲调试RRO时排查问题的一般步骤。

RRO 故障排查

Android系统提供了丰富的指令用于排查RRO的各种错误与异常,如果你的 RRO 没有按照预期那样正常运行,可以按照下面列出的步骤进行排查。

注意事项

1)在Android 12上『目标应用』的AndroidManifest.xml中不能有android:sharedUserId=""标签,否则启用RRO时『目标应用』的 activity 将不会重启,需要自行重启『目标应用』资源替换才能生效。

sharedUserId 标签实际上在Android 10以后已经被标记为过时,具体原因可以参考官方文档:developer.android.com/guide/topic…

2)在 Android 10或更低的版本上使用OverlayManager的应用,需要声明android.permission.CHANGE_OVERLAY_PACKAGES权限,Android 11之后不必声明此权限。

第 1 步:列出 RRO

adb shell cmd overlay list --user current

在这一步骤中,可查看当前系统所有 RRO 『资源包』,如果上述指令没有展示出你的『资源包』,则说明该『资源包』未被正确安装。

包名前的符号表示 RRO 的状态,如下所示:

指标复制权状态
[ ]已安装并处于待激活状态
[X]安装并处于激活状态
---已安装但包含错误

第 2 步:启用和停用 RRO

使用adb 指令启用(或禁用)RRO:

adb shell cmd overlay [enable/disable] --user current [资源包 package name]

可以将 current 替换为你需要的用户。例如,可以将其替换为 010,也可以保持原值不变。

有的时候我们使用IOverlayManagerOverlayManager开启RRO可能并不会生效,这时就可以使用adb指令启用RRO,如果adb指令可以生效,那么基本可以认为是我们对于OverlayManager的用法出现了错误,有关OverlayManager的用法请参考第五节 - RRO进阶

第 3 步:确认『资源包』状态

我们需要确保『资源包』被overlayManager服务正确识别,可以在shell中执行 dumpsys overlay,查看对应资源包的相关信息。

adb shell cmd overlay dump [资源包 package name]

  1. 检查安装了 RRO 的用户,即mUserId

  2. 检查mState的值:

    1. STATE_ENABLEDSTATE_ENABLED_IMMUTABLE :RRO 已启用并应用于目标应用。

    2. STATE_MISSING_TARGET :目标应用未安装。

    3. STATE_NO_IDMAP :AndroidManifest.xml 、 overlays.xml 或 overlayable.xml文件的设置方式有问题。使用adb logcat运行日志并搜索关键字idmap以识别错误,第4、5步会解释。

    4. STATE_UNKNOWN :OverlayManagerService 出现异常

第 4 步:检查『资源包』的AndroidManifest.xml

  1. 检查targetNametargetPackageandroid:targetName应该与目标应用程序中定义的可覆盖组具有相同的值。只有在定位覆盖层时才需要这样做。android:targetPackage始终是必需的,应包含目标应用程序的包名称。

  2. 检查 RRO 是否是静态的。默认情况下,静态RRO在系统启动时被启用,而动态RRO,则默认不启用。

  3. 检查静态 RRO 的优先级(动态 RRO 优先级始终设置为Integer.MAX_VALUE ,它们的应用顺序取决于它们的启用时间)。

  4. 多个 RRO 可能适用于同一目标。最后启动的应用具有更高的优先级。在 0 到 10 的范围内,10 最高,0 最低。

第 5 步:检查 overlays.xml

此项仅适用于 Android 11(及更高版本)

  1. 检查overlays.xml。确认打算替换的所有资源都在此文件中有定义。
<overlay>
    <item target="string/app_name" value="@string/overlaid_app_name" />
</overlay>
  1. 必须确保:
  • 目标应用程序中存在名称为app_namestring资源。

  • 名称为overlaid_app_namestring资源存在于资源包中。

  1. 如果目标应用有一个overlayable.xml文件,请确保该文件中包含app_name 。确保在AndroidManifest.xml文件中使用正确的targetName

如果要替换layout文件,请确保所有 ID 和应用命名空间属性都包含 overlays.xmloverlayable.xml中。例如:

<overlay>
<item target="layout/car_ui_base_layout_toolbar" value="@layout/car_ui_base_layout_toolbar" />
<item target="id/car_ui_toolbar_background" value="@id/car_ui_toolbar_background" />
<item target="attr/layout_constraintTop_toBottomOf" value="@attr/layout_constraintTop_toBottomOf" />
</overlay>

overlayable标签的使用方式,请继续阅读第5节-限制可替换资源

第 6 步:转储 idmap

进入到这一步之前,调试RRO时的所有问题都应该已经解决了,接下来转储 RRO 的idmap以了解资源是如何被解析的,以及它解析出的资源值为何与预期不同的原因。

1)在设备上找到idmap的路径。

adb shell
su
ls data/resource-cache

2)要转储该文件的内容。

idmap2 dump --idmap-path [path to your RRO idmap file]// 根目录执行要带上绝对路径 data/resource-cache

输出类似于以下内容,会显示 RRO 中的id映射到目标中的id,以及替换资源的名称。

RRO 进阶

常用 API

在第三节中,我们使用OverlayManager的 API 来启用、停用 RRO,但是实际上OverlayManager提供的 API 功能太少,实际开发中我们推荐使用功能更加丰富的IOverlayManager

IOverlayManager manger = IOverlayManager.Stub.asInterface(ServiceManager.getService(Context.OVERLAY_SERVICE));

IOverlayManager 提供了以下功能

  • getAllOverlays
Map<String, List<OverlayInfo>> allOverlays = overlayManager.getAllOverlays(userId);

返回有关指定用户的所有已安装资源包的OverlayInfo。如果没有为该用户安装资源包,则返回空映射(即从不返回null)。

返回的映射是目标应用包名-资源包列表的映射。给定目标包的每个列表都按优先级顺序排序,列表末尾具有最高优先级的覆盖层。

可以通过 UserHandle.myUserId() 的方式获取当前应用的userId。

  • getDefaultOverlayPackages
String[] overlayPackages = overlayManager.getDefaultOverlayPackages();

返回默认资源包的列表,如果没有,则返回空数组。

  • getOverlayInfo
OverlayInfo overlayInfo = overlayManager.getOverlayInfo(packageName, userId);

返回有关具有指定用户的给定包名称的OverlayInfo

  • getOverlayInfosForTarget
List<OverlayInfo> overlayInfosForTarget = overlayManager.getOverlayInfosForTarget(targetPackageName, userId);

返回指定用户的给定目标包的所有OverlayInfo。返回的列表根据列表末尾具有最高优先级的覆盖优先级排序。

  • setEnabled
boolean enabled = overlayManager.setEnabled(packageName, enbale, userId);

启用或禁用RRO。该方法始终可以禁用RRO,但由于技术和安全原因,可能不总是可以启用RRO,例如:没有安装相关的目标应用。

一般setEnable方法我们总是用于禁用RRO,而启动RRO会使用setEnabledExclusive或setEnabledExclusiveInCategory

  • setEnabledExclusive
boolean enable = overlayManager.setEnabledExclusive(packageName, enable, userId);

请求启用RRO,并禁用具有相同目标应用的任何其他资源包。

  • setEnabledExclusiveInCategory
boolean enabled = overlayManager.setEnabledExclusiveInCategory(packageName, userId);

请求启用RRO,并禁用具有相同目标包和类别的任何其他资源包。


以下内容出自官方文档:source.android.com/docs/core/r…

限制可替换资源

在 Android 10 或更高版本中,『目标应用』可以使用<overlayable> 标签公开一组允许 RRO 替换的资源,未被公开的资源,则不允许 RRO 替换。

在以下的示例 res/values/overlayable.xml 文件中,string/foointeger/bar 是用于为设备的外观设置主题背景的资源,为了替换这些资源,『资源包』必须根据名称明确定位到可替换资源的集合。

<resources>
    <overlayable name="ThemeResources">
        <policy type="public">
            <item type="color" name="purple_200" />
            <item type="color" name="purple_500" />
        </policy>
    </overlayable>
</resources>

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

  • 两个不同的资源包可以同时定义 <overlayable name="purple_200">
  • 一个 APK 不能具有两个 <overlayable name="purple_200"> 块。

然后需要在资源包的AndroidManifest.xml中使用targetName指定ThemeResources。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.target.overlay.ten">

    <overlay
        android:targetName="ThemeResources"
        android:targetPackage="com.android.target" />

    <application android:hasCode="false"/>

</manifest>

当应用定义 <overlayable> 标记时,定位到该应用的叠加层需满足以下条件:

  • 必须指定 targetName

  • 只能替换 <overlayable> 标记中列出的资源。

  • 只能定位到一个 <overlayable> 名称。

叠加层的限制策略

使用<policy>标记可以在目标应用中对可替换资源施加限制。type属性指定叠加层必须满足哪些政策的要求才能替换包含的资源。支持以下类型:

  • public:任何叠加层均可替换相应资源。
  • system:系统分区上的任何叠加层均可替换相应资源。
  • vendor:vendor 分区上的任何叠加层均可替换相应资源。
  • product:product 分区上的任何叠加层均可替换相应资源。
  • signature:使用与目标 APK 相同的签名进行签名的任何叠加层均可替换相应资源。
<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。

注意:只有定位到 android 软件包的不可变叠加层才会影响通过 Resources.getSystem() 检索的资源值。

使用 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
  • oem
  • odm
  • product
  • system_ext

注意:当 OverlayManagerService 启用未配置的叠加层时,系统不会定义叠加层的顺序。

使用 <merge> 标记可将位于指定位置的其他配置文件合并到配置文件中。该标记的 path 属性表示要合并的文件相对于目录(包含叠加层配置文件)的路径。

配置静态 RRO(SRO)

静态RRO是在系统启动后自动生效,不需要再使用OverlayManager启用。

Android 10 或以下版本

使用以下清单属性配置『资源包』的不可变性和优先级。

  • android:isStatic。当此布尔值属性的值设置为 true 时,叠加层会默认处于启用状态并且不可变,这会导致叠加层无法停用。
  • android:priority。当多个静态叠加层以相同的资源值为替换目标时,此数字属性的值(仅影响静态叠加层)将配置叠加层的优先级。数值越大表示优先级越高。
<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 的新变化)。

总结

以上就是有关Runtime Resource Overlay的所有介绍了,可以看出RRO本质上是一套动态资源替换机制,应用换肤只是它功能的一小部分而已。总结一下,RRO有以下两个主要的优点:

  • 功能强大,可以替换多种资源类型;

  • 对目标应用无任何侵入,不需要集成换肤框架,省去了维护框架的成本;

但是RRO也有一个缺点,那就是更换资源时,目标应用的activity会发生重启,当下Android APP开发普遍不喜欢考虑处理activity重启的情况,这时就有可能产生bug需要处理。

总得来说,使用RRO进行换肤是一套利大于弊的方案,博主之前的公司已经在实际的项目中落地并量产。但是RRO并不是万能的,实际开发中也有可能出现一些意想不到的情况,这时候就需要我们再引入其它的换肤方案,以后再继续介绍应用的其它换肤方案与原理。

好了,感谢你的阅读,希望对你有所帮助。

参考资料:

source.android.com/docs/device…

source.android.com/docs/core/r…

source.android.com/docs/device…

www.jianshu.com/p/398f1beb1…