跨进程通信(IPC)是我们在编写程序时经常遇到的情况,Android平台也给我们提供了许多IPC手段:比如基于binder的广播、AIDL、远程服务等等,基于存储器的sharedPreference、ContentProvider等。不过,Android是基于Linux的,linux本身的IPC手段自然也照样适用,比如socket、管道、内存映射等等。其中,内存映射(或者说建立在它的基础之上的Posix共享内存)我们可以简单地认为它是把同一块物理内存映射到了不同进程的地址空间中,这样虽然在这两个进程中我们的两个指针分别指向不同的虚拟地址,但实际上他们访问的是同一块物理内存。这样跨进程通信的好处是,整个过程无需借助内核转发,不需要copy,速度非常快,适合大量数据在不同进程间的传输,比如Android在处理音频数据的时候,就采用了这种方式。
现在各个手机里面都会有一个主题市场类的应用,我们可以选择不同的主题,下载并应用到我们的手机,效果非常酷炫。这些主题会改变我们很多系统应用比如launcher、systemui、设置、短信、联系人等等的图标啊,配色啊什么的。虽然这种效果有不同的实现方式,但是有一点是肯定的,那就是它们把同一个资源id,映射到了不同的主题包里的资源。我们画图来对比一下
我们看到,这两者有异曲同工之妙!Android的主题换肤的实现有多种,这里我们讲Android源生的方式,也就是Sony在Android5.0上为AOSP提供的Runtime Resources Overlay。虽然Runtime Resources Overlay(后面我们就简称RRO了)主要是针对主题,但它可以用的地方绝对不仅仅是主题这一块儿(比如,RRO也是资源修复的另外一种思路)。需要说明的是,虽然Android5.0上已经引入了RRO机制,但它在android5.x上很不成熟,不成熟到什么程度呢?It does NOT work at all!毕竟刚刚引入,bug比较多也可以理解。RRO真正可用的第一个版本是Android6.0,建议在这个平台下测试,因为更高的版本加入了相关权限控制。
假设我们的一个App(包名:com.demo.app)中有个字符串:
//values/strings.xml
//layout_main.xml
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/greeting" />
那么这个TextView将会显示:Good Morning!
我们再写一个APK,这个apk里没有代码,只有一个字符串资源:
//values/strings.xml
//AndroidManifest.xml
然后编译我们的这个覆盖包:com.demo.app.overlay,完了把它push到/vendor/overlay/目录下(需要root权限),然后重启,然后点开我们的app:com.demo.app,我们就会发现TexView的内容变成了Good Night!
这就是RRO在起作用了,虽然我们没有改动com.demo.app里的任何东西,但它显示的内容的的确确变了!
那么RRO到底是如何起作用的,它是如何实现了这个资源id的映射呢?我们来看一张图:
这是我写的一个基于RRO的主题换肤demo的原理图。纵向分为三个层次:应用层、framework的java层、framework的native层,我们用不同的颜色来表示。它涉及主题市场应用进程、Launcher与SystemUI等需要改变皮肤的进程、system_server进程、installd进程、idmap进程等至少5个进程,我们用橙色来表示。另外,这些进程当中用到的关键组件比如PackageManagerService、AssetManager、Restable、Resources等我们用浅粉色表示。
换肤进程,可以认为它就是主题市场应用,它负责展示设计师设计的主题包(我们它叫overlay包,里面全是资源,没有dex字节码,被覆盖的包我们叫target包,比如launcher等),当用户点击下载后,会把主题包(一系列的overlay包)下载下来。当用户选择某个主题点击“应用”按钮的时候,我们就会把与这个主题对应的资源包copy到一个特定的目录,然后通过binder调用到PMS里对应的方法去做idmap,这就进入到了system_server进程。
从名字就可以看出idmap就是对资源id做动态映射的。其实,在做idmap前,PMS还会先扫描一下这些overlay package,提取出相关信息。另外,android本身是在开机时只做一次idmap的,我们这里做了改动,实现了动态idmap,需要的时候就可以通过相关接口去做。然后在system_server里,它会通过local_socket请求installd进程去做idmap。也就是说,idmap并不是在system_server进程里做的。为什么不放到system_server进程里呢?因为system_server权限不够。是的,哪怕是system_server也没有权限去做idmap。
installd是由init进程起来的一个守护进程,它主要负责我们安装包的管理,比如install、uninstall、rename、dexopt等等,当然也包括去做idmap。
不过,idmap也不是installd进程亲自操刀的,它会fork出子进程,然后在里面让idmap这个bin来完成具体的工作。idmap的主要逻辑是在AssetManager模块的ResourceTypes.cpp里完成的,它会对overlay包和target包作比较,抽取出他们资源之间的映射关系,并输出到一个idmap文件里,放在/data/resource-cache/这个目录下。这个文件的路径是以@作为分隔符的,并且以idmap结尾,形如:vendor@[email protected]@idmap。
到这一步,我们图中的左半部分也就进行完了,映射关系也已经生成,剩下的工作就是重启我们的target package(也就是systemui、launcher等需要改变主题的应用),让他们重新加载资源,这个时候,它们就会根据idmap文件里的映射关系,找到正确的资源,我们的RRO机制也就生效了。这就是上图中的右半部分的流程了。
其实,我们完全可以不用重启这些target package,只需要让它们重新加载资源并刷新就可以了。
我们在这里只需要对其原理有个直观的认识即可,后面我们会从system_server、installd与idmap、AssetManager三个方面来详细介绍其实现。
其实主题换肤的实现还有其它方式,比如基于资源动态加载的那些框架,这里就不具体列举了哈。RRO和它们相比有何不同呢?
1. 资源的动态加载框架,目前来说大多量级比较轻,它属于App层面的技术;而RRO的量级比较重,它是framework层的技术。
2.资源的动态加载框架比较灵活,要注意不同平台的兼容问题,基本没别的特殊要求;而RRO则是framework的一部分,要动它,首先得有framework的代码,然后还有各种权限问题要注意。
3.资源的动态加载框架一般集成到一个个的APK里,它只能对一个应用生效,但RRO是在framework里的,它可以对所有应用生效。可以设想,我们的主题涉及到10个应用UI的替换,那么这10个应用都要集成(或者说引用这一框架);但RRO不需要这样,在Android5.x以上,所有应用就都有这个能力。
4.资源的动态加载框架需要应用集成它们,并调用它的API,也就是说会有许多代码层面的依赖;RRO不需要应用做任何改动就可生效。
5.资源的动态加载框架在运行时的效率远远没有RRO高:
我们稍做说明:target就是我们要换肤的应用,可以理解为launcher、systemui、settings等;资源对象我们可以理解为AssetManager。在java代码中,我们要获取主题包里的资源,最坏的情况下,资源的动态加载框架需要先根据资源的id,从它本身的资源包中查寻出这个资源的entryName(当然,如果知道了这个资源的包名、type和资源名这步可以省略),然后再根据entryName去拿到这个资源在主题包(或者叫皮肤包)中的资源id,然后再去获取资源。这当中最耗时的是根据资源名称得到资源id这一步,AssetManager要遍历key string pool,对当中的每一个字符串做比较,然后才能找到正确结果返回。也就是说,这仍然是基于字符串比较的,效率极低,这样是为什么android不推荐根据资源的名称来获取资源的原因。但RRO对主题包的资源的访问,和访问它自身的资源基本没啥区别,非常简洁,直接通过id来获取。而资源id实质上就是资源的索引,也就是说RRO对主题包(这里叫overlay包更合适)资源的访问是基于索引的,所以会很快。
6.让主题生效,也就是用户点击“应用”后,RRO会有一些耗时的操作,主要是生成idmap文件,也就是对资源id做映射;而资源的动态加载框架则没有这一步。
总结下来也是各有优缺点,我们可以根据具体的需求做出合适的选择。下一篇,我们开始从system_server入手来详细分析RRO的实现过程与原理。