Android學(xué)習(xí)感悟之進(jìn)程間通信——AIDL詳解

本篇包含進(jìn)程間通信——AIDL所涉及到的知識(shí)的自我總結(jié)(內(nèi)容詳細(xì))

通過前段時(shí)間對(duì)AIDL的學(xué)習(xí)以及最近一些資料的查閱,特在此總結(jié)一下我所理解到的AIDL,下文將從以下幾個(gè)方面來分析。

  • WHY(目的)
  • Support(支持的數(shù)據(jù)類型)
  • How(如何使用)
  • What's The Meaning(AIDL生成的java文件各部分的含義)
  • Permission(客戶端調(diào)用時(shí)的權(quán)限驗(yàn)證)
  • Best Practice(使用連接池管理)

注:在學(xué)習(xí)AIDL之前需要有一定的基礎(chǔ),需要了解序列化以及Service的使用。

一、WHY

首先進(jìn)程間通信(IPC,Inter-process Communication)的方式有很多,例如Bundle、文件共享、AIDL、Messenger、ContentProvider以及Socket,方法是多種多樣,而我們更應(yīng)該根據(jù)自己的情況去選擇合適的方式,下面列舉出每種方式的適用場景:

名稱 優(yōu)點(diǎn) 缺點(diǎn) 適用場景
Bundle 簡單易用 只能傳輸Bundle支持的數(shù)據(jù)類型 四大組件間的進(jìn)程通信
文件共享 簡單易用 不適合高并發(fā)場景,無法及時(shí)通信 無高并發(fā)訪問,交換簡單數(shù)據(jù),實(shí)時(shí)性不高
AIDL 功能強(qiáng)大,支持一對(duì)多并發(fā)通信,支持及時(shí)通信 使用復(fù)雜,需要處理線程同步 一對(duì)多且有遠(yuǎn)程調(diào)用需求
Messenger 功能一般,支持一對(duì)多串行,支持實(shí)時(shí)通信 不能很好處理高并發(fā),不支持遠(yuǎn)程調(diào)用,數(shù)據(jù)只能使用Message傳輸 低并發(fā)的一對(duì)多即時(shí)通信,無須直接返回結(jié)果,無遠(yuǎn)程調(diào)用
ContentProvider 數(shù)據(jù)訪問方面功能強(qiáng)大,支持一對(duì)多并發(fā)數(shù)據(jù)共享 受約束的AIDL,主要提供數(shù)據(jù)的增刪改查 一對(duì)多進(jìn)程間數(shù)據(jù)共享
Socket 功能強(qiáng)大,支持一對(duì)多并發(fā)實(shí)時(shí)通信 實(shí)現(xiàn)復(fù)雜,不支持直接的遠(yuǎn)程調(diào)用 網(wǎng)絡(luò)數(shù)據(jù)交互

注:該表格信息來自《Android開發(fā)藝術(shù)探究》

從上表格我們可以看出,AIDL出現(xiàn)的目的就是為了解決一對(duì)多并發(fā)及時(shí)通信了,雖然socket也支持,但其更多應(yīng)用于網(wǎng)絡(luò)數(shù)據(jù)交互,所以在Android中,google為我們提供了AIDL來方便開發(fā)者實(shí)現(xiàn)該功能。

二、Support

在進(jìn)程間通信中,對(duì)象的引用是無法傳遞的,因?yàn)槠鋬?nèi)存地址都不在一個(gè)區(qū)域內(nèi),所以數(shù)據(jù)的傳輸都需要序列化后才行,基礎(chǔ)數(shù)據(jù)類型自然是沒問題,系統(tǒng)提供的ArrayList以及HashMap也實(shí)現(xiàn)了序列化,所以對(duì)于自定義的對(duì)象實(shí)現(xiàn)序列化就只能實(shí)現(xiàn)serializable或者parcelable接口,其中前者是java提供的,后者是Android提供的且效率更高,所以在之后的數(shù)據(jù)序列化中都只使用實(shí)現(xiàn)parcelable接口的方法即可,總的來說支持的數(shù)據(jù)類型包括:

  • 基本數(shù)據(jù)類型:int、long、char、boolean、double等
  • 系統(tǒng)已實(shí)現(xiàn)類型:String、CharSequence、ArrayList、HashMap(后兩個(gè)的每個(gè)元素也得被AIDL支持才行)
  • 正確實(shí)現(xiàn)parcelable接口的對(duì)象
  • 所有AIDL本身

對(duì)于自定義實(shí)現(xiàn)parcelable接口的類以及AIDL都必須通過import導(dǎo)入完全限定名才可以使用,而實(shí)現(xiàn)parcelable接口的類也需要?jiǎng)?chuàng)建一個(gè)與之對(duì)應(yīng)的aidl文件才能被識(shí)別,例如:

#Person.java文件
package net.arvin.androidart.aidl;
//省略導(dǎo)入的包
public class Person implements Parcelable {
//省略具體內(nèi)容
}
 
#Person.aidl文件
package net.arvin.androidart.aidl;parcelable Person;

其中aidl的包名要相同才能正確的找到相應(yīng)的java文件。

除此之外還有一個(gè)知識(shí)點(diǎn)那就是對(duì)于非基礎(chǔ)數(shù)據(jù)類型的類型,在aidl中作為參數(shù)使用時(shí)有三個(gè)標(biāo)記,分別時(shí)in、out、inout,表示數(shù)據(jù)的流向:

名稱 含義
in 表示能讀取到傳入的對(duì)象中的值,而修改后不會(huì)修改傳入對(duì)象的內(nèi)容
out 表示讀取到傳入的對(duì)象為空,修改后會(huì)把傳入對(duì)象的內(nèi)容改為修改的內(nèi)容
inout 表示既能讀取到對(duì)象,又能修改對(duì)象,效果就好比統(tǒng)一進(jìn)程中對(duì)象的傳遞一樣

三、How

這里的實(shí)現(xiàn)時(shí)基于AS的(Eclipse只是創(chuàng)建的文件放置的目錄不同,具體細(xì)節(jié)這里就不再介紹),對(duì)于如何實(shí)現(xiàn)我認(rèn)為有以下三部分:

  • 定義AIDL接口
  • 在服務(wù)端返回其Binder以及所需實(shí)現(xiàn)的接口
  • 在客戶端拿到Binder進(jìn)行調(diào)用

下面就一步一步的來介紹如何實(shí)現(xiàn),現(xiàn)在就以客戶端需要向服務(wù)端添加和查詢?nèi)宋镄畔槔?,其中人物信息只包含名稱和年齡即可(這里就不使用基本數(shù)據(jù)類型了,因?yàn)榛緮?shù)據(jù)類型會(huì)少了定義AIDL支持的對(duì)象這一步)

1、定義AIDL接口

a、創(chuàng)建aidl目錄

在src/main下創(chuàng)建一個(gè)aidl文件夾,再創(chuàng)建一個(gè)包,例如net.arvin.androidart.aidl,建議讓所有的aidl在同一個(gè)包下,會(huì)更方便。

b、定義Person對(duì)象并實(shí)現(xiàn)parcelable接口

定義和序列化的實(shí)現(xiàn)這里就不貼代碼了,序列化可通過AS插件生成,需要注意的是這時(shí)候只支持in那種標(biāo)記的數(shù)據(jù)流向,若要支持out,則還需定義readFromParcel(Parcel in)方法,具體原因在解釋每一部分的含義時(shí)會(huì)介紹。只需要注意,需要在a中所創(chuàng)建的包下創(chuàng)建Person.aidl,然后也需要在main/java下的net.arvin.androidart.aidl包中創(chuàng)建Person.java,其主要原因是gradle默認(rèn)的java文件都在main/java文件夾下(這里就不提供修改方法了,修改后就可以讓.java文件定義在aidl的文件下就可以了),這里就先這樣麻煩一點(diǎn)吧。

c、定義通信的AIDL接口

代碼很簡單,就是在剛才創(chuàng)建的Person.aidl包下創(chuàng)建IPersonCount.aidl文件(文件名可隨便定義,后綴卻必須為.aidl),內(nèi)容如下:

package net.arvin.androidart.aidl;

import net.arvin.androidart.aidl.Person;

interface IPersonCount {
    boolean addPerson(in Person person);
    List<Person> getPersons();
}
d、生成Binder

上面三步寫好后就可以點(diǎn)擊build->make project,如果沒有編譯出錯(cuò),那么恭喜你,第一步已經(jīng)成功了。那么你就可以在你的那個(gè)module的build/generated/source/aidl/debug/packagename/下看到所生成的IPersonCount.java了,其中有個(gè)靜態(tài)內(nèi)部類Stub就是之后需要的Binder了,其他的我們就暫時(shí)先不看。

2、在服務(wù)端返回其Binder以及所需實(shí)現(xiàn)的接口

注意,對(duì)于進(jìn)程間通信有兩種情況,一種在同一個(gè)應(yīng)用內(nèi),另一種是在不同的應(yīng)用內(nèi);同一應(yīng)用就沒啥可說就可少了接下來的一步,若是不同應(yīng)用內(nèi),我們還需要把第一步中定義的所有文件原封不動(dòng)的拷貝到另一個(gè)應(yīng)用中,并編譯,假如第一步中我們在服務(wù)端已經(jīng)定義好了,那么接下來我們就需要把Person.java、Person.aidl、IPersonCount.aidl三個(gè)文件在保證包名不變的情況下拷貝過去并編譯,就能生成好同樣的IPersonCount.java文件了,這一步一定需要仔細(xì)。接下來就開始在服務(wù)端返回binder,而我們最開始接觸到binder,就是在Service中,然后在bindService的時(shí)候就會(huì)用到Binder,所以我們就需要定義Service。

這個(gè)也很簡單,就是定義一個(gè)類繼承Service,然后實(shí)例化IPersonCount.Stub這時(shí)候需要實(shí)現(xiàn)剛才定義的addPerson和getPerson方法,具體代碼這里也不貼了,很簡單,然后在Service的onBind中返回剛才實(shí)例化的IPersonCount.Stub,然后再在清單文件中去聲明改Service,即可。

3、在客戶端綁定服務(wù),拿到Binder,并使用

這時(shí)候我們回到剛才的客戶端中,在Activity中通過bindService去綁定Service,其中Intent設(shè)置的ComponentName為服務(wù)端中定義的Service的報(bào)名和完全限定名,另外ServiceConnection在onServiceConnected的時(shí)候會(huì)帶有剛才服務(wù)端返回的binder,這時(shí)候我們就可以通過IPersonCount.Stub.asInterface(iBinder)獲取到Binder,例如叫做iPersonCount,然后就可以在Activity中使用iPersonCount.addPerson(person)和iPersonCount.getPersons()方法了。

這樣整個(gè)使用過程就完成了,整體來說還是比較簡單的,主要需要注意的就是對(duì)于書寫aidl文件時(shí)類型的引用。最后會(huì)附上項(xiàng)目地址。

四、What's The Meaning

在定義AIDL接口一節(jié)中說到,最終會(huì)生成一個(gè)java文件,他其實(shí)才是我們應(yīng)用的核心,AIDL其實(shí)只是一種開發(fā)語言,方便我們生成那個(gè)java文件,幫我們完成了其中每次都差不多的重復(fù)工作,雖然系統(tǒng)能為我們完成這一個(gè)工作,我們還是應(yīng)該了解其中每部分到底是做什么的。

先來看看IPersonCount.java這個(gè)類,包含了什么東西,代碼不少,這里還是不貼了,可自己對(duì)照著看,這里只提供一個(gè)我所整理的簡單的類圖:

IPersonCount類關(guān)系圖

IPersonCount類關(guān)系圖.png

這樣一看其實(shí)就很清晰了:

  • IPersonCount是一個(gè)接口,繼承自IInterface接口,里邊有一個(gè)Stub抽象靜態(tài)內(nèi)部類以及aidl中定義的方法;
  • Stub是繼承自Binder并實(shí)現(xiàn)了IPersonCount的一個(gè)抽象靜態(tài)內(nèi)部類,里邊有一個(gè)靜態(tài)內(nèi)部類、三個(gè)屬性和四個(gè)方法,還有兩個(gè);
  • Proxy是Stub的靜態(tài)內(nèi)部類,有一個(gè)屬性和五個(gè)方法,后兩個(gè)是對(duì)IPersonCount接口的實(shí)現(xiàn)。

然后一個(gè)一個(gè)的介紹每個(gè)屬性和方法的含義:

1、IPersonCount類

這個(gè)類就是一個(gè)接口,比較簡單就不介紹了。

2、IPersonCount.Stub類

a 、DESCRIPTOR:

這是Binder的唯一標(biāo)識(shí),一般都是當(dāng)前AIDL生成的類名;

b、TRANSACTION_XXX:

以這個(gè)開頭的都是統(tǒng)一定義這個(gè)方法的code,通過code就能知道調(diào)用的是哪個(gè)方法;

c、asInterface:

是將傳入的Binder轉(zhuǎn)化為相應(yīng)的AIDL所定義的接口,看其實(shí)現(xiàn)可發(fā)現(xiàn)queryLocalInterface用于區(qū)分是統(tǒng)一進(jìn)程調(diào)用的,還是不同進(jìn)程調(diào)用的,其具體實(shí)現(xiàn)在Binder中,在構(gòu)造函數(shù)中賦值,若是同一進(jìn)程,則使用的其實(shí)就是這個(gè)Binder,若是不同進(jìn)程,則調(diào)用創(chuàng)建一個(gè)Proxy代理,用于調(diào)用服務(wù)端的onTransact方法,執(zhí)行具體的實(shí)現(xiàn);

d、asBinder:

這是返回當(dāng)前對(duì)象,因?yàn)樵搶?duì)象在服務(wù)端實(shí)例化后就是一個(gè)Binder,這里使用這個(gè)方法來定義,我猜想是為了更清晰的表達(dá)吧,同時(shí)要調(diào)整Binder我們也只需要重寫這個(gè)方法就可以了,也方便;

e、onTransact:

首先了解一下這些個(gè)參數(shù)的意思:

  • code(表示客戶端調(diào)用的是哪個(gè)方法);
  • data(表示序列化的傳入?yún)?shù));
  • reply(表示序列化回應(yīng)相關(guān)的數(shù)據(jù));
  • flags(這是一個(gè)可選的參數(shù),默認(rèn)都是0,可以傳1,表示立即就有返回,不會(huì)等待,只應(yīng)用在調(diào)用者與被調(diào)用者在不同的進(jìn)程中)。

再看其返回值,返回false表示調(diào)用不成功,而下文中的權(quán)限則可在這里去配置;

再看看其方法的具體實(shí)現(xiàn),首先通過switch區(qū)分調(diào)用的是哪個(gè)方法,然后執(zhí)行相應(yīng)的操作,而每個(gè)方法其實(shí)其流程都差不多,首先獲取參數(shù),然后調(diào)用該方法對(duì)應(yīng)的實(shí)現(xiàn),最后再通過reply中處理數(shù)據(jù)。

讀取數(shù)據(jù),會(huì)根據(jù)定義的這個(gè)方法的類型去讀取,如果是基本類型則調(diào)用相應(yīng)的read方法就能獲得,例如data.readInt、data.readString等,若是對(duì)象則通過readInt判斷不為0,再使用那個(gè)對(duì)象序列化中的CREATOR去createFromParcel來獲得對(duì)象;

執(zhí)行相應(yīng)方法,就是調(diào)用該方法,若有返回值則用相應(yīng)的對(duì)象接收;

操作reply,如果有返回值則就通過reply寫入,aidl所支持的類型都有相應(yīng)的write方法,例如:writeTypedList、writeToParcel、writeInt等等;

到這里這個(gè)類就差不多結(jié)束了,其中對(duì)于參數(shù)數(shù)據(jù)流向的標(biāo)記,下文中再介紹,核心的東西其實(shí)就在onTransact中,而與其它的關(guān)聯(lián)則是通過asInterface聯(lián)系;

3、IPersonCount.Stub.Proxy類

其實(shí)這個(gè)在上文中有個(gè)簡單的說明,它的作用就是在asInterface時(shí)實(shí)例化一個(gè)對(duì)象返回,當(dāng)在客戶端調(diào)用相應(yīng)方法時(shí),先獲取傳入數(shù)據(jù)data,執(zhí)行transact方法,再處理一下reply用以響應(yīng),有返回值的則返回。接下來也依次描述一下各個(gè)成員以及方法的含義。

a、mRemote

這個(gè)就是asInterface中傳入的binder,這里存儲(chǔ)起來方便使用;

b、asBinder

返回binder;

c、getInterfaceDescriptor

返回描述與Stub一致;

d、實(shí)現(xiàn)接口中定義的方法

這里以addPerson為例,因?yàn)閿?shù)據(jù)流向標(biāo)記中inout會(huì)包含in和out兩種標(biāo)記,所以這里看到的是以inout數(shù)據(jù)流向的代碼,下文會(huì)逐一分析哪部分是干什么的。

public boolean addPerson(net.arvin.androidart.aidl.Person person) throws android.os.RemoteException {
    //前兩行是初始化data和reply,data是用來傳遞參數(shù)數(shù)據(jù)的,reply是用于返回?cái)?shù)據(jù)以及寫入out流向的數(shù)據(jù)的
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    //這里定義result,會(huì)根據(jù)相應(yīng)方法定義對(duì)應(yīng)類型,如果沒有返回則不定義該變量
    boolean _result;
    try {
        //這一步都有,將DESCRIPTOR作為令牌記錄下檔前讀取的是哪個(gè)Binder,是用來在onTransact時(shí)驗(yàn)證
        _data.writeInterfaceToken(DESCRIPTOR);
        //這一部分是in數(shù)據(jù)流向的操作,不同的對(duì)象寫入data的方式不同,如果只是out流向則不包含這一部分
        if ((person != null)) {
            _data.writeInt(1);
            person.writeToParcel(_data, 0);
        } else {
            _data.writeInt(0);
        }
        //這一部分就是調(diào)用遠(yuǎn)程binder中的onTransact方法,其方法中會(huì)直接調(diào)用onTransact方法,傳入的參數(shù)和上文中onTransact方法的參數(shù)含義一致
        mRemote.transact(Stub.TRANSACTION_addPerson, _data, _reply, 0);
        _reply.readException();
        //這一部分就是從reply中獲得result,不同類型獲取方式不同,用于最后返回;
        _result = (0 != _reply.readInt());
        //這一部分就是out數(shù)據(jù)流向的操作,當(dāng)然也是不同類型,操作方式不同,如果只是in標(biāo)記則沒有這一部分,需要注意的是寫出時(shí)的順序要和寫入時(shí)數(shù)據(jù)的順序相同
        if ((0 != _reply.readInt())) {
            person.readFromParcel(_reply);
        }
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    return _result;
}

通過上邊方法的注釋,我覺得對(duì)于這個(gè)方法就差不多了,其實(shí)就是讀取數(shù)據(jù),調(diào)用遠(yuǎn)程方法,處理返回?cái)?shù)據(jù)。

這里再細(xì)看細(xì)看一下在onTransact方法中不同數(shù)據(jù)流向代碼會(huì)是怎樣,這里還是以addPerson為例,數(shù)據(jù)流向也為inout。

case TRANSACTION_addPerson: {
    //這一部分很明顯就對(duì)應(yīng)著proxy中data的writeInterfaceToken
    data.enforceInterface(DESCRIPTOR);
    //這一部分就是in數(shù)據(jù)流向的操作,不同類型之行方法不同,如果只有out則只會(huì)初始化一個(gè)對(duì)象實(shí)例,若傳入的是空,對(duì)應(yīng)著proxy中data讀取就是0,這里也就賦值為空了
    net.arvin.androidart.aidl.Person _arg0;
    if ((0 != data.readInt())) {
        _arg0 = net.arvin.androidart.aidl.Person.CREATOR.createFromParcel(data);
    } else {
        _arg0 = null;
    }
    //這一步是數(shù)據(jù)邏輯執(zhí)行的核心,會(huì)去調(diào)用我們在服務(wù)端實(shí)現(xiàn)的方法
    boolean _result = this.addPerson(_arg0);
    reply.writeNoException();
    //這一步是將result寫入,如果沒有返回值則沒有這一步
    reply.writeInt(((_result) ? (1) : (0)));
    //這一步是out數(shù)據(jù)流向的操作,是將這里操作完的傳入的參數(shù)寫入到reply中,以便在proxy中寫出
    if ((_arg0 != null)) {
        reply.writeInt(1);
        _arg0.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
    } else {
        reply.writeInt(0);
    }
    //這里返回true表示調(diào)用成功
    return true;
}

這樣聯(lián)通起來一看,就想通,目前來看就對(duì)其內(nèi)部數(shù)據(jù)存儲(chǔ)以及讀取時(shí)的規(guī)則不是很清楚,但是對(duì)于使用確實(shí)知道都會(huì)一一對(duì)應(yīng)有寫入就有寫出。

這里邊還有一個(gè)技術(shù)點(diǎn)就是調(diào)用時(shí),線程的切換問題,當(dāng)客戶端調(diào)用服務(wù)端的代碼時(shí)由于不知道遠(yuǎn)程操作是否耗時(shí),所以盡量都在子線程中調(diào)用,而服務(wù)端的執(zhí)行代碼,即使是耗時(shí)操作也可以不用開新的線程,因?yàn)槠浔旧硪呀?jīng)在Binder的線程池中調(diào)用了;而若是服務(wù)端要調(diào)用客戶端,則同樣的道理,只是反過來了,具體的例子會(huì)在最后實(shí)踐中應(yīng)用。

五、Permission

如果任何一個(gè)客戶端臉上了我們的服務(wù)端,而執(zhí)行了這些方法,顯然時(shí)不安全的,而我們也是可以通過一些方法去限制訪問,總結(jié)起來就兩點(diǎn)

  • 通過Service連接時(shí)限制
  • 通過調(diào)用onTransact時(shí)限制

而我們限制的點(diǎn),也是多種多樣,由于沒在項(xiàng)目中使用過,別人的意見是設(shè)置權(quán)限以及限制包名來限制訪問,接下來就依次說說具體的實(shí)現(xiàn)。

a、通過Service連接時(shí)限制

我們就以權(quán)限為例,這里現(xiàn)在清單文件中自定義一個(gè)permission:

<permission
    android:name="net.arvin.androidart.permission.BinderPoolService"
    android:protectionLevel="normal" />

然后我們在Service中,在onBind方法返回參數(shù)前作判斷,例如:

int check = checkCallingOrSelfPermission("net.arvin.androidart.permission.BinderPoolService");
if (check == PackageManager.PERMISSION_DENIED) {
    return null;
}

這樣如果在客戶端調(diào)用時(shí)沒有申明這個(gè)權(quán)限就只會(huì)返回空,這樣就完成了簡單的限制。除此之外對(duì)于Service的連接限制還有其它辦法,我還沒實(shí)際運(yùn)用過就不再介紹了,可以自行查詢。

b、通過調(diào)用onTransact時(shí)限制

從上文的介紹中可以知道,該返回如果返回false則就不會(huì)調(diào)用成功,所以我們就可以在不符合的條件返回false,方法也很簡單,我們只需要重寫onTransact方法中去判斷即可,例如:

@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
    int check = checkCallingOrSelfPermission("net.arvin.androidart.permission.BinderPoolService");
    if (check == PackageManager.PERMISSION_DENIED) {
        return false;
    }
    
    String packageName;
    String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
    if (packages != null && packages.length > 0) {
        packageName = packages[0];
        if (!packageName.startsWith("net.arvin")) {
            return false;
        }
    }
    return super.onTransact(code, data, reply, flags);
}

邏輯也很簡單,就不介紹了,就是限制權(quán)限和包名,都通過就執(zhí)行我們剛才的代碼就可以了。

六、Best Practice

注:標(biāo)題黨了一下,畢竟我還沒在實(shí)踐中用過,都是看著別人這么介紹的,自己總結(jié)后也確實(shí)覺得這樣的實(shí)踐方式還不錯(cuò),所以才起了這個(gè)標(biāo)題

上面已經(jīng)簡單介紹過AIDL的使用以及客戶端和服務(wù)端的調(diào)用,當(dāng)時(shí)客戶端在調(diào)用服務(wù)端的時(shí)候都是直接調(diào)用,沒有開子線程,如果服務(wù)端執(zhí)行的操作是耗時(shí)較長的,則就會(huì)出現(xiàn)ANR了。而這一點(diǎn)也只需開線程就能解決就不再詳細(xì)介紹了,下面著重介紹的是:

  • 服務(wù)端回調(diào)客戶端
  • 連接池的使用

a、服務(wù)端回調(diào)客戶端

我們發(fā)現(xiàn)其實(shí)之前的調(diào)用都是單向的,往往有時(shí)我們希望服務(wù)端有的數(shù)據(jù)變化了能通知客戶端,而不用一直去調(diào)用才知道,這時(shí)候這種方法就派上用場了。

其實(shí)原理和在同一個(gè)進(jìn)程中時(shí)是一樣的,就是客戶端向服務(wù)端注冊一個(gè)回調(diào),而當(dāng)有更新的時(shí)候服務(wù)端再調(diào)用回調(diào)就可以了。

當(dāng)然這個(gè)回調(diào)也得使用AIDL來定義,保證定義的接口客戶端與服務(wù)端同步,如下:

// IOnNewPersonIn.aidl
package net.arvin.androidart.aidl;

import net.arvin.androidart.aidl.Person;

interface IOnNewPersonIn {
    void onNewPersonIn(in Person newPerson);
}
// IPersonCount.aidl
package net.arvin.androidart.aidl;

import net.arvin.androidart.aidl.Person;
import net.arvin.androidart.aidl.IOnNewPersonIn;

interface IPersonCount {
    boolean addPerson(in Person person);

    List<Person> getPersons();
    
    void registerListener(in IOnNewPersonIn listener);
    
    void unregisterListener(in IOnNewPersonIn listener);
}

然后我們再服務(wù)端去實(shí)現(xiàn)這個(gè)注冊與反注冊,其中存儲(chǔ)用普通的List來存的話直接反注冊時(shí)無效的,要用RemoteCallbackList<E extends IInterface>來存,這樣才能在反注冊時(shí)取消掉,因?yàn)槠鋬?nèi)部是以binder為key,listener為value存儲(chǔ)的,具體的實(shí)現(xiàn)還沒去深究。還有一點(diǎn)需要注意的就是對(duì)于這個(gè)RemoteCallbackList的讀取,一定需要以beginBroadcast開始,finishBroadcast結(jié)束,中間遍歷使用getBroadcastItem獲取,這里就這一部分代如下:

@Override
public void registerListener(IOnNewPersonIn listener) throws RemoteException {
    mListeners.register(listener);
}

@Override
public void unregisterListener(IOnNewPersonIn listener) throws RemoteException {
    mListeners.unregister(listener);
}

private void onNewPersonIn(Person newPerson) {
    int size = mListeners.beginBroadcast();
    for (int i = 0; i < size; i++) {
        IOnNewPersonIn onNewPersonIn = mListeners.getBroadcastItem(i);
        try {
            if (onNewPersonIn != null) {
                onNewPersonIn.onNewPersonIn(newPerson);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    mListeners.finishBroadcast();
}

這里onNewPersonIn的調(diào)用需要在子線程,具體代碼中開了個(gè)線程去處理,這里就不展示了。

客戶端需要做的就是通過binder注冊這個(gè)回調(diào),然后再轉(zhuǎn)換到主線程中去操作,如下:

private IOnNewPersonIn.Stub onNewPersonIn = new IOnNewPersonIn.Stub() {
    @Override
    public void onNewPersonIn(Person newPerson) throws RemoteException {
        Message message = mHandler.obtainMessage();
        message.what = 0;
        message.obj = newPerson;
        mHandler.sendMessage(message);
    }
};

private IPersonCount iPersonCount;

try {
    if (iPersonCount != null) {
        iPersonCount.registerListener(onNewPersonIn);
    }
} catch (RemoteException e) {
    e.printStackTrace();
}

@Override
public void handleMessage(Message msg) {
    if (msg.what == 0) {
        Person person = (Person) msg.obj;
        edAge.setText(person.getAge()+"");
        edName.setText(person.getName());
    }
}

這也是部分代碼,具體的操作也很簡單,連接得到IpersonCount后,再注冊回調(diào),回調(diào)中通過Handler處理消息轉(zhuǎn)化到主線程再顯示出來。

這樣這一部分的大體邏輯就已經(jīng)出來了,最主要就是Listener的存儲(chǔ)那里以及回調(diào)的處理要轉(zhuǎn)換到主線程。

b、連接池的使用

如果你按照這種例子多寫幾個(gè)會(huì)發(fā)現(xiàn),怎么一個(gè)連接的就得寫一個(gè)Service,這豈不是不好很繁瑣,而且Service開多了對(duì)性能也有影響,所以我們引入了中間人,幫助我們通過code去連接我們需要的AIDL對(duì)應(yīng)的Binder,而Binder我們通過AIDL就已經(jīng)實(shí)現(xiàn)了大多的邏輯,只需要再去實(shí)現(xiàn)定義的具體方法即可。

首先定義AIDL接口

// IBinderPool.aidl
package net.arvin.androidart.aidl;

// Declare any non-default types here with import statements
interface IBinderPool {
    /**
    * @param binderCode, the unique token of specific Binder<br/>
    * @return specific Binder who's token is binderCode.
    */
    IBinder queryBinder(int binderCode);
}

實(shí)現(xiàn)生成的Binder

public class BinderPoolImpl extends IBinderPool.Stub {
    public static final int BINDER_COMPUTE = 0;
    public static final int BINDER_PERSON_COUNT = 1;
    
    @Override
    public IBinder queryBinder(int binderCode) throws RemoteException {
        IBinder binder = null;
        switch (binderCode) {
            case BINDER_PERSON_COUNT: {
                binder = new PersonCountImpl();
                break;
            }
            case BINDER_COMPUTE: {
                binder = new IntegerAddImpl();
                break;
            }
            default:
            break;
        }
        return binder;
    }
}

邏輯很簡單,就是根據(jù)這里提供的binderCode去查詢返回相應(yīng)的的binder實(shí)例。同理,我們可以把字簽在PersonCountService中的binder提取出來。

實(shí)現(xiàn)BinderPoolService

public class BinderPoolService extends Service {
    @Override
    public IBinder onBind(Intent intent) {
        return new BinderPoolImpl();
    }
}

對(duì)于配置那些就不貼出來了。

這樣服務(wù)端的連接池相關(guān)代碼就已經(jīng)寫完了,接下來再看看客戶端的。

由于連接池是為了通用,為了避免重復(fù)創(chuàng)建的開銷,所以這里會(huì)把連接池作為單例模式使用。代碼如下:

public class BinderPool {
    private static final String TAG = "BinderPool";
    public static final int BINDER_COMPUTE = 0;
    public static final int BINDER_PERSON_COUNT = 1;

    private Context mContext;
    private IBinderPool mBinderPool;
    private static BinderPool sInstance;

    private BinderPool(Context context) {
        mContext = context.getApplicationContext();
        connectBinderPoolService();
    }

    public static BinderPool getInstance(Context context) {
        if (sInstance == null) {
            synchronized (BinderPool.class) {
                if (sInstance == null) {
                    sInstance = new BinderPool(context);
                }
            }
        }
        return sInstance;
    }

    private synchronized void connectBinderPoolService() {
        Intent service = new Intent();
        service.setComponent(new ComponentName("net.arvin.androidart",
                "net.arvin.androidart.multiProcess.BinderPoolService"));
        mContext.bindService(service, mBinderPoolConnection,
                Context.BIND_AUTO_CREATE);
    }

    public IBinder queryBinder(int binderCode) {
        IBinder binder = null;
        try {
            if (mBinderPool != null) {
                binder = mBinderPool.queryBinder(binderCode);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return binder;
    }

    private ServiceConnection mBinderPoolConnection = new ServiceConnection() {

        @Override
        public void onServiceDisconnected(ComponentName name) {
        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mBinderPool = IBinderPool.Stub.asInterface(service);
            try {
                mBinderPool.asBinder().linkToDeath(mBinderPoolDeathRecipient, 0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    };

    private IBinder.DeathRecipient mBinderPoolDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            Log.w(TAG, "binder died.");
            mBinderPool.asBinder().unlinkToDeath(mBinderPoolDeathRecipient, 0);
            mBinderPool = null;
            connectBinderPoolService();
        }
    };
}

代碼也不多,很好理解,里邊主要就是三個(gè)事:

  • 實(shí)現(xiàn)單例
  • 連接服務(wù)端獲取Binder
  • 斷開后重連

前兩個(gè)技術(shù)點(diǎn)沒什么好說的都差不多,第三個(gè)重連機(jī)制,使用的是Binder的DeathRecipient接口回調(diào),使用方法也很簡單,就是在獲取到binder后linkToDeath,其中傳入DeathRecipient接口的實(shí)現(xiàn),若是被斷開連接后回調(diào),先調(diào)用unlinkToDeath,再將binder設(shè)置為空,再重新連接,就實(shí)現(xiàn)了整個(gè)過程。

然后是客戶端對(duì)BinderPool的使用

mBinderPool = BinderPool.getInstance(this);
iPersonCount = IPersonCount.Stub.asInterface(mBinderPool.queryBinder(BinderPool.BINDER_PERSON_COUNT));

第一句獲取Binder池實(shí)例,第二句就是通過binder池獲取對(duì)應(yīng)的binder,再獲取到相應(yīng)的接口,這樣就能正常使用了,這里就不貼具體調(diào)用代碼了。

到這里,整篇就結(jié)束了,若有差錯(cuò),請多多指教。

Demo源碼

AIDL的Demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評(píng)論 3 416
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評(píng)論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,714評(píng)論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評(píng)論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,410評(píng)論 0 288
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評(píng)論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,776評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,976評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評(píng)論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,210評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評(píng)論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,654評(píng)論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,958評(píng)論 2 373

推薦閱讀更多精彩內(nèi)容