Shadow的跨进程设计与插件Service原理

栏目: IOS · Android · 发布时间: 5年前

内容简介:这篇文章介绍一下Shadow的跨进程设计和插件Service的原理。一同讲这两部分是因为它们是相关的。这篇文章假设读者对于Android的Service、Binder通信没有那么了解,因此会提及一些可能对你来说有些简单的内容。在Android系统中,应用可以是多进程的。这在移动端操作系统中应该是非常高级的设计了,很多移动端操作系统、嵌入式系统都是不支持的。多进程程序给程序设计带来了很有优点,也带来更多的复杂性。Android系统中的“四大组件(Activity,Service,Receiver,Provid

这篇文章介绍一下Shadow的跨进程设计和插件Service的原理。一同讲这两部分是因为它们是相关的。这篇文章假设读者对于Android的Service、Binder通信没有那么了解,因此会提及一些可能对你来说有些简单的内容。

跨进程设计对插件框架的必要性

在Android系统中,应用可以是多进程的。这在移动端操作系统中应该是非常高级的设计了,很多移动端操作系统、嵌入式系统都是不支持的。多进程程序给程序设计带来了很有优点,也带来更多的复杂性。

Android系统中的“四大组件(Activity,Service,Receiver,Provider)”全都是可以跨进程通信的组件(下文中的组件指的都是这些组件)。每个组件都可以在AndroidManifest中配置到一个指定的进程。Android系统其实不允许应用自己管理自己进程的生命周期。但是由于我们只要Start一个Intent启动属于那个进程的组件,就能启动那个进程。再用 Java 一般的进程操作API,比如 System.exit() 方法就能杀死一个进程。由于这些很容易做到的特点,让很多Android开发以为自己可以管理进程。实际上这样理解Android进程是不对的。Android系统对进程的设计是这样的,系统收到需要用到某个组件的请求时(比如start Activity或bind Service),就会检查这个组件在AndroidManifest中注册的进程是否已经启动了。如果这个进程还没有启动,系统就会首先启动这个进程,然后构造一个应用注册的Application对象,调用Application对象的 attachBaseContext() 方法。然后构造所有注册了应该在这个进程的ContentProvider,初始化它们,调用它们的 onCreate() 方法。确保后面所有组件,包括还没有被调用的Application对象的 onCreate() 方法都能正常使用这个进程的所有ContentProvider。然后再调用Application对象的 onCreate() 方法。最后才开始初始化本来需要用到这个进程的组件。所以,进程的启动是根据组件的需求启动的。进一步的,这种设计下理应让进程的结束也根据组件的需求。实际上也是这样的,当这个进程中的所有组件都不再被需要时,比如Activity finish了,或者Service stop了,或者没有任何bind了,都会让系统认为这个进程没有存在的必要了。这时系统就会决定回收进程、杀死进程了。当然系统还会在一些“必要”的时刻直接回收进程,比如内存不足等,或者内存不足首先回收了不在前台的Activity、Service等,进而导致进程符合了前面说的条件,不再被需要了。因此,当我们用 System.exit() 等方法关闭进程时,或者遭遇了Crash,系统是不会认为进程不再需要了的。大概是为了避免死循环Crash,Crash的组件会被系统认为不再需要。不过如果这个进程还有其他组件处于活跃状态,或者Activity栈中有多个Activity,最上面的Activity Crash了,它下面压着的Activity就应该露出了成为活跃的了。这种情况下,由于系统认为其他组件还是需要这个进程的,就会将进程的创建流程重新走一遍,启动应该活跃的组件。所以,这里要理解好,组件对于系统来说不是我们常见的“对象”的概念,它不在自己运行的进程内存中表示和记录,而是在系统管理进程中以记录的形式记录的。这些组件中以Activity最为特殊,在编写Activity的时候,不能将Activity简单思考为一个对象。要进一步理解,Activity是有持久化状态的,这些状态就是通过savedInstanceState来表达的。所以,Activity和Service最大的区别不是Activity有界面而Service没有界面, Activity和Service最大的区别是Activity是有状态的,Service是无状态的

将组件放置在单独的进程中有很多优点,基本上都是围绕进程具有单独的内存资源的。对于插件框架来说,有两点十分必要。一是插件一般都是热更新的,质量上要求可能会降低一些,一旦 出现Crash不会影响其他进程的组件 。比如说在宿主的主进程显示一个大厅界面,其中某个按钮跳转到插件。插件在单独进程启动后如果出现Crash,宿主的大厅界面不会受到任何影响。如果插件也在宿主的主进程,就会导致大厅界面也会因进程重启而重新创建。二是Android的JVM虚拟机不支持Native动态库反加载,所以在同一个进程中相同so库的不同版本即不能同时加载,也不能换着加载,会造成 插件和宿主存在so库冲突

多进程也带来更多复杂性,就是它的缺点了。比如,跨进程调用的所有参数都必须是可序列化对象;跨进程通信时对面的进程可能没有启动,也可能已经死了;跨进程通信出现异常,整个跨进程调用的堆栈不会是连着的,而且异常对象通常是不能序列化跨进程传输的。如何控制插件进程退出或重启供另一业务使用。另外,进程的启动速度也比较慢。

Shadow的跨进程设计

主要基于以上两点, Shadow设计的插件框架基本模型 是:Manager、LoadParameters、Loader三个部分。其中Manager工作在宿主进入插件的入口界面所在进程,负责下载插件、安装插件,然后将插件信息封装在LoadParameters中控制Loader启动插件。LoadParameters是一个可序列化的结构体,可以跨进程传输。Loader工作在插件进程,负责将插件免安装的运行起来,解决插件框架的核心问题。

Shadow中有一个叫做 PluginProcessService 的Service是跨进程设计的关键部分,我们简称它 PPS

PPS有多个作用:

  1. 代表插件进程的生命周期。插件进程由它触发创建,由它负责自毁。
  2. 接收反向注册进来的插件文件路径管理器(UuidManager,后续文章介绍插件包管理时再细讲),供Loader查找Manager安装好的插件文件路径。
  3. 加载动态实现的Runtime和Loader。
  4. 获取Loader的Binder接口。
  5. 使插件中的Service能够跨进程工作

我们前面复习过,进程的启动必须由一个组件触发。那么一个没有界面的Service就是一个不错的选择,因为我们通常要对插件进行“预加载”,可能会静默启动插件的Application对象,或启动插件的Service等。还有要想让系统知道这个插件进程是有用的,就必须有活跃的组件在这个进程。我们的插件中的组件全都是没有安装的组件,系统都不知道他们存在,肯定不能靠它们了。靠插件的壳子代理组件也不行,因为我们是一个全动态插件框架,那些壳子代理组件也是插件的一部分,还没有加载呢,所以也不能靠它们。这就需要有一个专门负责启动插件进程的Service,所以它就叫 PluginProcessService 了。Service的Bind语义在这里也很正常,Manager就以Bind的方式启动这个PPS,直到宿主认为不再需要这个插件了,再通过Manager unbind这个PPS。Manager通过Bind拿到的Binder就是PPSController,通过这个PPSController操作PPS,让宿主得以使用“插件服务”。所以PPS是一个货真价实的Service。

插件Service的实现原理

选择Service来触发启动插件进程还有一个原因是,我们如果想让插件进程的插件Service能像正常Service一样跨进程通信,就必须在插件进程至少有一个真的注册在宿主中的Service。这涉及一个Binder的基本知识,就是Binder是一个中心化的跨进程通信框架。每一个Binder都分本地端和远程端,本地端实现功能,远程端供其他进程调用功能。直接实现的Binder自然就是本地端了,而远程端怎么实现呢?实际上把一个本地Binder通过另一个已经存在远程端的Binder跨进程传输一下,就自动把这个本地Binder送到Binder的中心管理器中注册并生成远程端了,新生成的远程端就通过那个已经存在的Binder的远程端输出出来了。这里可能自然会想到第一个Binder哪里来的的问题,简单说就是第一个Binder在设计中特殊处理了,详细的设计可以自行Google一下。所以,要想插件Service能正常跨进程工作,就要把插件Service的Binder通过一个已经存在的Binder传输一次。因此,最简单的办法就是通过PPS的Binder传输一次。

所以,我们将Loader本身也设计成了一个插件Service(即 dynamic-loader )。因为全动态的设计中,宿主中的代码不会直接操作Loader,真正操作Loader的是动态实现的Manager。因此Loader和Manager都是动态实现,Loader上的接口就没必要在PPS上固定写死了。PPS上只保留了加载Runtime和Loader的必要方法。Loader本身的Binder先通过PPS跨进程通信到Manager进程,从而使Loader的Binder成了跨进程的Binder。然后Loader上再暴露的 bindPluginService 方法再将插件Service的Binder通过Loader的Binder跨进程传输其他进程,就是的插件Service真正可以面向其他进程工作起来了。我们的插件Service实现就是这么简单。可以看出来Shadow的插件Service是没有单独的代理壳子Service的,只依赖一个PPS就实现了不限数量的插件Service支持。

为什么Shadow里的Service都没有用aidl实现?

这是因为这些Binder跨进程调用都是有可能会失败的,失败了不能粗暴的Crash。所以,PPS和 dynamic-loader 的Binder都是半手工写的Binder。半手工就是用aidl先生成代码,再复制出来添加自定义可序列化Exception的能力。实现Manager跨进程操作Loader可以Catch异常。

PPS可以有多个

由于全动态的设计,在一个宿主中可以有多个Manager实现。一个Manager实现也可以同时操作多个PPS,只需要继承PPS注册在不同的进程中就可以了。由于Loader、Runtime也是动态的,所以不同的插件进程可以使用不同版本的Loader实现。

待改进的

Shadow的Sample中还有我们自己的业务中,都对壳子代理组件指定了进程名。对我们业务来说,这些壳子实际上是旧框架遗留在宿主中被Shadow复用的。实际上开发完Shadow的PPS,我们就意识到,这些壳子组件应该是可以应用 android:multiprocess 特性的。

根据文档: developer.android.com/guide/topic…

android:multiprocess为true时,Activity和Provider的行为是启动Intent时处于哪个进程,就将被启动的Activity或Provider启动在哪个进程。

既然PPS已经决定了插件进程,由PPS启动插件Activity就可以使壳子工作在PPS所在进程了。这样可以在同一个宿主中应用多个插件进程时少注册一些壳子组件。

欢迎大家实验一下然后提一个PR来改进这个问题。

github.com/Tencent/Sha…

PS: 新注册的掘金账号,发文章曝光量很低。选择来掘金分享也是希望吸引更多开发者关注到Shadow。所以请大家支持一下,点个赞提高点我的掘力值,以便更好的继续分享。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Realm of Racket

Realm of Racket

Matthias Felleisen、Conrad Barski M.D.、David Van Horn、Eight Students Northeastern University of / No Starch Press / 2013-6-25 / USD 39.95

Racket is the noble descendant of Lisp, a programming language renowned for its elegance and power. But while Racket retains the functional goodness of Lisp that makes programming purists drool, it wa......一起来看看 《Realm of Racket》 这本书的介绍吧!

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换