Overlay 机制:RRO 与 SRO、主题定制、资源覆盖

资源覆盖,说白了就是让系统在运行时或编译时,把默认的资源文件替换成你想要的。嗯,这听起来很简单,但背后涉及到的机制——RRO 和 SRO,却是整个 Android 主题定制、多语言适配、甚至厂商定制 ROM 的基石。

我记得刚接触 AOSP 那会儿,为了给一个设备做定制 UI,我直接修改了 framework-res.apk 里的资源。结果呢?每次 OTA 升级都要手动合并,痛苦不堪。后来我才明白,Overlay 机制就是用来优雅解决这个问题的。

什么是 Overlay?为什么需要它?

Overlay 直译过来就是「覆盖层」。你可以把它想象成一张透明胶片,盖在原来的画上。胶片上画了什么,最终显示的就是什么。

在 Android 系统里,资源覆盖分为两种:

  • 编译时覆盖(SRO):在构建系统时直接替换资源,一锤子买卖。
  • 运行时覆盖(RRO):在系统运行时动态叠加资源,灵活切换。

为什么要搞这么复杂?你想想看,一个手机厂商要定制自己的主题,总不能去改 Google 的原始代码吧?那样升级一次崩一次。Overlay 机制允许你在不修改原始 APK 的前提下,优雅地替换资源。

核心思想: 资源覆盖 = 目标包 + 覆盖包。系统优先使用覆盖包中的资源,找不到时才回退到目标包。

SRO:静态资源覆盖

SRO(Static Resource Overlay)发生在编译阶段。说白了,就是在 make 的时候,把覆盖资源直接打包进目标 APK 里。

我在项目中遇到过这样一个场景:某款平板需要把默认的 Launcher 图标全部换成圆形。如果用 SRO,只需要在 device 目录下创建一个 overlay 文件夹,把新图标放进去,然后在 BoardConfig.mk 里声明一下:

# 在 BoardConfig.mk 中
DEVICE_PACKAGE_OVERLAYS := device/xxx/overlay

编译系统会自动扫描这个目录,找到与目标包同名的资源文件,然后覆盖掉原始资源。

SRO 的特点:

  • 性能最好,因为编译时就已经确定
  • 无法动态切换,改完必须重新编译
  • 适合厂商定制、多产品线差异化

注意: SRO 覆盖的是整个资源文件,不是单个条目。比如你要改一个 strings.xml 里的某个字符串,必须提供完整的 strings.xml 文件,而不是只写那一个条目。

RRO:运行时资源覆盖

RRO(Runtime Resource Overlay)是 Android 8.0 之后主推的方式。它允许你在系统运行时动态加载资源覆盖包,不需要重新编译整个系统。

我曾经踩过一个坑:在 Android 7.1 上做主题切换,用的是老式的 ResourceManager 替换方式,结果内存泄漏严重。后来升级到 RRO,一切都清爽了。

RRO 的工作原理是这样的:

  1. 创建一个独立的 APK,只包含资源文件,没有代码
  2. 在 AndroidManifest.xml 中声明 <overlay> 标签
  3. 系统服务 OverlayManagerService 负责管理这些覆盖包
  4. 应用运行时,Resources 对象会优先查找覆盖包中的资源

来看一个典型的 RRO 覆盖包 AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.overlay">
    <overlay
        android:targetPackage="com.android.systemui"
        android:targetName="SystemUIResources"
        android:priority="1" />
    <application android:hasCode="false" />
</manifest>

这里的关键属性:

  • targetPackage:要覆盖的目标应用包名
  • targetName:覆盖的目标名称,用于区分多个覆盖包
  • priority:优先级,数字越大优先级越高

RRO 与 SRO 的对比

特性 SRO RRO
生效时机 编译时 运行时
动态切换 不支持 支持(需重启目标应用)
性能开销 略有(资源查找多一层)
适用场景 厂商固件定制 主题商店、动态换肤
调试难度 中(需检查 overlay 是否启用)

主题定制的实战套路

说到主题定制,RRO 是绝对的主角。我参与过的一个项目,需要支持用户从主题商店下载并应用主题,核心流程是这样的:

  1. 主题 APK 作为 RRO 包安装到系统
  2. OverlayManagerService 注册该覆盖包
  3. 用户选择主题时,调用 OverlayManager.setEnabled() 启用对应覆盖包
  4. 系统广播 ACTION_OVERLAY_CHANGED,应用收到后刷新界面

代码层面,启用一个 RRO 覆盖包很简单:

// 需要系统权限
OverlayManager om = getSystemService(OverlayManager.class);
om.setEnabled("com.example.theme.blue", true, UserHandle.CURRENT);

小技巧: 调试 RRO 时,可以用 cmd overlay list 命令查看当前所有覆盖包的状态。如果发现覆盖没生效,先检查这个命令的输出,看看你的包是不是处于 "enabled" 状态。

资源覆盖的优先级规则

当多个覆盖包同时存在时,系统按照以下优先级查找资源:

  1. 目标应用自身的资源(原始资源)
  2. 优先级最高的 RRO 包中的资源
  3. 次高优先级的 RRO 包中的资源
  4. 以此类推,直到找到为止

如果两个 RRO 包优先级相同,则按包名排序,字母序靠前的优先。

我曾经遇到过一个诡异的问题:某个主题的颜色总是显示不对。查了半天,发现是系统内置了一个优先级更高的 RRO 包,把颜色覆盖了。嗯,从那以后我养成了习惯,每次调试先 cmd overlay list 看一眼。

SVG 流程图:Overlay 资源查找过程

Overlay 资源查找流程 应用请求资源 有 RRO 覆盖? (已启用) 查找 RRO 包 按优先级排序 找到资源? 返回覆盖值 查找目标 APK 自身资源 返回最终资源 目标包资源 RRO 覆盖包 查找结果

避坑指南

做 Overlay 开发这些年,我踩过的坑不少,挑几个典型的说说:

  • 资源 ID 冲突:RRO 包中的资源 ID 必须与目标包一致。如果你在覆盖包里新增了一个资源,目标包里没有对应的 ID,那这个资源永远不会被用到。
  • 类型不匹配:不能用 color 覆盖 dimen,也不能用 string 覆盖 drawable。系统会直接忽略类型不匹配的覆盖。
  • 权限问题:RRO 包需要 android.permission.CHANGE_OVERLAY_PACKAGES 权限,而且只有系统应用才能声明 <overlay> 标签。
  • 缓存问题:启用或禁用 RRO 后,目标应用需要重启才能看到效果。有些应用会监听 ACTION_OVERLAY_CHANGED 广播,但并不是所有应用都这么做。

曾经踩过的坑: 我在 Android 9 上做过一个主题,覆盖了 framework-res 里的颜色资源。结果发现某些第三方应用的颜色没变。查了半天才发现,那些应用用了自己的资源缓存,根本不走 framework 的资源查找路径。解决方案是让主题覆盖到具体应用的包名上,而不是 framework-res。

总结

Overlay 机制是 Android 系统资源管理的精髓之一。SRO 适合厂商固件层面的定制,RRO 则撑起了整个主题生态。理解这两者的区别和适用场景,能让你在做定制 ROM 或主题开发时少走很多弯路。

我个人习惯是:能上 RRO 就上 RRO,除非是那些永远不变的系统级资源(比如开机动画、默认壁纸),才考虑用 SRO。毕竟,动态切换带来的灵活性,在今天的 Android 生态里太重要了。

公众号:蓝海资料掘金营,微信 deep3321