摘要|Abstract
跨 App 通信的方式都围绕着 Android 的四大组件实现,各有用途。对于一般的 App 来说,几乎只需要对外提供特定 Activity 即可;而对于提供服务的特殊 App 来说,如何合理、优雅、高效地提供数据给其他 App 就是关键技术了。
关键字|Keyword
AIDL、跨进程、Service
零、需求描述
Android 的四大组件皆有「跨进程」的能力。Activity 可提供页面和 onActivityResult 的回传数据,ContentProvider 可提供自由的数据查询和数据变化的监听,BroadcastReceiver 可提供一对一到一对多的数据通信,Service 可提供除 UI 外的最大程度的自定义功能。
随着 App 之间互动需求的增加,广播机制单向通信、无状态等弊端逐渐显露出来,我们需要用一种稳定可靠的双向通信机制实现部分紧密交互的需求。
需求是这样的,有一个 App 实现了对系统功能的深度管控并保持跟服务端的长连接,其他 App 在使用过程中或需要调用管控并异步得知结果,或需要保持实时接收服务端的某些信息。
一、AIDL和通信机制
Service 也是相当常用的 Android 组件了,在 App 内使用时,我们通常让 onBind 方法返回 null,而这个 onBind 方法就是跨进程通信的核心。
基于 Service 的跨进程通信是类似 C/S 架构的,Server 端提供 Service,在 onBind 返回 IBinder 对象,Client 通过 IBinder 对象获取 Server 的数据或者调用 Server 提供的功能。
一次通信必定由 Client 发起,Server 给予回应。通信的过程都基于 IBinder 实现,IBinder 的定义可以跨进程,自然不能通过 Java 或者 Kotlin 定义,这就是 AIDL 存在的意义。
Tips: Binder 机制相当复杂,是整个 Android 系统的通信基础,好在我们只需要确保可用,不理解具体的原理也可先用着。想要了解原理推荐一篇大佬的文章:彻底理解Android Binder通信架构
AIDL 的语法类似于 Java,很好上手,直接编写代码编译时遇到问题再查即可。Android Studio 默认的 AIDL 文件模板中有对基本类型的支持说明:
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
仿照这个就可以实现大多数功能了,假设我们提供了静默安装 apk 的服务,接口就这么定义:
// IMCInterface.aidl
interface IMCInterface {
void installApk(String filePath);
}
编译之后,在 Server 项目中继承 IMCInterface.Stub 实现对外提供的 Binder 类:
class MdmControlService(private val context: Context) : IMCInterface.Stub() {
override fun installApk(filePath: String?) {
Log.e("MdmControlService", "installApk file path = $filePath")
// perform install
}
}
最后新建一个 Service,把 MdmControlService 的实例作为 IBinder 通过 onBind 传给其他 Client App 即可。要注意修改 AndroidManifest.xml,给 Service 增加供外部调用的 action 并使 exported = true 和 enabled = true。
<service android:name=".MCService"
android:exported="true"
android:enabled="true">
<intent-filter>
<action android:name="com.github.moqigit.mcservice"/>
</intent-filter>
</service>
// MCService
class MCService : Service() {
private var binder: IMCInterface.Stub? = null
override fun onBind(intent: Intent?): IBinder? {
Log.e("MCService", "onBind")
if (binder == null) {
initBinder()
}
return binder!!
}
private fun initBinder() {
binder = MdmControlService(applicationContext)
}
}
对外的服务就提供好了,可以写 Client 来测试一下。下文有对 Client 的封装代码,测试代码便不再赘述。
现在这种实现有一个明显的问题,Client 可以跨进程调用到 installApk 函数了,但无法得知安装结果,安装成功时还有可能监听系统广播,安装失败的话就无迹可寻了。installApk 结果不能同步返回,我们得加一个「回调」。
二、回调和双向通信
采用回调的方式处理异步操作的结果已经是刻在 Java 程序员 DNA 里的解决方案了,在这个功能中,回调接口也涉及跨进程(Server 触发 Client 接收),回调接口也需要使用 AIDL 定义。
// IMCCallback.aidl
interface IMCCallback {
void onSuccess();
void onFailed(int errorCode, String errorMsg);
}
// 修改 IMCInterface
import com.github.moqigit.mcservice.IMCCallback;
interface IMCInterface{
void installApk(String filePath, IMCCallback cb);
}
跟 IMCInterface 相比,IMCCallback 实现的过程要反过来,IMCCallback.Stub 接口在 Client 中实现,由 Server 调用。
回调的问题可以到此为止了,需求中还缺最后一项。Server App 会收到来自远程的消息,类似于推送消息,这种情况下通信的发起方一定是 Server App,实际上需要双向通信。
三、一对多数据观察
即使有了双向通信机制,我们仍然将 Service App 称为 Server,需要使用服务的 App 称为 Client。考虑到 Server 几乎常驻运行,而 Client 应该只在运行时接收消息,自然应该用观察者模式实现。
Observer 由 Server 提供接口,Client 提供实现,通过 IMCInterface 注册和反注册。
// Observer 定义
interface IMCObserver{
void observe(int status, String message, String json);
}
// in IMCInterface
// …
String registerObserver(IMCObserver observer);
void unregisterObserver(String token);
除了全量分发调用 observe 函数之外,还可能有分发的优先级顺序、中途打断、定向分发、Observer 数量限制等奇奇怪怪的需求,在封装的过程中都能实现。可见 Service 跨进程通信的可定制程度已经接近同一 App 了。
Tips: 实际上在同一 App 中 startActivity 也用到了 Binder 跨进程通信,Binder 的性能很有保障(但也别滥用)
四、封装打包快速接入(简述)
功能都实现了,但还不能用,我们没必要在每个 App 中都复制一份 aidl 文件,应该单独封装成 module 通过依赖的方式接入其他项目。目标 app 添加依赖,创建对象即可连接。
封装方式本身不复杂,根据业务去做就好了,这一节的重点在于当我们想通过 Service 提供服务的时候,必须对外提供一个 Client 的封装,加在一起才是完整的功能。封装之后 Client 调用代码会非常简单:
val mcc = MCC().apply {
eventHandler = {
Log.e("asdfg", "event = $it")
if (it.code == 100) {
val isOpened = it.data.toBoolean()
val c = if (isOpened) {
Color.GREEN
} else {
Color.RED
}
vb.viewIndicator.setBackgroundColor(c)
}
}
}
mcc.connect(this)
vb.btnOpenEyeprotect.setOnClickListener {
mcc.mdmController?.setEyeProtect(true)
}
vb.btnCloseEyeprotect.setOnClickListener {
mcc.mdmController?.setEyeProtect(false)
}
看一下效果,用分屏模式同时打开两个 App 可以直观地看到两个 Client 的行为。由于功能没实现,demo 里用一个 View 表示护眼模式的状态,来看:
冰糖肥肠,鼓掌!.mp3
上面的代码用在项目里肯定还差很多,App 被杀死的时候 unregister 未必能调用到,可能需要其他监控生命周期的机制。出于安全性考虑,也应该有对 Client 的认证机制。Server 的 Service 应该做保活和重启时恢复状态等功能。等等等等。
Service 自定义能力很强,同时也意味着很多功能都必须自己实现,如果不是必须的话,ContentProvider 和 BroadcastReceiver 是更合适的选择。
感谢阅读,任何问题欢迎评论 ~ 新的一年,一起加油吧~