Android進階寶典 -- 告別繁瑣的AIDL吧,手寫IPC通訊框架,5行程式碼實現程序間通訊

語言: CN / TW / HK

對於程序間通訊,很多專案中可能根本沒有涉及到多程序,很多公司的app可能就一個主程序,但是對於程序間通訊,我們也是必須要了解的。

如果在Android中想要實現程序間通訊,有哪些方式呢?

(1)發廣播(sendBroadcast):e.g. 兩個app之間需要通訊,那麼可以通過傳送廣播的形式進行通訊,如果只想單點通訊,可以指定包名。但是這種方式存在的弊端在於傳送方無法判斷接收方是否接收到了廣播,類似於UDP的通訊形式,而且存在丟資料的形式;

(2)Socket通訊:這種屬於Linux層面的程序間通訊了,除此之外,還包括管道、訊號量等,像傳統的IPC程序間通訊需要資料二次拷貝,這種效率是最低的;

(3)AIDL通訊:這種算是Android當中主流的程序間通訊方案,通過Service + Binder的形式進行通訊,具備實時性而且能夠通過回撥得知接收方是否收到資料,弊端在於需要管理維護aidl介面,如果不同業務方需要使用不同的aidl介面,維護的成本會越來越高。

那麼本篇文章並不是說完全丟棄掉AIDL,它依然不失為一個很好的程序間通訊的手段,只是我會封裝一個適用於任意業務場景的IPC程序間通訊框架,這個也是我在自己的專案中使用到的,不需要維護很多的AIDL介面檔案。

有需要原始碼的夥伴,可以去我的github首頁獲取 FastIPC原始碼地址分支:feature/v0.0.1有幫助的話麻煩給點個star⭐️⭐️⭐️

1 服務端 - register

首先這裡先說明一下,就是對於傳統的AIDL使用方式,這裡就不再過多介紹了,這部分還是比較簡單的,有興趣的夥伴們可以去前面的文章中檢視,本文將著重介紹框架層面的邏輯。

那麼IPC程序間通訊,需要兩個端:客戶端和服務端。服務端會提供一個註冊方法,例如客戶端定義的一些服務,通過向服務端註冊來做一個備份,當客戶端呼叫服務端某個方法的時候來返回值。

```kotlin object IPC {

//==========================================

/**
 * 服務端暴露的介面,用於註冊服務使用
 */
fun register(service: Class<*>) {
    Registry.instance.register(service)
}

} ```

其實在註冊的時候,我們的目的肯定是能夠方便地拿到某個服務,並且能夠呼叫這個服務提供的方法,拿到我想要的值;所以在定義服務的時候,需要注意以下兩點:

(1)需要定義一個與當前服務一一對應的serviceId,通過serviceId來獲取服務的例項;

(2)每個服務當中定義的方法同樣需要對應起來,以便拿到服務物件之後,通過反射呼叫其中的方法。

所以在註冊的時候,需要從這兩點入手。

1.1 定義服務唯一標識serviceId

kotlin @Target(AnnotationTarget.TYPE) @Retention(AnnotationRetention.RUNTIME) annotation class ServiceId( val name: String ) 一般來說,如果涉及到反射,最常用的就是通過註解給Class做標記,因為通過反射能夠拿到類上標記的註解,就能夠拿到對應的serviceId。 ```kotlin class Registry {

//=======================================
/**用於儲存 serviceId 對應的服務 class物件*/
private val serviceMaps: ConcurrentHashMap<String, Class<*>> by lazy {
    ConcurrentHashMap()
}

/**用於儲存 服務中全部的方法*/
private val methodsMap: ConcurrentHashMap<Class<*>, ConcurrentHashMap<String, Method>> by lazy {
    ConcurrentHashMap()
}


//=======================================

/**
 * 服務端註冊方法
 * @param service 服務class物件
 */
fun register(service: Class<*>) {

    // 獲取serviceId與服務一一對應
    val serviceIdAnnotation = service.getAnnotation(ServiceId::class.java)
        ?: throw IllegalArgumentException("只有標記@ServiceId的服務才能夠被註冊")
    //獲取serviceId
    val name = serviceIdAnnotation.name
    serviceMaps[name] = service
    //temp array
    val methods: ConcurrentHashMap<String, Method> = ConcurrentHashMap()
    // 獲取服務當中的全部方法
    for (method in service.declaredMethods) {

        //這裡需要注意,因為方法中存在過載方法,所以不能把方法名當做key,需要加上引數
        val buffer = StringBuffer()
        buffer.append(method.name).append("(")
        val params = method.parameterTypes
        if (params.size > 0) {
            buffer.append(params[0].name)
        }
        for (index in 1 until params.size) {
            buffer.append(",").append(params[index].name)
        }
        buffer.append(")")
        //儲存
        methods[buffer.toString()] = method
    }
    //存入方法表
    methodsMap[service] = methods
}

companion object {
    val instance by lazy { Registry() }
}

} ``` 通過上面的register方法,當傳入定義的服務class物件的時候,首先獲取到服務上標記的@ServiceId註解,注意這裡如果要註冊必須標記,否則直接拋異常;拿到serviceId之後,存入到serviceMaps中。

然後需要獲取服務中的全部方法,因為考慮到過載方法的存在,所以不能單單以方法名作為key,而是需要把引數也加上,因此這裡做了一個邏輯就是將方法名與引數名組合一個key,存入到方法表中。

這樣註冊任務就完成了,其實還是比較簡單的,關鍵在於完成2個表:服務表和方法表的初始化以及資料儲存功能

1.2 使用方式

```kotlin @ServiceId("UserManagerService") interface IUserManager {

fun getUserInfo(): User?
fun setUserInfo(user: User)
fun getUserId(): Int
fun setUserId(id: Int)

} 假設專案中有一個使用者資訊管理的服務,這個服務用於給所有的App提供使用者資訊查詢。kotlin @ServiceId("UserManagerService") class UserManager : IUserManager {

private var user: User? = null
private var userId: Int = 0

override fun getUserInfo(): User? {
    return user
}

override fun setUserInfo(user: User) {
    this.user = user
}

override fun getUserId(): Int {
    return userId
}

override fun setUserId(id: Int) {
    this.userId = id
}

} ``` 使用者中心可以註冊這個服務,並且呼叫setUserInfo方法儲存使用者資訊,那麼其他App(客戶端)連線這個服務之後,就可以呼叫getUserInfo這個方法,獲取使用者資訊,從而完成程序間通訊。

java 2023-01-23 22:15:54.729 13361-13361/com.lay.learn.asm E/TAG: entrySet key class com.lay.learn.asm.binder.UserManager 2023-01-23 22:15:54.729 13361-13361/com.lay.learn.asm E/TAG: mapValue key setUserInfo(com.lay.learn.asm.binder.User) 2023-01-23 22:15:54.729 13361-13361/com.lay.learn.asm E/TAG: mapValue value public void com.lay.learn.asm.binder.UserManager.setUserInfo(com.lay.learn.asm.binder.User) 2023-01-23 22:15:54.729 13361-13361/com.lay.learn.asm E/TAG: mapValue key getUserInfo() 2023-01-23 22:15:54.729 13361-13361/com.lay.learn.asm E/TAG: mapValue value public com.lay.learn.asm.binder.User com.lay.learn.asm.binder.UserManager.getUserInfo() 2023-01-23 22:15:54.729 13361-13361/com.lay.learn.asm E/TAG: mapValue key getUserId() 2023-01-23 22:15:54.729 13361-13361/com.lay.learn.asm E/TAG: mapValue value public int com.lay.learn.asm.binder.UserManager.getUserId() 2023-01-23 22:15:54.729 13361-13361/com.lay.learn.asm E/TAG: mapValue key setUserId(int) 2023-01-23 22:15:54.729 13361-13361/com.lay.learn.asm E/TAG: mapValue value public void com.lay.learn.asm.binder.UserManager.setUserId(int) 我們看呼叫register方法之後,每個方法的key值都是跟引數繫結在一起,這樣服務端註冊就完成了。

2 客戶端與服務端的通訊協議

對於客戶端的連線,其實就是繫結服務,那麼這裡就會使用到AIDL通訊,但是跟傳統的相比,我們是將AIDL封裝到框架層內部,對於使用者來說是無感知的。

2.1 建立IPCService

這個服務就是用來完成程序間通訊的,客戶端需要與這個服務建立連線,通過服務端分發訊息,或者接收客戶端傳送來的訊息。

kotlin abstract class IPCService : Service() { override fun onBind(intent: Intent?): IBinder? { return null } } 這裡我定義了一個抽象的Service基類,為啥要這麼做,前面我們提到過是因為整個專案中不可能只有一個服務,因為業務眾多,為了保證單一職責,需要劃分不同的型別,所以在框架中會衍生多個實現類,不同業務方可以註冊這些服務,當然也可以自定義服務繼承IPCService。 kotlin class IPCService01 : IPCService() { }

在IPCService的onBind需要返回一個Binder物件,因此需要建立aidl檔案。

2.2 定義通訊協議

像我們在請求介面的時候,通常也是向服務端發起一個請求(Request),然後得到服務端的一個響應(Response),因此在IPC通訊的的時候,也可以根據這種方式建立通訊協議。

```kotlin data class Request( val type: Int, val serviceId: String?, val methodName: String?, val params: Array? ) : Serializable, Parcelable { //===================================== /*請求型別/ //獲取例項的物件 val GET_INSTANCE = "getInstance" //執行方法 val INVOKE_METHOD = "invokeMethod"

//=======================================

constructor(parcel: Parcel) : this(
    parcel.readInt(),
    parcel.readString(),
    parcel.readString(),
    parcel.createTypedArray(Parameters.CREATOR)
)

override fun writeToParcel(parcel: Parcel, flags: Int) {
    parcel.writeInt(type)
    parcel.writeString(serviceId)
    parcel.writeString(methodName)
}

override fun describeContents(): Int {
    return 0
}

override fun equals(other: Any?): Boolean {
    if (this === other) return true
    if (javaClass != other?.javaClass) return false

    other as Request

    if (type != other.type) return false
    if (serviceId != other.serviceId) return false
    if (methodName != other.methodName) return false
    if (params != null) {
        if (other.params == null) return false
        if (!params.contentEquals(other.params)) return false
    } else if (other.params != null) return false

    return true
}

override fun hashCode(): Int {
    var result = type
    result = 31 * result + (serviceId?.hashCode() ?: 0)
    result = 31 * result + (methodName?.hashCode() ?: 0)
    result = 31 * result + (params?.contentHashCode() ?: 0)
    return result
}

companion object CREATOR : Parcelable.Creator<Request> {
    override fun createFromParcel(parcel: Parcel): Request {
        return Request(parcel)
    }

    override fun newArray(size: Int): Array<Request?> {
        return arrayOfNulls(size)
    }
}

} ``` 對於客戶端來說,致力於發起請求,請求實體類Request引數介紹如下:

type表示請求的型別,包括兩種分別是:執行靜態方法和執行普通方法(考慮到反射傳參);

serviceId表示請求的服務id,要請求哪個服務,便可以獲取到這個服務的例項物件,呼叫服務中提供的方法;

methodName表示要請求的方法名,也是在serviceId服務中定義的方法;

params表示請求的方法引數集合,我們在服務端註冊的時候,方法名 + 引數名 作為key,因此需要知道請求的方法引數,以便獲取到Method物件。

```kotlin data class Response( val value:String?, val result:Boolean ):Parcelable { @SuppressLint("NewApi") constructor(parcel: Parcel) : this( parcel.readString(), parcel.readBoolean() )

override fun writeToParcel(parcel: Parcel, flags: Int) {
    parcel.writeString(value)
    parcel.writeByte(if (result) 1 else 0)
}

override fun describeContents(): Int {
    return 0
}

companion object CREATOR : Parcelable.Creator<Response> {
    override fun createFromParcel(parcel: Parcel): Response {
        return Response(parcel)
    }

    override fun newArray(size: Int): Array<Response?> {
        return arrayOfNulls(size)
    }
}

} ``` 對於服務端來說,在接收到請求之後,需要針對具體的請求返回相應的結果,Response實體類引數介紹:

result表示請求成功或者失敗;

value表示服務端返回的結果,是一個json字串。

因此定義aidl介面檔案如下,輸入一個請求之後,返回一個服務端的響應。

java interface IIPCServiceInterface { Response send(in Request request); } 這樣IPCService就可以將aidl生成的Stub類作為Binder物件返回。 ``` abstract class IPCService : Service() {

override fun onBind(intent: Intent?): IBinder? {
    return BINDERS
}

companion object BINDERS : IIPCServiceInterface.Stub() {
    override fun send(request: Request?): Response? {

        when(request?.type){

            REQUEST.GET_INSTANCE.ordinal->{

            }
            REQUEST.INVOKE_METHOD.ordinal->{

            }
        }

        return null
    }
}

} ```

2.3 內部通訊協議完善

當客戶端發起請求,想要執行某個方法的時候,首先服務端會先向Registery中查詢註冊的服務,從而找到這個要執行的方法,這個流程是在內部完成。

```kotlin override fun send(request: Request?): Response? { //獲取服務物件id val serviceId = request?.serviceId val methodName = request?.methodName val params = request?.params // 反序列化拿到具體的引數型別 val neededParams = parseParameters(params) val method = Registry.instance.findMethod(serviceId, methodName, neededParams) Log.e("TAG", "method $method") Log.e("TAG", "neededParams $neededParams") when (request?.type) {

    REQUEST_TYPE.GET_INSTANCE.ordinal -> {
        //==========執行靜態方法
        try {
            var instance: Any? = null
            instance = if (neededParams == null || neededParams.isEmpty()) {
                method?.invoke(null)
            } else {
                method?.invoke(null, neededParams)
            }
            if (instance == null) {
                return Response("instance == null", -101)
            }
            //儲存例項物件
            Registry.instance.setServiceInstance(serviceId ?: "", instance)
            return Response(null, 200)
        } catch (e: Exception) {
            return Response("${e.message}", -102)
        }
    }
    REQUEST_TYPE.INVOKE_METHOD.ordinal -> {
        //==============執行普通方法
        val instance = Registry.instance.getServiceInstance(serviceId)
        if (instance == null) {
            return Response("instance == null ", -103)
        }
        //方法執行返回的結果
        return try {

            val result = if (neededParams == null || neededParams.isEmpty()) {
                method?.invoke(instance)
            } else {
                method?.invoke(instance, neededParams)
            }
            Response(gson.toJson(result), 200)
        } catch (e: Exception) {
            Response("${e.message}", -104)
        }

    }
}

return null

} 當客戶端發起請求時,會將請求的引數封裝到Request中,在服務端接收到請求後,就會解析這些引數,變成Method執行時需要傳入的引數。kotlin private fun parseParameters(params: Array?): Array? { if (params == null || params.isEmpty()) { return null } val objects = arrayOfNulls(params.size) params.forEachIndexed { index, parameters -> objects[index] = gson.fromJson(parameters.value, Class.forName(parameters.className)) } return objects } 例如使用者中心呼叫setUserInfo方法時,需要傳入一個User實體類,如下所示: UserManager().setUserInfo(User("ming",25)) ``` 那麼在呼叫這個方法的時候,首先會把這個實體類轉成一個JSON字串,例如:

xml { "name":"ming", "age":25 } 為啥要”多此一舉“呢?其實這種處理方式是最快速直接的,轉成json字串之後,能夠最大限度地降低資料傳輸的大小,等到服務端處理這個方法的時候,再把Request中的params反json轉成User物件即可。

```kotlin fun findMethod(serviceId: String?, methodName: String?, neededParams: Array?): Method? { //獲取服務 val serviceClazz = serviceMaps[serviceId] ?: return null //獲取方法集合 val methods = methodsMap[serviceClazz] ?: return null return methods[rebuildParamsFunc(methodName, neededParams)] }

private fun rebuildParamsFunc(methodName: String?, params: Array?): String {

val stringBuffer = StringBuffer()
stringBuffer.append(methodName).append("(")

if (params == null || params.isEmpty()) {
    stringBuffer.append(")")
    return stringBuffer.toString()
}
stringBuffer.append(params[0]?.javaClass?.name)
for (index in 1 until params.size) {
    stringBuffer.append(",").append(params[index]?.javaClass?.name)
}
stringBuffer.append(")")
return stringBuffer.toString()

} ``` 那麼在查詢註冊方法的時候就簡單多了,直接抽絲剝繭一層一層取到最終的Method。在拿到Method之後,這裡是有2種處理方式,一種是通過靜態單例的形式拿到例項物件,並儲存在服務端;另一種就是執行普通方法,因為在反射的時候需要拿到類的例項物件才能呼叫,所以才在GET_INSTANCE的時候存一遍

3 客戶端 - connect

在第二節中,我們已經完成了通訊協議的建設,最終一步就是客戶端通過繫結服務,向服務端發起通訊了。

3.1 bindService

```kotlin /* * 繫結服務 * / fun connect( context: Context, pkgName: String, action: String = "", service: Class ) { val intent = Intent() if (pkgName.isEmpty()) { //同app內的不同程序 intent.setClass(context, service) } else { //不同APP之間進行通訊 intent.setPackage(pkgName) intent.setAction(action) } //繫結服務 context.bindService(intent, IpcServiceConnection(service), Context.BIND_AUTO_CREATE) }

inner class IpcServiceConnection(val simpleService: Class) : ServiceConnection {

override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
    val mService = IIPCServiceInterface.Stub.asInterface(service) as IIPCServiceInterface
    binders[simpleService] = mService
}

override fun onServiceDisconnected(name: ComponentName?) {
    //斷連之後,直接移除即可
    binders.remove(simpleService)
}

} ``` 對於繫結服務這塊,相信夥伴們也很熟悉了,這個需要說一點的就是,在Android 5.0以後,啟動服務不能只依賴action啟動,還需要指定應用包名,否則就會報錯。

在服務連線成功之後,即回撥onServiceConnected方法的時候,需要拿到服務端的一個代理物件,即IIPCServiceInterface的例項物件,然後儲存在binders集合中,key為繫結的服務類class物件,value就是對應的服務端的代理物件。

kotlin fun send( type: Int, service: Class<out IPCService>, serviceId: String, methodName: String, params: Array<Parameters> ): Response? { //建立請求 val request = Request(type, serviceId, methodName, params) //發起請求 return try { binders[service]?.send(request) } catch (e: Exception) { null } } 當拿到服務端的代理物件之後,就可以在客戶端呼叫send方法向服務端傳送訊息。

```kotlin class Channel {

//====================================
/**每個服務對應的Binder物件*/
private val binders: ConcurrentHashMap<Class<out IPCService>, IIPCServiceInterface> by lazy {
    ConcurrentHashMap()
}

//====================================

/**
 * 繫結服務
 *
 */
fun connect(
    context: Context,
    pkgName: String,
    action: String = "",
    service: Class<out IPCService>
) {
    val intent = Intent()
    if (pkgName.isEmpty()) {
        intent.setClass(context, service)
    } else {
        intent.setPackage(pkgName)
        intent.setAction(action)
        intent.setClass(context, service)
    }
    //繫結服務
    context.bindService(intent, IpcServiceConnection(service), Context.BIND_AUTO_CREATE)
}

inner class IpcServiceConnection(val simpleService: Class<out IPCService>) : ServiceConnection {

    override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
        val mService = IIPCServiceInterface.Stub.asInterface(service) as IIPCServiceInterface
        binders[simpleService] = mService
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        //斷連之後,直接移除即可
        binders.remove(simpleService)
    }
}


fun send(
    type: Int,
    service: Class<out IPCService>,
    serviceId: String,
    methodName: String,
    params: Array<Parameters>
): Response? {
    //建立請求
    val request = Request(type, serviceId, methodName, params)
    //發起請求
    return try {
        binders[service]?.send(request)
    } catch (e: Exception) {
        null
    }
}


companion object {
    private val instance by lazy {
        Channel()
    }

    /**
     * 獲取單例物件
     */
    fun getDefault(): Channel {
        return instance
    }
}

} ```

3.2 動態代理獲取介面例項

回到1.2小節中,我們定義了一個IUserManager介面,通過前面我們定義的通訊協議,只要我們獲取了IUserManager的例項物件,那麼就能夠呼叫其中的任意普通方法,所以在客戶端需要設定一個獲取介面例項物件的方法。

```kotlin fun getInstanceWithName( service: Class, classType: Class, clazz: Class<*>, methodName: String, params: Array ): T? {

//獲取serviceId
val serviceId = clazz.getAnnotation(ServiceId::class.java)

val response = Channel.getDefault()
    .send(REQUEST.GET_INSTANCE.ordinal, service, serviceId.name, methodName, params)
Log.e("TAG", "response $response")
if (response != null && response.result) {
    //請求成功,返回介面例項物件
    return Proxy.newProxyInstance(
        classType.classLoader,
        arrayOf(classType),
        IPCInvocationHandler()
    ) as T
}

return null

} ``` 當我們通過客戶端傳送一個獲取單例的請求後,如果成功了,那麼就直接返回這個介面的單例物件,這裡直接使用動態代理的方式返回一個介面例項物件,那麼後續執行這個介面的方法時,會直接走到IPCInvocationHandler的invoke方法中。

``` class IPCInvocationHandler( val service: Class, val serviceId: String? ) : InvocationHandler {

private val gson = Gson()

override fun invoke(proxy: Any?, method: Method?, args: Array<out Any>?): Any? {

    //執行客戶端傳送方法請求
    val response = Channel.getDefault()
        .send(
            REQUEST.INVOKE_METHOD.ordinal,
            service,
            serviceId,
            method?.name ?: "",
            args
        )
    //拿到服務端返回的結果
    if (response != null && response.result) {
        //反序列化得到結果
        return gson.fromJson(response.value, method?.returnType)
    }


    return null
}

} ``` 因為服務端在拿到Method的返回結果時,將javabean轉換為了json字串,因此在IPCInvocationHandler中,當呼叫介面中方法獲取結果之後,用Gson將json轉換為javabean物件,那麼就直接獲取到了結果。

3.3 框架使用

服務端: UserManager2.getDefault().setUserInfo(User("ming", 25)) IPC.register(UserManager2::class.java) 同時在服務端需要註冊一個IPCService的例項,這裡用的是IPCService01 xml <service android:name=".UserService" android:enabled="true" android:exported="true" /> <service android:name="com.lay.ipc.service.IPCService01" android:enabled="true" android:exported="true"> <intent-filter> <action android:name="android.intent.action.GET_USER_INFO" /> </intent-filter> </service> 客戶端:

呼叫connect方法,需要繫結服務端的服務,傳入包名和action kotlin IPC.connect( this, "com.lay.learn.asm", "android.intent.action.GET_USER_INFO", IPCService01::class.java )

首先獲取IUserManager的例項,注意這裡要和服務端註冊的UserManager2是同一個ServiceId,而且介面、javabean需要存放在與服務端一樣的資料夾下kotlin val userManager = IPC.getInstanceWithName( IPCService01::class.java, IUserManager::class.java, "getDefault", null ) val info = userManager?.getUserInfo() 通過動態代理拿到介面的例項物件,只要呼叫介面中的方法,就會進入到InvocationHandler中的invoke方法,在這個方法中,通過查詢服務端註冊的方法名從而找到對應的Method,通過反射呼叫拿到UserManager中的方法返回值。

這樣其實就通過5-6行程式碼,就完成了程序間通訊,是不是比我們在使用AIDL的時候要方便地許多。

4 總結

如果我們面對下面這個類,如果這個類是個私有類,外部沒法呼叫,想通過反射的方式呼叫其中某個方法。 ``` @ServiceId(name = "UserManagerService") public class UserManager2 implements IUserManager {

private static UserManager2 userManager2 = new UserManager2();

public static UserManager2 getDefault() {
    return userManager2;
}

private User user;

@Nullable
@Override
public User getUserInfo() {
    return user;
}

@Override
public void setUserInfo(@NonNull User user) {
    this.user = user;
}

@Override
public int getUserId() {
    return 0;
}

@Override
public void setUserId(int id) {

}

} ``` 那麼我們可以這樣做:

val method = UserManager2::class.java.getDeclaredMethod("getUserInfo") method.isAccessible = true method.invoke(this,params)

其實這個框架的原理就是上面這幾行程式碼所能夠完成的事;通過服務端註冊的形式,將UserManager2中所有的方法Method收集起來;當另一個程序,也就是客戶端想要呼叫其中某個方法的時候,通過方法名來獲取到對應的Method,呼叫這個方法得到最終的返回值