一个简单的跨进程通信SDK

前言

介绍下工作中一个有意思的跨进程SDK,其原理是通过ContentProvider+Binder来实现跨进程通信的封装,且客户端与服务提供端的接入成本较低。

客户端在发起调用时仅需要几行代码即可实现跨进程通信调用。

kotlin 复制代码
val methodName = "methodName" 
val params = "{\"key\":\"value\"}" 
val res = apiClientImpl.call(methodName, params) Log.d(TAG, "API call result: $res")

客户端不知道服务端是谁,服务端也不知道自己的客户端是谁,全由SDK来充当服务发现者,颇有SOA架构的味道。

周末抽空简单复现了下实现。

前置要求

客户端

实现ApiClient

创建继承自SDK库中的 ApiClient 的实现类:

kotlin 复制代码
class ApiClientImpl(val activityContext: Context) : ApiClient() {
    override fun getModuleName(): String {
        return "demo"
    }

    override fun getAppId(): String {
        return "server"
    }

    override fun getContext(): Context {
        return activityContext
    }

}
  • getAppId()是用于获取服务应用的包名。
  • getModuleName()是识别获取服务应用的服务名。
  • getContext()是用来获取服务端的ContentProvider。

服务端

1. 注册ContentProvider

在服务端的 AndroidManifest.xml 中注册SDK中的ApiPublisher

xml 复制代码
<provider
    android:name="com.ben.ipc.ApiPublisher"
    android:authorities="com.ben.server.provider"
    android:enabled="true"
    android:exported="true" />

2. 实现服务逻辑

创建实现 IpcServerListener 接口的服务类:

kotlin 复制代码
object ServerImplementation : IpcServerListener {

    val TAG = ServerImplementation::class.simpleName

    fun init() {
        Log.d(TAG, "ServerImplementation initialized")
        RemoteApiImpl.listener = this
    }

    override fun onCall(
        moduleName: String,
        method: String?,
        arg: String?
    ): String {
        Log.d(TAG, "onCall moduleName: $moduleName, method: $method, arg: $arg")
        return "Invokes method $method with arg $arg in module $moduleName"
    }
    
}

SDK实现

SDK采用了分层架构设计,通过AIDL和ContentProvider实现跨进程通信。以下是各个核心组件的实现细节:

1. 核心接口层

AIDL接口定义

定义了跨进程通信的标准接口:

aidl 复制代码
interface IRemoteApi {
    String onCall(String moduleName, String method, String param);
}

这个接口采用了通用的方法调用模式,通过 moduleNamemethodparam 三个参数实现灵活的服务调用。

  • moduleName是用于区分同一服务端的不同服务类或服务模块。
  • method是要调用具体的方法。
  • param的参数。

2. 客户端实现层

ApiClient抽象基类

提供了客户端的抽象接口:

kotlin 复制代码
abstract class ApiClient {
    abstract fun getModuleName(): String
    abstract fun getAppId(): String
    abstract fun getContext(): Context
    
    fun call(methodName: String, paras: String): String {
        return ApiManager.call(getAppId(), getModuleName(), methodName, paras, getContext())
    }
}

设计特点:

  • 抽象化客户端配置信息(模块名、应用ID、上下文)
  • 提供统一的调用入口 call() 方法
  • 将具体的路由逻辑委托给 ApiManager
ApiManager服务管理器

负责应用ID到包名的映射和路由:

kotlin 复制代码
object ApiManager {
    private val appIdToPackageId = mutableMapOf<String, String>().apply {
        put("server", "com.ben.server")
    }
    
    fun call(appId: String, moduleName: String, method: String, params: String, context: Context): String {
        val packageId = appIdToPackageId[appId]
            ?: throw IllegalArgumentException("No package ID found for app ID: $appId")
        return Router.route(packageId, moduleName, method, params, context)
    }
}

核心功能:

  • 维护应用ID与包名的映射关系
  • 提供服务发现机制
  • 参数验证和错误处理
Router路由器

实现具体的跨进程通信逻辑:

kotlin 复制代码
object Router {
    fun route(packageId: String, moduleName: String, method: String, arg: String?, context: Context): String {
        val builder = Uri.Builder()
        builder.scheme("content").authority(packageId + "." + ApiPublisher::class.simpleName)
        val uri = builder.build()
        
        val contentResolver = context.contentResolver
        val contentProviderClient = contentResolver.acquireContentProviderClient(uri)
        
        if (contentProviderClient != null) {
            val bundle = contentProviderClient.call(method, arg, null)
            val binder = bundle?.getBinder("binder")
            val remoteApiImpl = IRemoteApi.Stub.asInterface(binder)
            val res = remoteApiImpl.onCall(moduleName, method, arg)
            contentProviderClient.release()
            return res
        }
        return "-1"
    }
}

实现细节:

  • 构建ContentProvider的URI(格式:content://{packageId}.ApiPublisher
  • 通过ContentResolver获取远程ContentProvider
  • 调用ContentProvider的 call() 方法获取IBinder
  • 将IBinder转换为AIDL接口代理
  • 执行远程方法调用并返回结果

3. 服务端实现层

ApiPublisher服务发布者

作为ContentProvider暴露服务:

kotlin 复制代码
class ApiPublisher : ContentProvider() {
    private val remoteApi: IBinder = RemoteApiImpl
    
    override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
        val res = Bundle()
        res.putBinder("binder", remoteApi)
        return res
    }
}

核心功能:

  • 继承ContentProvider提供跨进程访问能力
  • call() 方法中返回IBinder对象
  • 作为服务注册中心的角色
RemoteApiImpl服务实现

实现AIDL接口:

kotlin 复制代码
object RemoteApiImpl : IRemoteApi.Stub() {
    var listener: IpcServerListener? = null
    
    override fun onCall(moduleName: String, method: String?, param: String?): String {
        return listener?.onCall(moduleName, method, param) ?: "No listener set"
    }
}

设计模式:

  • 采用委托模式,将具体业务逻辑委托给 IpcServerListener
  • 作为AIDL接口的具体实现,处理跨进程调用
  • 使用单例模式确保全局唯一
IpcServerListener业务接口

定义服务端业务接口:

kotlin 复制代码
interface IpcServerListener {
    fun onCall(moduleName: String, method: String?, arg: String?): String
}

5. SDK架构优势

分层解耦
  • 接口层:AIDL定义标准通信协议
  • 客户端层:ApiClient、ApiManager、Router处理客户端逻辑
  • 服务端层:ApiPublisher、RemoteApiImpl处理服务端逻辑
  • 业务层:IpcServerListener处理具体业务逻辑
扩展性设计
  • 服务发现:通过ApiManager的映射表支持多服务注册
  • 协议统一:通过AIDL接口统一通信协议
  • 业务解耦:通过监听器模式分离框架和业务代码
通信流程
  1. 客户端调用 → ApiClient.call()
  2. 服务发现 → ApiManager查找目标包名
  3. 路由转发 → Router构建URI并获取ContentProvider
  4. 获取服务 → ContentProvider返回IBinder
  5. 远程调用 → 通过AIDL代理调用远程方法
  6. 业务处理 → RemoteApiImpl委托给IpcServerListener
  7. 结果返回 → 沿原路径返回调用结果

这种设计实现了客户端和服务端的完全解耦,支持动态服务发现,具有良好的可扩展性和维护性。

写在最后

上述的代码是简化了的实现,是存在可优化点的,如缓存Binder实例,如在SDK侧进行方法名的校验等等。

有错误欢迎指出。

相关推荐
MiyamuraMiyako21 分钟前
从 0 到发布:Gradle 插件双平台(MavenCentral + Plugin Portal)发布记录与避坑
android
NRatel1 小时前
Unity 游戏提升 Android TargetVersion 相关记录
android·游戏·unity·提升版本
叽哥3 小时前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走4 小时前
创建自定义语音录制View
android·前端
用户2018792831674 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831674 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker5 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong6 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil7 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin
程序员码歌13 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端