[Android] Service App : 通过 Binder 对外提供服务

1,629 阅读3分钟

摘要|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 是更合适的选择。

感谢阅读,任何问题欢迎评论 ~ 新的一年,一起加油吧~