IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 实现Android跨进程组件通信能有多简单? -> 正文阅读

[移动开发]实现Android跨进程组件通信能有多简单?

实现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的朋友们可能知道这体验并不好。我觉得麻烦的点主要在于:

  1. 定义AIDL接口文件(无IDE自动提示支持)
  2. 编译AIDL生成Stub类 (需预先编译一次生成)
  3. 继承Stub类并实现AIDL中定义的接口 (必须继承Stub类型,不如普通服务的实现类只需实现对应的接口,可以继承其他类进行代码复用)
  4. 通过代理接口进行远程调用必须捕获RemoteException (要么每个调用都catch处理一下,要么写个包装类wrapper把异常吃掉返回默认值)

如果涉及到接口的改动,又要编译两次,改AIDL文件,改Stub类实现文件,如果在第4步用了包装类,还得改包装类文件……

所以,跨进程通信的问题点主要麻烦在于AIDL书写起来太麻烦了!

现在的期望是:通过现在流行的APT(注解处理工具)技术动态生成这些模板代码,让我们回到最初,还是只关注接口、实现就好了,让跨进程组件通信和普通组件通信一样简单、优雅。

那要怎么做呢,继续往下看!

如何简化AIDL实现?

仔细看来,AIDL流程中真正的实现核心还在于接口定义(对应AIDL定义)和接口实现(Stub实现),与普通服务定义并无二致。唯一比进程间服务多的处理大概就是远程服务需要考虑的RemoteException的处理了。整体实现过程其实好多都是模板化的工作,真正的核心也就是普通的接口定义以及实现,只要我们能够自动生成那些模板类,把实际实现代理给接口的实现类就好了。基于这种情况,我们来考虑使用动态生成代码的方式来避免写这些模板代码。

哪些东西可以自动生成

  1. AIDL文件:AIDL与Interface接口功能是一致的,我们可以根据普通的Interface定义生成AIDL
  2. Stub类的实现:可自动生成Stub实现,内部只需要代理Interface的实现即可把远程调用转发给Interface的实现类
  3. Binder接口代理:如上面分析,ADIL通信中远端拿到的是服务的代理接口,每个调用都需要捕获RemoteException,我们可以自动生成这样的一个代理,将异常通过统一的方式转发出来,对于不想做特殊处理的调用者,提供默认的实现捕获异常或者重新抛出RuntimeException类型的异常,反正就是不需要调用者必须在每个方法的调用时进行异常处理了,让调用者对于是否要处理调用的异常有选择的权利。

新的跨进程通信模型

假设我们已经完成以上文件的自动生成,若定义一个IMusic的接口,提供音乐相关服务,我们会自动生成如下文件:

  1. IMusicBinder.aidl: AIDL定义文件,编译后会生成IMusicBinderIMusicBinder.Stub
  2. IMusicRemote.java:继承IMusicBinder.Stub,用于发布到远端,内部代理IMusic的具体实现。
  3. IMusicRemoteProxy.java:实现IMusic接口,用于远端服务调用,代理IMusicBinder远程服务,内部处理每个调用的异常。

另外假设MusicServiceIMusic的实现类,那新的模型大致就可以做成下面的样子:
SimpleService跨进程通信模型

流程大致描述为:

  1. 进程A的服务实现组件中,通过SimpleService把实现了IMusic的MusicService对象作为远程服务发布。
  2. 进程A的SImpleService内部把MusicService包装成IMusicRemote发布到RemoteServiceManager。RemoteServiceManager可以实现为上面讲到的一个Android的Service,用于真正的管理所有的远程服务对象的Binder。
  3. 进程B的服务使用组件中通过SimpleService发起IMusic服务的获取请求。
  4. 进程B的SimpleService向RemoteServiceManager请求IMusicRemote对象。
  5. RemoteServiceManager返回保存的IMusicRemote对象给进程B的SimpleService。
  6. 进程B的SimpleService将IMusicRemote对象包装成IMuisicRemoteProxy返回给服务使用组件。

自此,服务使用组件就拿到了一个IMusic服务对象,可以进行跨进程通信了。

于是,我们完成了简化的进程间通信模型,解答了小节标题提出的问题。
这样我们就可以只定义普通的接口和接口实现,但却等发布为一个跨进程的服务了!

我们可以:不用再写AIDL,不用再等两遍编译,不用再考虑每次调用跨进程方法需要处理烦人的RemoteException,不用再害怕修改一点接口而去改很多个文件了,甚至可以用上IDE的重构功能,轻松修改接口定义实现……

具体实现,开始使用

经过几周的努力,目前大概实现了一个可用的版本: github传送

具体怎么用呢,看下面:

一、先看看普通组件服务发布使用流程,如下:

// 1. 公共依赖模块,声明服务接口
interface IMusic {
  fun play(name: String)
}

// 2. 音乐组件模块,实现服务接口
class MusicService: IMusic {
  override fun play(name: String) {
    Logging.d("MusicService", "play $name")
  }
}

// 3. 音乐组件模块,发布服务
SimpleService.publishService(IMusic::class.java, MusicService())

// 4. 其他组件模块,获取使用服务
val music = SimpleService.getService(IMusic::class.java)
music.play("千里之外")

二、重头戏来了,跨进程组件服务发布流程,如下:

注意:每个进程在使用跨进程服务相关接口前,都需要先初始化远程服务,提供context,一般可在Appiliction的onCreate方法初始化:

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        // 【变化1】使用跨进程服务之前需要先初始化,用于连接RemoteServiceManager
        SimpleService.initRemoteService(this)
    }
}

具体发布流程:

// 1. 公共依赖模块,声明服务接口
// 【变化2】为跨进程服务接口添加‘RemoteServive’注解,SimplieService将自动生成AIDL类型
@RemoteService
interface IMusic {
  fun play(name: String)
}

// 2. 音乐组件模块,实现服务接口
class MusicService: IMusic {
  override fun play(name: String) {
    Logging.d("MusicService", "play $name")
  }
}

// 3. 音乐组件模块,发布远程服务
// 【变化3】发布服务改为publishRemoteService方法的调用
SimpleService.publishRemoteService(IMusic::class.java, MusicService())

// 4. 其他组件模块(跨进程),获取并使用远程服务
// 【变化4】获取远程服务接口变为getRemoteServiceWait,此处使用同步等待5s的方式获取。
// 还提供回调以及协程的异步方式获取,分别是bindRemoteService和getRemoteService
val music = SimpleService.getRemoteServiceWait(IMusic::class.java, 5000)
music.play("千里之外")

以上,今4个简单的变动,便完成了跨进程服务的发布使用,增量变动只有一个,就是初始化远程服务,其余部分都和写本地服务基本一致!

三、跨进程组件服务发布支持Parcelable类型参数

// 声明音乐信息类型
// 使用‘ParcelableAidl’注解修饰Pacelable类型,SimpleService将自动生成AIDL声明
@ParcelableAidl
@Parcelize
class MusicInfo(var name: String, var author: String): Parcelable

// 增强服务接口,让play接口接收到更丰富的信息以便进行更准确的音乐匹配
@RemoteService
interface IMusic {
  fun play(info: MusicInfo)
}

四、也支持AIDL的修饰符,oneway、in、out、inout

@RemoteService
interface IMusic {
  // play不需要返回值,可以声明为oneway
  @OneWay
  fun play(info: MusicInfo)
  // 读取原始的音频数据,参数data声明为out
  fun getRawData(info: MusicInfo, @Out data: ByteArray)
}

五、回调也是支持的

// 声明播放进度回调接口
@RemoteService
interface OnPlayProgressUpdate {
  fun onProgress(progress: Int)
}

@RemoteService
interface IMusic {
  // play参数中添加播放进度回调
  @OneWay
  fun play(info: MusicInfo, onProgress: OnPlayProgressUpdate)
  fun getRawData(info: MusicInfo, @Out data: ByteArray)
}

以上,基本上让跨进程服务和进程间服务使用无太大差别了,需要做的仅仅是加几个注解。

六、开始之前

当然,在使用之前我们需要做一些基本的项目配置,如下:

  1. 在项目根目录的build.gradle中添加jitpack的maven仓库以及插件依赖:
buildscript {
    // 统一simple-service库的版本号
    ext.simple_service_version = "1.0.7"
  
    repositories {
        maven { url 'https://jitpack.io' }
    }
  
    dependencies {
      	// 声明simple-service的gradle插件依赖
        classpath("com.github.xiangning17.simpleservice:plugin:$simple_service_version")
    }
}

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}
  1. 在远程服务接口声明的基础模块build.gradle中:
// 启用simple-service插件
apply plugin: 'simple-service'

dependencies {
    // 添加库依赖,并暴露给上层组件
    api "com.github.xiangning17.simpleservice:core:$simple_service_version"
  
    // 声明注解处理器,用于解析RemoteService等注解,并生成相关代理类型
    // 如果需要处理被注解的kotlin类,需要使用kapt
    annotationProcessor "com.github.xiangning17.simpleservice:annotation-processor:$simple_service_version"
}
  1. 在app模块的build.gradle中:
// app模块启用simple-service插件,用于设置RemoteServiceBridg的进程
apply plugin: 'simple-service'

// 通过以下配置指定RemoteServiceBridge远程服务桥所在的进程
// RemoteServiceBridge是RemoteServiceManager远程服务管理器对外的窗口,是一个Android Service。
// 当其他进程想要连接RemoteServiceManager时,就通过绑定RemoteServiceBridge服务得到服务进程的单例RemoteServiceManager对象,
// 然后其他进程就可以进行远程服务的查询获取以及发布监听了。

// 这个配置项可以省略,则默认就是applicationId,也就是主进程。
// 但是即便省略该项配置,在app模块启用'simple-service'插件的步骤还是不能省略,
// 不然不能成功设置RemoteServiceBridge服务所在的进程
// 由于RemoteServiceManager用于管理所有远程服务,要尽量保持稳定,不要因为某个进程的其他逻辑崩溃被牵连死亡
// 对稳定性比较高的场景其实还是建议尽量放到单独的进程,如下:
simpleService {
    bridgeServiceProcess = ":simple.service.remote"
}

高级用法

除以上基本能力外,SimpleService还提供以下特性。

开始之前,先定义几个概念:

  • 远程服务:指被@RemoteService修饰的接口,如IMusic。
  • 远程服务实现:指‘远程服务’的实现类,如MusicService就是对IMusic的远程服务实现。
  • 远程服务Binder:指对‘远程服务’自动生成的Binder类型,用于跨进程的Binder服务传递,如IMusicRemote。
  • 远程服务代理:指远程服务使用者实际拿到的对象,是对远程服务Binder的一个代理实现,如IMusicRemoteProxy

1. 远程服务重新发布能力

通过SimpleService获取远程服务时,我们实际拿到的是一个代理类型,即为远程服务代理,这个类型是根据@RemoteService注解处理的接口自动生成的。其实现了被注解的接口,且代理了远程服务Binder。因为返回给服务使用者的对象是代理对象,所以我们能做到更新具体执行的远程服务Binder指向。这个特性可用于远程服务的重新发布。如下:

// 组件A,发布IMusic为MusicService
SimpleService.publishRemoteService(IMusic::class.java, MusicService())

// 组件B,获取并使用IMusic
val music = SimpleService.getRemoteServiceWait(IMusic::class.java, 5000)
music.play("千里之外")

// 组件A,重新发布IMusic为MusicServiceNew
SimpleService.publishRemoteService(IMusic::class.java, MusicServiceNew())

// 组件B,此时不用再次获取IMusic,直接就可以使用到MusicServiceNew的服务,由SimpleService内部自动更新了远程服务的指向
music.play("美丽新世界")

这个特性除了在组件A想主动更新服务实现时,对于组件A所在进程异常退出后又重启发布服务的情况,也能进行断线重连

甚至,如果你想对本地服务也实现这种服务更新,也可以将其通过远程服务的方式进行发布_

2. 自定义远程服务调用时的异常处理

由于我们返回给使用者的远程服务代理代理了对远程服务Binder的访问,那其内部必然是要处理RemoteException等调用异常的,目前的方式是通过IMethodErrorHandler接口把具体调用时的异常处理让上层进行实现,类似动态代理。现在默认情况下,对所有远程服务代理的处理都是使用默认提供的DefaultValueMethodErrorHandler,该处理器捕获所有异常,并返回默认的返回值。你如果想自定义该异常处,比如需求上不能吃掉某些异常,需要再次把抛出去,或者针对某些特殊类型异常做特定处理等等,可以通过以下方式设置错误处理器:

// 组件B,使用者注册IMusic远程服务的调用错误处理器(同一个进程内全局生效)
SimpleService.registerMethodErrorHandler(IMusic::class.java, errorHandler))

3. 获取一个远程服务代理远程服务Binder

在服务使用端,有时希望获取到远程服务代理对象代理的远程服务代理,比如某个接口的一些回调参数,我们想使用RemoteCallbackList进行管理时(参考项目源码的RemoteServiceManagerImpl),需要IInterface类型的对象,可以通过以下方式:

// 组件B,获取服务
val music = SimpleService.getRemoteServiceWait(IMusic::class.java, 5000)

// 组件A,后续某个时机想拿到‘远程服务代理’对象的`远程服务Binder`
val musicBinder:IMusicBinder = SimpleService.getServiceRemoteInterface(IMusic::class.java, music)

结尾

以上,就是这次介绍的简单实现跨进程组件通信的方案了,你get到了吗!

欢迎体验,欢迎提出问题,欢迎来github给我点赞!

github传送

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-16 11:52:22  更:2021-08-16 11:53:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 10:29:49-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码