Android换肤方案

2,377 阅读5分钟

一、Android Overlay机制

developer.aliyun.com/article/126…

Android Overlay是一种资源替换机制,它能在不重新打包apk的情况下,实现资源文件的替换(res目录非assert目录),Overlay又分为静态Overlay(Static Resource Overlay)与运行时Overlay(Runtime Resource Overlay)。

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

配置步骤

  1. 创建一个只包含res的项目(资源包)

  1. 配置AndroidManifest.xml

package:资源包包名,可以任意命名,一般皮肤主题或者overlay为后缀

android:hasCode="false":必须设定为 false。由于无法替换代码,因此 RRO 无法使用 DEX 文件

android:isStatic="false":必须设定为 false才能在多套资源之间实现动态切换

android:priority:设置资源包的优先级,值越大,优先级越高,用于存在多个overlay.apk情况下的判断

android:resourcesMap:Android 11及以上新增配置,用于指定资源映射关系

android:targetPackage:目标apk包名

  1. 定义资源映射

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

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

注意,target标签中的color并没有带上@标记,它实际上仅仅是一个字符串而不是引用,系统通过该字符串在目标应用中查找对应资源类型和资源名。

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

  1. 限制可替换资源

在目标应用中创建res/values/overlayable.xml文件,该文件中指定了能被覆盖的资源

  • 一个应用可以有多个overlayable标记,但不能重名,比如上面的"OverlayableResource_1"和"OverlayableResource_2";多个overlayable可以定义相同的可覆盖资源;
  • 当应用定义<overlayable> 标记时,资源包只能覆盖列出的资源;
  • 资源包必须在AndroidManifest中指定android:targetName,指定目标应用中的overlayable,只能指定一个overlayable
  1. 限制策略

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

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

如需指定多个策略,请使用竖线 | 作为分隔符。 如果指定了多个策略,资源包只需满足一个政策的要求即可替换 <policy> 标记中列出的资源。

  1. 构建资源包

使用Android Studio直接安装或者只用adb install [RRO apk]。正式发布时,资源包可以发布到vendor/overlay目录下,目标应用则可以根据需要可以灵活放置。之后重启Android系统即可。

调试资源包

使用代码动态切换

注意事项

  • 使用 OverlayManager API 启用和停用可变叠加层(使用 Context#getSystemService(Context.OVERLAY_SERVICE) 检索 API 接口)。叠加层只能由其目标软件包或具有 android.permission.CHANGE_OVERLAY_PACKAGES 权限的软件包启用。在您启用或停用叠加层后,配置更改事件会传播到目标软件包,并且目标 activity 会重新启动
  • 在Android 12上目标应用的AndroidManifest.xml中不能有android:sharedUserId=""标签,否则启用RRO时目标应用的 activity 将不会重启,需要自行重启目标应用资源替换才能生效。

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

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

二、第三方换肤方案

Android-skin-support Android-Skin-Loader

Android中我们使用Resource获取res目录下的资源,比如

换肤无非就是替换这些资源,以getString为例,最终会走到getText

Resource通过AssetManager获取资源,AssetManager提供访问原始资源文件的接口,比如res、assets目录,一般我们就是通过Resource间接调用这些接口。也就是说,如果想实现换肤, 核心 就是换 AssetManager

经此替换,getColor()getString()getDrawable()等接口拿到的就是皮肤包的资源。

三方换肤的两种思路

  1. 完整版:

自定义LayoutInflater.Factory2,在onCreateView()中将sdk中的view替换成自定义支持换肤的view,

将资源Resource对象注入到自定义view中,对原方法(比如setBackground()setTextColor())进行拦截

  1. 简约版:

不对view进行替换,仅对需要支持换肤的属性进行替换,

在创建view的时候解析view支持换肤的属性并缓存,在切换皮肤的时候通过view的对应方法(比如setBackground()setTextColor())实现动态切换资源。

三、Android里的View是怎么解析的?

以AppCompatActivity为例,它通过AppCompatDelegate代理来处理setContentView()和所有的生命周期事件,

重点看看LayoutInflater是如何把布局xml中的节点解析成View的,

通过自定义 Factory2 拦截系统View的创建过程,替换成自定义支持换肤的View, Android-skin-support 就是这么做的。