Android资源Overlay机制总结

1. 概述

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

2. 静态Overlay

静态Overlay,简称为SRO,发生在编译时,需要在Android系统源码环境中进行配置。SRO在网上介绍也比较多,配置起来也比较简单,在这里不再赘述,有兴趣可以看下源码的Demo和以下的链接。

3. 运行时Overlay

运行时Overlay,简称RRO,顾名思义,该机制的资源替换发生在运行时。RRO是在Android5.0后引入的,它能在 apk 运行时,自动加载需要定制的资源,而不加载原有的资源。
RRO机制的主要应用场景是厂商定制系统级的主题切换功能。但是理论上来说,第三方应用也是可以利用RRO实现动态切换主题功能的,前提是需要有相应的权限并且可以获取到OverlayManagerService的Binder实例。

3.1 与SRO的区别

  1. RRO能直接定制替换第三方APK的资源,而不需要其源码。SRO则需要对应APK的源码才能完成,一般而言,第三方是不会提供项目源码的。
  2. RRO的编译结果会得到一个xxx_overlay.apk,加上原项目的apk,总共会有2个apk,而SRO最终只会得到一个已经完成资源替换的apk。得到的overlay.apk可以视为一个正常的apk,因为它能被安装,含有自己的AndroidManifest.xml文件,当然正常下,overlay.apk是不含有执行代码的。
  3. RRO不能替换AndroidManifest.xml文件及reference resource类型的文件,如layout、anim、xml目录中的xml文件。虽然RRO具有自己的AndroidManifest.xml文件,但它却不能替换源项目中的AndroidManifest.xml文件。关于layout目录中的xml文件,SRO是可以替换的。
//以下几种,就属于reference resource
Resources resources = getResources();
XmlResourceParser parser0 = resources.getXml(R.xml.test);
XmlResourceParser parser1 = resources.getLayout(R.layout.activity_main);
XmlResourceParser parser2 = resources.getAnimation(R.anim.test);

对于Android的resource文件类型,可以看官网的介绍:
https://developer.android.com/guide/topics/resources/available-resources.html

源码(Android 7.0+)中的SystemUI的黑暗模式也是使用了RRO机制,制作主题包的方式可以参考该工程。源码路径:
frameworks/base/packages/overlays/SysuiDarkThemeOverlay/

3.2 配置步骤

下面以创建Launcher3的RRO为例进行说明如何制作主题包apk。

  1. 创建一个新项目,包名命名为com.android.launcher3.overlay,事实上包名可以随意命名,这样命名可读性高,一看包名就知道是哪个项目的overlay。
  2. 编辑overlay项目的AndroidManifest.xml文件,文件内容如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.launcher3.overlay"> 
    <overlay android:targetPackage="com.android.launcher3"
    android:priority="1"
    android:isStatic="false"/> 

    <application
        android:hasCode="false" />
</manifest>

其中,

  • package: 此overlay APK的包名
  • android:targetPackage:需要overlay的项目的包名
  • android:priority:设置overlay.apk的优先级,值越大,优先级越高,用于存在多个overlay.apk情况下的判断。
  • android:isStatic="false": 设置该属性,才能RRO。但是如果想在多套主题之间实现动态切换,这个属性不能设置为true
  • android:hasCode="false":表明此Module是否有源代码
  1. 梳理需要替换的res资源

overlay可以替换的资源包括但不限于以下几类。
android:textColor="#FFFFFFFF"android:text="标题"这种hard code的写法是无法替换的,所以建议把资源单独定义再引用。
我们可以将Android资源类型分为三类:

  • 列表类型的xml资源文件,该类型的文件内部包含了多个资源项,典型的就是strings.xml,很多项目会有config.xml也大都是这种类型的文件。
  • 值类型的xml文件,该类型的一个文件对应了一个资源项,比如layout目录下的布局文件,以及某些项目menu目录下的xml文件也大都是这种类型。
  • 数据型资源文件,比如图片,音频,视频以及其他数据类型的文件,该类型的一个文件对应一个资源项。
    在最终的安装包中第三种数据类型的资源文件会原封不动的保留。第二种值类型的xml资源文件,也会按照原有的目录结构和文件名字放入安装包中, 只是原有的xml格式会被压缩成特定的二进制流。列表型xml资源文件的内容会被全部编译到resources.arsc中。
  • 关于Android资源系统的相关介绍,可以查看官方文档:
    https://developer.android.com/guide/topics/resources/available-resources.html
    https://developer.android.com/guide/topics/resources/index.html
  1. 编写mk文件(或build.gradle文件),编译、打包、签名,并将生成的overlay.apk输出到/vendor/overlay(Android O加强了安全方面的限制,实际目录会有所变化)目录下,其中签名需要与源项目签名一致,否则不会生效。实践中发现,编译出来的overlay APK,不需要放到固定目录,采用adb install的方式安装,然后重启,也是可以的。
打包APK的方法:
  • 在源码环境下使用Android.mk文件然后进行模块化编译,类似的Android.mk内容参考如下:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_RRO_THEME := Launcher3Overlay
LOCAL_MODULE_TAGS := optional
LOCAL_SRC_FILES := $(call all-java-files-under,res)
LOCAL_SDK_VERSION = current
LOCAL_PACKAGE_NAME := Launcher3
LOCAL_CERTIFICATE := platform
LOCAL_MODULE_PATH := $(PRODUCT_OUT)/vendor/overlay/
include $(BUILD_PACKAGE)
  • 使用aapt命令打包,然后签名,必须要签名,否则打包生成的apk是无法安装的。

这一步不一定要手动打包&签名,LOCAL_CERTIFICATE := platform 可以通过这个变量为overlay apk指定与targetPackage一致的签名

打包命令:
aapt p -f -I [ANDROID_SDK_PATH]/android.jar \
    -S [OVERLAY_PATH]/res \
    -M [OVERLAY_PATH]/AndroidManifest.xml \
    -c [PRODUCT_AAPT_CONFIG] \
    -F [MODULE_NAME]-overlay.apk
签名命令:
java -jar [PATH]/signapk.jar \
    [PATH]/platform.x509.pem \
    [PATH]/platform.pk8 \
    [MODULE_NAME]-overlay.apk [MODULE_NAME]-overlay.apk_signed

注意:

  1. 把app放到vendor/overlay下面以后,还要activate,默认是disable的。
    activate有两种方式:
    • 一种是overlay的app的清单文件中,overlay标签设置 isStatic="true"通过这种方式设置是无法动态切换的
    • 一种是加系统属性ro.boot.vendor.overlay.theme=overlay_packagename,多个主题包时用分号分割包名

以上是网上的一些说明,实践中(Android8.0)发现打包生成的overlay apk,通过adb install的方式安装然后重启,也是可以生效的。原因在于开机启动之后,PMS会对已安装的overlay apk进行资源映射,使其与targetPackage联系起来。

  1. OverlayApk与TargetApk需要签名一致
  2. OverlayApk与TargetApk必须保持统一的资源ID,系统或应用或其他自定义的资源ID
  • 如果你正在开发动态主题更换,则发起主题更换的应用需要申请 CHANGE_OVERLAY_PACKAGES 权限,或者该应用为system或root
    android.permission.CHANGE_OVERLAY_PACKAGES
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.launcher3">
    <uses-permission
        android:name="android.permission.CHANGE_OVERLAY_PACKAGES">
    </uses-permission>
  • 通过Android Studio来打包生成overlay.apk也是可行的,主要在build.gradle中加上以下配置。其它则像正常apk配置即可。
android {
    sourceSets {
        main {
            manifest.srcFile 'src/main/AndroidManifest.xml'
            resources.srcDirs = ['src/main/res']
            res.srcDirs = ['src/main/res']
        }
    }
}

3.3 常见问题

  1. 关于overlay目录的问题

有文档中也提到需要把编译出来的overlay APK放到/verdor/overlay/或者/system/vendor/overlay/目录中。实际调试结果是直接push到该目录,并没有生效。原因是建立资源的映射是由installd来发起的,所以只通过adb install,触发安装流程就可以。

  1. idmap
    idmap的description,英文水平有限就不翻译了,自己看吧。
Idmap files play an integral part in the runtime resource overlay framework. An idmap 
file contains a mapping of resource identifiers between overlay package and its target 
package; this mapping is used during resource lookup. Idmap files also act as control 
files by their existence: if not present, the corresponding overlay package is ignored 
when the resource context is created. 

Idmap files are stored in /data/resource-cache. For each pair (target package, overlay 
package), there exists exactly one idmap file, or none if the overlay should not be used. 

安装overlay APK之后,会在/data/resource-cache/目录生成一个idmap文件,如:data@app@com.android.launcher3.overlay-l_6QRyYd76m32DoDg7ZW9w\=\=@base.apk@idmap。

想知道overlay了哪些资源,可以输入idmap --inspect idmap文件名来查看。详细的idmap使用方法,可以通过idmap --help查看。

4. Overlay机制原理

4.1 资源ID

我们知道资源文件在打包之后,会在R.java文件中生成一个对应的int类型资源id,这个id遵循以下的规则:

0xppttiiii

其中,

  • pp:01表示是系统资源,7f表示是应用资源
  • tt:用于表示资源类型,如string、array、图片资源等
  • iiii:用于表示相同类型的不同资源

4.2 原理

以我们常见的图片资源访问为例,当我们在应用层调用getDrawable方法的时候,会调用系统的getResource方法,该方法会根据资源id及设备配置信息(语言、像素密度等)来查找并返回合适的图片资源。此处,我们只关注根据id来选择资源。

如果id表明是系统资源,那么就会去framework-res.apk中查找并返回资源,一般位于/system/framework目录下,可root手机查看;如果是应用资源则在overlay和app中查找并返回。

image
单个overlay包

当对应项目只有一个overlay包时,那么查找资源时,会先从overlay.apk中进行查找,查找成功直接返回,反之则从app中查找。

多个overlay包

当对应项目具有多个overlay包时,会按照overlay包的优先级从高到底依次进行查找,如果overlay包都查找失败,才会到app中进行查找。
以访问R.drawable.ic_01为例,会先从overlay1包先查找,因为其优先级最高,查找到则直接返回。
如果访问R.drawable.ic_02,先从overlay1查找,查找失败,接着从overlay2中查找,查找到直接返回。
如果访问R.drawable.ic_03,依次从overlay1,overlay2中查找,查找失败,最后会从app中查找到,并返回。

4.3 小结

SRO实际上只是利用AAPT重新打包,发生在编译时;RRO,才是overlay机制的关键,其本质是Android系统的动态资源查找机制。

实际运用当中,Overlay机制一般用于手机厂商为不同运营商做客制化定制,或者做主题换肤;一般,第三方应用是无法享受该机制的,除非与手机厂商合作,不过应用层现在也有了换肤框架Android-Skin-Loader,其本质也是利用Android的资源查找机制。

5. OverlayManager源码简析

5.1 主要涉及的类

OverlayManager相关的源码主要在以下目录:
frameworks/base/core/java/android/content/om/IOverlayManager.aidl
frameworks/base/core/java/android/content/om/OverlayInfo.java
frameworks/base/services/core/java/com/android/server/om/IdmapManager.java
frameworks/base/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java
frameworks/base/services/core/java/com/android/server/om/OverlayManagerService.java
frameworks/base/services/core/java/com/android/server/om/OverlayManagerSettings.java
frameworks/base/cmds/idmap/idmap.cpp

5.2 IOverlayManager.setEnabledExclusive()简析

以接口IOverlayManager.setEnabledExclusive()为例。这个接口是设置targetPackage的overlayPackage是否为enable,并且是排它的。意思就是,如果这个targetPackage有多个overlayPackage,也就是多套主题,只要设置了其中一套为enable,其它的由系统帮你设置为disable。
笔者曾根据Overlay机制开发过一个动态主题更换功能,这个接口便是主要用到的一个系统接口。OverlayManagerService是非公开的系统服务,所以只能在系统层面获取到该服务对象。调用过程懒得贴代码了,只画了个图,读者可根据源码自行研读。其中标红1316有些细节没有在图中体现出来,各位请在读源码过程中自行体会。

setEnabledExclusive.jpg

总的来说,设置targetPackage的某个overlayPackage为enable,OverlayManagerServiceImpl更新状态,并调用IdmapManager对资源重新映射,把更新后的状态设置到OverlayManagerSettings,OverlayManagerSettings则会把状态更新到/data/system/overlays.xml配置文件里。
ActivityManagerService.scheduleApplicationInfoChanged()之后的操作,就是更新Configuration,并触发Activity或Service的重新启动或者回调相应组件的onConfigurationChanged()方法,从而完成动态主题更换。

5.3 结语

由于水平有限,很多细节没有面面俱到,也有些是笔者还没搞清楚的,难免会有纰漏误解的地方。希望以此抛砖引玉,望各位大牛不吝批评指教,共同学习交流。

参考资料