实现Android跨进程组件通信能有多简单?
作为一个Android开发,都要会点组件化知识。组件化的主要的特点,是剥离依赖,但组件间不直接依赖后,通信问题要怎么解决呢。
通常我们用的一下这种类似Binder通信的C/S架构,由一个ServiceManager服务管理器作为一个桥梁提供服务注册和服务查询,业务上要进行通信就是以下三部曲:定义服务接口,发布服务,使用服务。 
这套架构在单一进程间运行很简单且直观,我们需要做的就是把服务接口下沉到公共依赖,在组件A实例化服务接口的对象,然后通过ServiceManager进行服务发布,组件B通过ServiceManager获取并使用服务就好了。但要想跨进程,就有点麻烦了。
保持这套架构的场景下,我们可以去写AIDL,让ServiceManager支持管理Binder类型的服务就好了,ServiceManager的角色用一个Android的Service去实现,每个组件要发布、查询服务,直接通过bind这个ServiceManager就可以了。通信模型大致如下:

看来做跨进程通信支持,模型变化也并不大,还剩下的问题就仅仅是每个服务类型的实现了,通常来讲就是定义AIDL以及实现Binder服务类。市面上很多框架也确实做到了这点,比如爱奇艺开源的Andromeda就支持跨进程的服务发布。
但我们还是得自己写AIDL,远程服务的使用端用IInterface代理的远程服务对象,每次使用都要处理RemoteException也要处理。实际上写过AIDL的朋友们可能知道这体验并不好。我觉得麻烦的点主要在于:
- 定义AIDL接口文件(无IDE自动提示支持)
- 编译AIDL生成Stub类 (需预先编译一次生成)
- 继承Stub类并实现AIDL中定义的接口 (必须继承Stub类型,不如普通服务的实现类只需实现对应的接口,可以继承其他类进行代码复用)
- 通过代理接口进行远程调用必须捕获RemoteException (要么每个调用都catch处理一下,要么写个包装类wrapper把异常吃掉返回默认值)
如果涉及到接口的改动,又要编译两次,改AIDL文件,改Stub类实现文件,如果在第4步用了包装类,还得改包装类文件……
所以,跨进程通信的问题点主要麻烦在于AIDL书写起来太麻烦了!
现在的期望是:通过现在流行的APT(注解处理工具)技术动态生成这些模板代码,让我们回到最初,还是只关注接口、实现就好了,让跨进程组件通信和普通组件通信一样简单、优雅。
那要怎么做呢,继续往下看!
如何简化AIDL实现?
仔细看来,AIDL流程中真正的实现核心还在于接口定义(对应AIDL定义)和接口实现(Stub实现),与普通服务定义并无二致。唯一比进程间服务多的处理大概就是远程服务需要考虑的RemoteException的处理了。整体实现过程其实好多都是模板化的工作,真正的核心也就是普通的接口定义以及实现,只要我们能够自动生成那些模板类,把实际实现代理给接口的实现类就好了。基于这种情况,我们来考虑使用动态生成代码的方式来避免写这些模板代码。
哪些东西可以自动生成?
- AIDL文件:AIDL与Interface接口功能是一致的,我们可以根据普通的Interface定义生成AIDL
- Stub类的实现:可自动生成Stub实现,内部只需要代理Interface的实现即可把远程调用转发给Interface的实现类
- Binder接口代理:如上面分析,ADIL通信中远端拿到的是服务的代理接口,每个调用都需要捕获RemoteException,我们可以自动生成这样的一个代理,将异常通过统一的方式转发出来,对于不想做特殊处理的调用者,提供默认的实现捕获异常或者重新抛出RuntimeException类型的异常,反正就是不需要调用者必须在每个方法的调用时进行异常处理了,让调用者对于是否要处理调用的异常有选择的权利。
新的跨进程通信模型
假设我们已经完成以上文件的自动生成,若定义一个IMusic的接口,提供音乐相关服务,我们会自动生成如下文件:
- IMusicBinder.aidl: AIDL定义文件,编译后会生成
IMusicBinder和IMusicBinder.Stub。 - IMusicRemote.java:继承
IMusicBinder.Stub,用于发布到远端,内部代理IMusic的具体实现。 - IMusicRemoteProxy.java:实现
IMusic接口,用于远端服务调用,代理IMusicBinder远程服务,内部处理每个调用的异常。
另外假设MusicService是IMusic的实现类,那新的模型大致就可以做成下面的样子: 
流程大致描述为:
- 进程A的服务实现组件中,通过SimpleService把实现了IMusic的MusicService对象作为远程服务发布。
- 进程A的SImpleService内部把MusicService包装成IMusicRemote发布到RemoteServiceManager。RemoteServiceManager可以实现为上面讲到的一个Android的Service,用于真正的管理所有的远程服务对象的Binder。
- 进程B的服务使用组件中通过SimpleService发起IMusic服务的获取请求。
- 进程B的SimpleService向RemoteServiceManager请求IMusicRemote对象。
- RemoteServiceManager返回保存的IMusicRemote对象给进程B的SimpleService。
- 进程B的SimpleService将IMusicRemote对象包装成IMuisicRemoteProxy返回给服务使用组件。
自此,服务使用组件就拿到了一个IMusic服务对象,可以进行跨进程通信了。
于是,我们完成了简化的进程间通信模型,解答了小节标题提出的问题。 这样我们就可以只定义普通的接口和接口实现,但却等发布为一个跨进程的服务了!
我们可以:不用再写AIDL,不用再等两遍编译,不用再考虑每次调用跨进程方法需要处理烦人的RemoteException,不用再害怕修改一点接口而去改很多个文件了,甚至可以用上IDE的重构功能,轻松修改接口定义实现……
具体实现,开始使用
经过几周的努力,目前大概实现了一个可用的版本: github传送
具体怎么用呢,看下面:
一、先看看普通组件服务发布使用流程,如下:
interface IMusic {
fun play(name: String)
}
class MusicService: IMusic {
override fun play(name: String) {
Logging.d("MusicService", "play $name")
}
}
SimpleService.publishService(IMusic::class.java, MusicService())
val music = SimpleService.getService(IMusic::class.java)
music.play("千里之外")
二、重头戏来了,跨进程组件服务发布流程,如下:
注意:每个进程在使用跨进程服务相关接口前,都需要先初始化远程服务,提供context,一般可在Appiliction的onCreate方法初始化:
class App : Application() {
override fun onCreate() {
super.onCreate()
SimpleService.initRemoteService(this)
}
}
具体发布流程:
@RemoteService
interface IMusic {
fun play(name: String)
}
class MusicService: IMusic {
override fun play(name: String) {
Logging.d("MusicService", "play $name")
}
}
SimpleService.publishRemoteService(IMusic::class.java, MusicService())
val music = SimpleService.getRemoteServiceWait(IMusic::class.java, 5000)
music.play("千里之外")
以上,今4个简单的变动,便完成了跨进程服务的发布使用,增量变动只有一个,就是初始化远程服务,其余部分都和写本地服务基本一致!
三、跨进程组件服务发布支持Parcelable类型参数
@ParcelableAidl
@Parcelize
class MusicInfo(var name: String, var author: String): Parcelable
@RemoteService
interface IMusic {
fun play(info: MusicInfo)
}
四、也支持AIDL的修饰符,oneway、in、out、inout
@RemoteService
interface IMusic {
@OneWay
fun play(info: MusicInfo)
fun getRawData(info: MusicInfo, @Out data: ByteArray)
}
五、回调也是支持的
@RemoteService
interface OnPlayProgressUpdate {
fun onProgress(progress: Int)
}
@RemoteService
interface IMusic {
@OneWay
fun play(info: MusicInfo, onProgress: OnPlayProgressUpdate)
fun getRawData(info: MusicInfo, @Out data: ByteArray)
}
以上,基本上让跨进程服务和进程间服务使用无太大差别了,需要做的仅仅是加几个注解。
六、开始之前
当然,在使用之前我们需要做一些基本的项目配置,如下:
- 在项目根目录的build.gradle中添加jitpack的maven仓库以及插件依赖:
buildscript {
ext.simple_service_version = "1.0.7"
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
classpath("com.github.xiangning17.simpleservice:plugin:$simple_service_version")
}
}
allprojects {
repositories {
maven { url 'https://jitpack.io' }
}
}
- 在远程服务接口声明的基础模块build.gradle中:
apply plugin: 'simple-service'
dependencies {
api "com.github.xiangning17.simpleservice:core:$simple_service_version"
annotationProcessor "com.github.xiangning17.simpleservice:annotation-processor:$simple_service_version"
}
- 在app模块的build.gradle中:
apply plugin: 'simple-service'
simpleService {
bridgeServiceProcess = ":simple.service.remote"
}
高级用法
除以上基本能力外,SimpleService还提供以下特性。
开始之前,先定义几个概念:
- 远程服务:指被
@RemoteService修饰的接口,如IMusic。 - 远程服务实现:指‘远程服务’的实现类,如
MusicService就是对IMusic的远程服务实现。 - 远程服务Binder:指对‘远程服务’自动生成的Binder类型,用于跨进程的Binder服务传递,如
IMusicRemote。 - 远程服务代理:指远程服务使用者实际拿到的对象,是对远程服务Binder的一个代理实现,如
IMusicRemoteProxy。
1. 远程服务重新发布能力
通过SimpleService获取远程服务时,我们实际拿到的是一个代理类型,即为远程服务代理,这个类型是根据@RemoteService注解处理的接口自动生成的。其实现了被注解的接口,且代理了远程服务Binder。因为返回给服务使用者的对象是代理对象,所以我们能做到更新具体执行的远程服务Binder指向。这个特性可用于远程服务的重新发布。如下:
SimpleService.publishRemoteService(IMusic::class.java, MusicService())
val music = SimpleService.getRemoteServiceWait(IMusic::class.java, 5000)
music.play("千里之外")
SimpleService.publishRemoteService(IMusic::class.java, MusicServiceNew())
music.play("美丽新世界")
这个特性除了在组件A想主动更新服务实现时,对于组件A所在进程异常退出后又重启发布服务的情况,也能进行断线重连。
甚至,如果你想对本地服务也实现这种服务更新,也可以将其通过远程服务的方式进行发布_。
2. 自定义远程服务调用时的异常处理
由于我们返回给使用者的远程服务代理代理了对远程服务Binder的访问,那其内部必然是要处理RemoteException等调用异常的,目前的方式是通过IMethodErrorHandler接口把具体调用时的异常处理让上层进行实现,类似动态代理。现在默认情况下,对所有远程服务代理的处理都是使用默认提供的DefaultValueMethodErrorHandler,该处理器捕获所有异常,并返回默认的返回值。你如果想自定义该异常处,比如需求上不能吃掉某些异常,需要再次把抛出去,或者针对某些特殊类型异常做特定处理等等,可以通过以下方式设置错误处理器:
SimpleService.registerMethodErrorHandler(IMusic::class.java, errorHandler))
3. 获取一个远程服务代理的远程服务Binder
在服务使用端,有时希望获取到远程服务代理对象代理的远程服务代理,比如某个接口的一些回调参数,我们想使用RemoteCallbackList进行管理时(参考项目源码的RemoteServiceManagerImpl),需要IInterface类型的对象,可以通过以下方式:
val music = SimpleService.getRemoteServiceWait(IMusic::class.java, 5000)
val musicBinder:IMusicBinder = SimpleService.getServiceRemoteInterface(IMusic::class.java, music)
结尾
以上,就是这次介绍的简单实现跨进程组件通信的方案了,你get到了吗!
欢迎体验,欢迎提出问题,欢迎来github给我点赞!
github传送
|