安卓開發之IPC機制 機製詳解(附AIDL範例)

2020-08-12 22:13:24

IPC(Inter-Process Communication),意爲進程間通訊或者跨進程通訊,是指兩個進程之間進行數據交換的過程。前面在學習Handler機制 機製時提到過執行緒與進程的概念,在安卓中一個進程可以簡單理解爲一個程式或應用,而執行緒是CPU排程的最小單元,一個進程可以包含多個執行緒,也可以只有一個執行緒即主(UI)執行緒。

1. 多進程模式

既然是跨進程通訊,必然要在多進程模式下進行。

使用多進程的情況分爲兩種:

  • 由於某些原因,應用自身需要採用多進程模式來實現。可能原因有:應用中某些模組因特殊原因要執行在單獨進程中;爲加大一個應用可使用的記憶體,需要通過多進程來獲取多份記憶體空間
  • 當前應用需要向其它應用獲取數據,例如通過系統提供的ContentProvider去查詢數據時,其實也是一種進程間通訊

安卓中開啓多進程模式的方式

  • 通過JNI在native層fork一個新的進程,我們常見的應用反附加偵錯就用的是這種方法,應用啓動時自己先fork一個進程附加自己
  • 給四大元件指定android:process屬性,進程名的命名規則:
    • 1)預設:沒有指定該屬性則執行在預設進程,其進程名就是包名
    • 2)完整命名的進程:例如android:process="com.example.myapplication.remote,表示該進程屬於全域性進程,其他有着相同ShareUID和簽名的應用可以通過ShareUID方式和它跑在同一個進程中,在這種情況下,無論它們是否跑在同一進程,它們均可共用data目錄,元件資訊,如果跑在同一進程中,還可共用記憶體數據,看起來就像是一個應用的兩個部分
    • 3)以":「開頭的進程:例如android:process=」:remote",進程名包名+":remote"=com.example.myapplication:remote,表示該進程屬於當前應用的私有進程,其他進程的元件不能和它跑在同一進程中

多進程模式的執行機制 機製

所有執行在不同進程的四大元件,只要它們之間需要通過記憶體來共用數據,都會共用失敗。這是因爲Android爲每個應用/進程分配了一個獨立的虛擬機器,不同的虛擬機器在記憶體分配上有不同的地址空間,這會導致在不同的虛擬機器中存取同一個類的物件會產生多份副本。舉例來說,假如兩個進程中都存在同一個類,並且這兩個類是互不幹 不乾擾的,在其中一個進程中修改類變數的值只會影響當前進程,對其他進程不會造成任何影響。

使用多進程會造成的問題:

  • 靜態變數和單例模式失效:Android爲每個應用/進程分配了一個獨立的虛擬機器,不同的虛擬機器在記憶體分配上有不同的地址空間
  • 執行緒同步機制 機製失效:原因同上
  • SharedPreference的可靠性下降:SharedPreferences不支援兩個進程同時進行寫操作,否則會導致數據丟失,這是因爲SP的底層是通過讀/寫XML檔案來實現的,併發寫甚至併發讀都有可能出現問題
  • Application會多次建立:Android會爲新的進程分配獨立的虛擬機器,相當於系統又把這個應用重新啓動了一次,那麼自然就會建立新的Application

2. Binder機制 機製

2.1 Android新增Binder機制 機製的原因

我們知道任何一個操作系統都有對應的IPC機制 機製,例如:

  • Windows:通過剪下板、管道等進行進程間通訊
  • Linux:通過名稱空間、共用內容、號志等進行進程間通訊
  • Android:沒有完全繼承Linux,取而代之的是Binder機制 機製,並通過Binder機制 機製實現了例如 Bundle、檔案共用、AIDL、Messager、Socket、ContentProvider等IPC方式,這些方式本質上都是通過Binder機制 機製來實現的只不過是封裝方式不同。Android另開爐竈新增Binder機制 機製肯定是有原因的,主要有以下幾點:
  1. 傳輸效率高、可操作性強:傳輸效率主要影響因素是記憶體拷貝的次數,拷貝次數越少,傳輸速率越高。幾種數據傳輸方式比較:
    在这里插入图片描述
    對於訊息佇列、Socket和管道來說,數據先要從發送方的快取區拷貝到內核開闢的快取區中,再從內核快取區拷貝到接收方的快取區,一共兩次拷貝,圖片來自Android - Binder驅動
    在这里插入图片描述
    而對於Binder來說,數據從發送方的快取區拷貝到內核的快取區,而接收方的快取區與內核的快取區是對映到同一塊實體地址的,只需要一次拷貝:
    在这里插入图片描述
    共用記憶體雖然在傳輸時沒有拷貝數據,但其控制機制 機製複雜,綜合來看Binder的傳輸效率是最好的。

  2. 實現C/S架構方便:Linux的IPC方式中只有Socket支援C/S的通訊模式,而Socket主要用於網路間的通訊且傳輸效率低、開銷大。Binder基於C/S 架構設計 ,Server端與Client端相對獨立,穩定性較好。
    如下圖所示,Binder框架定義了四個角色:Server,Client,ServiceManager和Binder驅動,其中Server、Client、ServiceManager執行於使用者空間,Binder驅動執行於內核空間,Binder框架如下圖所示:
    在这里插入图片描述
    這裏先解釋下上面說的使用者空間和內核空間。我們知道在Android中每一個進程/應用都是獨立的,只能執行在自己進程所擁有的虛擬地址空間,且都由兩部分組成,一部分是使用者空間,另一部分是內核空間,對於使用者空間來說,不同進程/應用之間彼此是不能共用的,而內核空間卻是可共用的。Client進程與Server進程通訊利用進程間可共用的內核記憶體空間來完成底層通訊工作,Client端與Server端進程跟內核空間的驅動進行互動往往採用ioctl(專門用於裝置輸入輸出操作的系統呼叫)等方法。這樣做一方面可以限制其他進程存取自己的虛擬地址空間,安全性高,另一方面內核共用也有助於系統維護和併發操作,節省空間:
    在这里插入图片描述

  3. 安全性高:傳統Linux IPC的接收方無法獲得對方進程可靠的UID/PID,從而無法鑑別對方身份,而Binder機制 機製爲每個進程分配了UID/PID且在Binder通訊時會根據UID/PID進行有效性檢測

2.2 Binder機制 機製流程簡介

剛上來就分析具體的流程確實比較困難,這裏我們通過一個例子瞭解Binder執行機制 機製。

首先介紹下Binder框架定義的幾個角色:

  • Server&Client:伺服器&用戶端。在Binder驅動和ServiceManager提供的基礎設施上,進行Client和Server之間的通訊,這裏Server、Client角色不是固定的,例如它們和ServiceManager通訊時(通訊基於Binder機制 機製)角色就不相同,我們下面 下麪會說到
  • Binder驅動:與硬體裝置沒有關係,只不過其工作方式與裝置驅動程式是一樣的,它負責進程之間Binder通訊的建立,Binder在進程之間的傳遞,Binder參照計數管理,數據包在進程之間的傳遞和互動等一系列底層支援
  • ServiceManager:服務管理者,將Binder名字轉換爲Client中對該Binder的參照,使得Client可以通過Binder名字獲得Server中Binder實體的參照。流程如下圖所示:
    在这里插入图片描述
    ServiceManager、 Server、Client三者之間的相互通訊就是基於Binder機制 機製的:
    • 1.註冊服務:Server進程要先註冊Service到ServiceManager。該過程中Server是用戶端,ServiceManager是伺服器端
    • 2.獲取服務:Client進程使用某個Service前,要先從ServiceManager中獲取相應的Service,通常是Service參照的代理物件(假如Server、Client不處於同一進程的話)。該過程中Client是用戶端,ServiceManager 是伺服器端
    • 3.使用服務:Client根據得到的Service資訊建立與Service所在的Server進程通訊的線路,然後就可以直接與Service互動。該過程中Client是用戶端,Server是伺服器端

圖中也看到Client、Server、ServiceManager之間的互動都是虛線表示,是由於它們彼此都不是直接互動的,而是都通過與Binder驅動進行互動從而實現IPC通訊方式。這裏可能會有人問爲什麼Client不直接和Server通訊而要加個ServiceManager呢?其實這樣做一方面便於管理service和Client的請求;另一方面在應用程式開發時,只需爲Client建立到Server的連線,就可花很少時間和精力去實現Server相應功能。舉個例子,你(client)畢業了想向你們班所有同學(Server)寫信,考慮兩種情況:一是你們有個線上通訊錄文件(ServiceManager),同學們畢業前都主動把自己聯繫地址「註冊」了上去,這樣你寄信的時候查一下儲存的通訊錄(ServiceManager)對應名字的地址就好;二是沒有這個通訊錄(ServiceManager),你寫信要一個個打電話詢問然後寄出去,顯而易見第一種方式簡單一些。當然這個例子不太恰當,Android既然這樣做那麼肯定是有好處的。

這裏參考詳細說說Binder通訊原理與機制 機製給的Binder跨進程通訊的例子以理清上面的流程:

假如Client進程呼叫Server進程的computer物件的add方法:

  • 1.註冊服務:Server進程向ServiceManager註冊,告訴ServiceManager我是Server,我有computer物件,我能做add這個事
  • 2.獲取服務:Client進程向ServiceManager查詢,我要呼叫Server進程的computer物件的add方法,這個過程會經過Binder驅動,當向ServiceManager查詢完畢後,Binder驅動就發揮作用了,它不會直接將computer物件返回給Client進程,而是將computer物件轉換成了computerProxy代理物件,並將這個代理物件返回給了Client進程,因此Client進程拿到的實際上是一個代理物件computerProxy,並且這個代理物件也有add方法,但是這個add方法只是對參數進行一些包裝而已
    -3.使用服務:當Client進程要呼叫add方法,就把這個訊息發送給Binder驅動:「我要呼叫Server進程的computer物件的add方法(其實呼叫的是代理物件的add方法,與此同時代理物件的方法會將Client傳遞的參數打包成Parcel序列化物件)」,代理物件將Parcel發送給內核中的Binder驅動,Binder驅動發現呼叫的是computerProxy代理物件的add方法,它知道Client進程實際上是要呼叫computer物件的add方法,這時Binder驅動就去通知Server進程,呼叫你的computer物件的add方法並將結果給我,然後Server進程就讀取Binder驅動中的請求數據,如果是發送給自己的,就解析Parcel物件,呼叫了computer物件的add方法並將計算結果發送給了Binder驅動,Binder驅動再返回給Client進程。從始至終Client進程都以爲自己呼叫的是真實的computer物件的add方法,其實它只是呼叫了代理物件的add方法而已,不過好在Client最終還是拿到了計算結果。這裏和前面問題一樣,爲什麼不直接去呼叫而是要加個代理?這是因爲通過引入代理物件的方式來間接存取目標物件可以減少直接存取目標物件給系統帶來的不必要複雜性,並且通過代理物件可以對原有的業務增強。再舉個不恰當的例子,你想購買國外某公司旗下的某款產品,假如沒有淘寶啊之類的第三方代理,你自己打電話過去說我想買你們公司這款產品,那中間快遞運輸、關稅、售後等等之類的都得你自己去解決,那還不如讓代理去做這些「底層」的東西,你只需要提出需求給代理然後收貨就好

再簡單一點看的話,之前在學習bindservice啓動Service的時候,自定義的Service(就看做是伺服器端吧)在onBind()方法中就要返回一個包含了「伺服器端」業務呼叫的Binder物件,在「用戶端」角度來說,這個返回的就是一個Binder參照,通過這個Binder參照,「用戶端」就可以獲取「伺服器端」提供的服務或者數據。這裏也可以看到,「伺服器端」想要實現被「跨進程」存取,就必須繼承Binder類。
在这里插入图片描述
MainActivity中:
在这里插入图片描述在这里插入图片描述
上面還提到了代理就簡單瞭解一下即可,詳細可參考代理模式以及在Android中的使用 ,代理模式Proxy就是給某個物件提供一個代理物件,並由代理物件控制對原物件的存取。UML圖如下所示:
在这里插入图片描述

  • 公共介面角色:定義了Real Subject和Proxy的共同介面,這樣在任何可以使用Real Subject的地方都可以使用Proxy,例如公司和代理都有sale銷售方法
  • Real Subject(國外公司) :定義了Proxy所代表的Real Subject,並定義實現具體業務邏輯的實現
  • Proxy(第三方代理) :持有Real Subject的參照,控制和限制Real Subject的實現,可在任何時候操作目標物件;提供一個與Real Subject相同的介面,可在任何時候替代目標物件;並且還可以擁有自己的處理方法(預處理和善後)

2.3 Binder 工作原理

在这里插入图片描述

  • Server端:在伺服器端建立好一個Binder物件後,內部就會開啓一個執行緒用於接收Binder驅動發送的訊息,收到訊息後呼叫onTranscat方法,並按照參數執行不同的伺服器端程式碼
  • Binder驅動:在伺服器端成功Binder物件後,Binder驅動也會建立一個mRemote物件(也是Binder類),用戶端可藉助它呼叫transcat()即可向伺服器端發送訊息
  • Client端:用戶端要想存取Binder的遠端服務,就必須獲取遠端服務的Binder物件在Binder驅動層對應的mRemote參照。當獲取到mRemote物件的參照後,就可以呼叫相應Binder物件的暴露給用戶端的方法

具體可參考:

3.Android中的IPC方式

前面提到過,Bundle、AIDL、Socket等一些IPC方式實際都是通過Binder來實現,只不過封裝方式不同,這裏簡單瞭解下Android中的幾種IPC也就是跨進程通訊的方式,詳細的解釋可以參考《Android開發藝術探索》:

3.1 Bundle

Bundle在之前學習Activity通訊的時候學過,它可以在Activity、Service和Receiver之間通過Intent.putExtra()傳遞Bundle數據。舉個例子:

    //MainActivity中
    Intent intent = new Intent(MainActivity.this,SecondActivity.class);
    Bundle data = new Bundle();
    data.putString("name","lisi");//將數據放入bundle
    data.putInt("age",27);
    data.putBoolean("isfemale",true);
    intent.putExtras(data);

    //在SecondActivity中,將傳遞的數據取出
    Bundle data = getIntent().getExtras();//從bundle中取出數據
    String name = data.getString(MainActivity.NAME_KEY);
    int age = data.getInt(MainActivity.AGE_KEY);
    boolean isfemale = data.getBoolean(MainActivity.IS_FEMALE_KEY);
    Log.d(LOG_TAG,"name = "+name);
    Log.d(LOG_TAG,"age = "+age);
    Log.d(LOG_TAG,"isfemale = "+isfemale);

Bundle也可以傳遞序列化物件:

    //存入數據
    Person person = new Person();//Person是一個JavaBean,實現了Serializable介面
    person.setName("lisi");//設定相應的屬性值
    person.setAge(27);
    person.setIsfemale(true);
    mIntent.putExtra(PERSON_KEY,person);

    //取出數據
    Bundle data = intent.getExtras();
    Person person = (Person) data.getSerializable(MainActivity.PERSON_KEY);
    Log.d(LOG_TAG,"name = "+person.getName());
    Log.d(LOG_TAG,"age = "+person.getAge());
    Log.d(LOG_TAG,"isfemale = "+person.isfemale());

其原理是實現了Parcelable介面,它可方便的在不同的進程中傳輸,另外要注意Bundle不支援的數據型別無法在進程中被傳遞,例如進程A進行計算後的結果想要傳遞給進程B,但是結果卻不是Bundle所支援的數據型別,這種情況下可以將在A進程進行的計算過程轉移到B進程中的一個Service裡去做,避免了進程間的通訊問題。

3.2 檔案共用

兩個進程通過讀/寫同一個檔案來交換數據,例如A進程把數據寫入檔案,B進程通過讀取這個檔案來獲取數據,這種方式適合對數據同步要求不高的進程之間的通訊,並且要妥善處理併發讀/寫的問題。

3.3 Messager

Messenger 是一種輕量級的 IPC 方案,通過它可在不同進程中傳遞Message物件。

Messenger.send(Message);

它的底層實現是 AIDL ,它對 AIDL 進行了封裝,更便於進行進程間通訊。Messenger一次只處理一個請求,所以在伺服器端不用考慮執行緒同步的問題。工作原理圖如下圖所示:
在这里插入图片描述
Message和Messenger實現了Parcelable介面,所以可以在進程間進行傳遞。Message是我們所要傳遞資訊的載體,Messenger提供了傳遞的渠道,Handler是最終的資訊接受和處理中心,Handler本身是無法進行接受訊息的,只不過在建立Messenger時,Messenger持有 Handler的物件 ,在 Messenger內部呼叫了Handler的handleMessage方法,讓其去處理Message 。

實現方法如下,具體程式碼可參考IPC 之 Messenger 的使用

伺服器端

  • 建立一個Service用於提供服務
  • Service中建立一個Handler用於接收器用戶端進程發來的數據
  • 利用Handler建立一個Messenger物件
  • 在Service的onBind()中返回Messenger對應的Binder物件

用戶端

  • 通過bindService系結伺服器端的Service
  • 通過系結後返回的IBinder物件建立一個Messenger,進而可向伺服器端進程發送Message數據。(完成單向通訊)
  • 在用戶端建立一個Handler並由此建立一個Messenger,並通過Message物件的replyTo欄位傳遞給伺服器端進程。伺服器端通過讀取Message得到Messenger物件,進而向用戶端進程傳遞數據。(完成雙向通訊)

3.4 ContentProvider

ContentProvider是Android提供的用來進行不同應用間數據共用的方式,例如我們之前獲取通訊錄/簡訊資訊就是通過ContentProvider實現的,這裏因爲通訊錄/簡訊也是一個應用,那我們的應用要想獲取這些資訊勢必要跨進程/應用通訊,底層就是通過Binder實現的。具體用法參考之前的文章安卓開發之內容提供器

使用時注意一下幾點:

  • 除了onCreate()執行在UI執行緒中,其他的query()、update()、insert()、delete()和getType()都執行在Binder執行緒池中
  • CRUD四大操作存在多執行緒併發存取,要注意在方法內部要做好執行緒同步
  • 一個SQLiteDatabase內部對數據庫的操作有同步處理,但多個SQLiteDatabase之間無法同步

3.5 Socket

不僅可以跨進程、還可以跨裝置通訊。
原理參考之前的文章安卓開發網路程式設計學習之Socket
Demo參考Android:這是一份很詳細的Socket使用攻略

3.6 AIDL

AIDL(Android介面定義語言):如果在一個進程中要呼叫另一個進程中物件的方法,可使用AIDL生成可序列化的參數,AIDL會生成一個伺服器端物件的代理類,通過它,用戶端就會間接呼叫伺服器端物件的方法,對我們初學者來說,AIDL的作用就是可以在自己的APP裡系結一個其他APP的service,這樣自己的APP就可以和其他APP互動。當然我們也可以自己寫Binder程式碼去實現進程間通訊,但是你會發現寫的程式碼和AIDL的程式碼差不多,而且AIDL還幫我們自動生成了一些Binder相關程式碼不用我們自己再去寫,大大簡化了開發過程。大致流程可以參考前面Binder原理圖。

3.6.1 AIDL語法

3.6.1.1 數據型別

AIDL支援的數據型別包括以下幾種:

  • 基本數據型別:byte,int,long,float,double,boolean,char
  • String型別
  • CharSequence型別
  • ArrayList、HashMap且裏面的每個元素都能被AIDL支援
  • 實現Parcelable介面的物件
  • 所有AIDL介面本身

要注意的是,AIDL除了基本數據型別,其它型別(例如我們自定義的實體型別)的參數必須標上方向:in、out或inout,用於表示在跨進程通訊中數據的流向。

  • in:表示數據只能由用戶端流向伺服器端。伺服器端將會接收到這個物件的完整數據,但在伺服器端修改它不會對用戶端輸入的物件產生影響。
  • out:表示數據只能由伺服器端流向用戶端。伺服器端將會接收到這個物件的的空物件,但在伺服器端對接收到的空物件有任何修改之後用戶端將會同步變動。
  • inout:表示數據可在伺服器端與用戶端之間雙向流通。伺服器端將會接收到用戶端傳來物件的完整資訊,且用戶端將會同步伺服器端對該物件的任何變動。
3.6.1.2 AIDL檔案型別
  • 所有AIDL檔案都是以.aidl作爲後綴的
  • 根據用途區分,AIDL檔案有兩種,一種是用於定義介面,以供系統使用來完成跨進程通訊;另一種是用於宣告parceable物件,以供其他AIDL檔案使用,並且這種AIDL檔案中宣告自定義的Parcelable物件時必須把java檔案和自定義的AIDL檔案顯式的import進來,無論是否在同一包內

3.6.1 AIDL使用

伺服器端
首先看下目錄結構:
在这里插入图片描述
在AndroidStudio中工程目錄的Android檢視下,右鍵new一個AIDL檔案,預設將建立一個與java資料夾同級的aidl資料夾用於存放AIDL檔案,這裏命名爲Book.aidl,可以將原有抽象方法basicTypes刪除:

package com.demo.testaidl;
interface Book {
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

新建一個同名的實體類Book實現Parcelable介面:

public class Book implements Parcelable {

    private String name;

    public Book(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "book name:" + name;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.name);
    }

    public void readFromParcel(Parcel dest) {
        name = dest.readString();
    }

    protected Book(Parcel in) {
        this.name = in.readString();
    }

    public static final Creator<Book> CREATOR = new Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel source) {
            return new Book(source);
        }

        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };
}

新建AIDL檔案,定義一個介面IBookManager,在這個介面裏宣告兩個方法,分別用於新增Book數據和獲取所有Book數據,因爲AIDL是介面定義語言,所以不能在AIDL檔案裡對方法進行實現:

interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
}

然後編譯伺服器端程式碼,會自動生成一個 IBookManager的java類, 裏面書寫了Binder相關程式碼:
在这里插入图片描述
IBookManager程式碼如下:

public interface IBookManager extends android.os.IInterface
{
  /** Default implementation for IBookManager. */
  public static class Default implements com.demo.testaidl.IBookManager
  {
    @Override public java.util.List<com.demo.testaidl.Book> getBookList() throws android.os.RemoteException
    {
      return null;
    }
    @Override public void addBook(com.demo.testaidl.Book book) throws android.os.RemoteException
    {
    }
    @Override
    public android.os.IBinder asBinder() {
      return null;
    }
  }
  /** Local-side IPC implementation stub class. */
  public static abstract class Stub extends android.os.Binder implements com.demo.testaidl.IBookManager
  {
    private static final java.lang.String DESCRIPTOR = "com.demo.testaidl.IBookManager";
    /** Construct the stub at attach it to the interface. */
    public Stub()
    {
      this.attachInterface(this, DESCRIPTOR);
    }
    /**
     * Cast an IBinder object into an com.demo.testaidl.IBookManager interface,
     * generating a proxy if needed.
     */
    public static com.demo.testaidl.IBookManager asInterface(android.os.IBinder obj)
    {
      if ((obj==null)) {
        return null;
      }
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
      if (((iin!=null)&&(iin instanceof com.demo.testaidl.IBookManager))) {
        return ((com.demo.testaidl.IBookManager)iin);
      }
      return new com.demo.testaidl.IBookManager.Stub.Proxy(obj);
    }
    @Override public android.os.IBinder asBinder()
    {
      return this;
    }
    @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
    {
      java.lang.String descriptor = DESCRIPTOR;
      switch (code)
      {
        case INTERFACE_TRANSACTION:
        {
          reply.writeString(descriptor);
          return true;
        }
        case TRANSACTION_getBookList:
        {
          data.enforceInterface(descriptor);
          java.util.List<com.demo.testaidl.Book> _result = this.getBookList();
          reply.writeNoException();
          reply.writeTypedList(_result);
          return true;
        }
        case TRANSACTION_addBook:
        {
          data.enforceInterface(descriptor);
          com.demo.testaidl.Book _arg0;
          if ((0!=data.readInt())) {
            _arg0 = com.demo.testaidl.Book.CREATOR.createFromParcel(data);
          }
          else {
            _arg0 = null;
          }
          this.addBook(_arg0);
          reply.writeNoException();
          return true;
        }
        default:
        {
          return super.onTransact(code, data, reply, flags);
        }
      }
    }
    private static class Proxy implements com.demo.testaidl.IBookManager
    {
      private android.os.IBinder mRemote;
      Proxy(android.os.IBinder remote)
      {
        mRemote = remote;
      }
      @Override public android.os.IBinder asBinder()
      {
        return mRemote;
      }
      public java.lang.String getInterfaceDescriptor()
      {
        return DESCRIPTOR;
      }
      @Override public java.util.List<com.demo.testaidl.Book> getBookList() throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        java.util.List<com.demo.testaidl.Book> _result;
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          boolean _status = mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            return getDefaultImpl().getBookList();
          }
          _reply.readException();
          _result = _reply.createTypedArrayList(com.demo.testaidl.Book.CREATOR);
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }
        return _result;
      }
      @Override public void addBook(com.demo.testaidl.Book book) throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          if ((book!=null)) {
            _data.writeInt(1);
            book.writeToParcel(_data, 0);
          }
          else {
            _data.writeInt(0);
          }
          boolean _status = mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            getDefaultImpl().addBook(book);
            return;
          }
          _reply.readException();
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }
      }
      public static com.demo.testaidl.IBookManager sDefaultImpl;
    }
    static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    public static boolean setDefaultImpl(com.demo.testaidl.IBookManager impl) {
      if (Stub.Proxy.sDefaultImpl == null && impl != null) {
        Stub.Proxy.sDefaultImpl = impl;
        return true;
      }
      return false;
    }
    public static com.demo.testaidl.IBookManager getDefaultImpl() {
      return Stub.Proxy.sDefaultImpl;
    }
  }
  public java.util.List<com.demo.testaidl.Book> getBookList() throws android.os.RemoteException;
  public void addBook(com.demo.testaidl.Book book) throws android.os.RemoteException;
}

其中一些重要的類與變數:

  • DESCRIPTOR:Binder的唯一標識,一般用當前Binder的類名錶示
  • asInterface():用戶端呼叫,將伺服器端的返回的Binder物件,轉換成用戶端所需要的AIDL介面型別物件。返回物件分兩種情況:若用戶端和伺服器端位於同一進程,則直接返回Stub物件本身;否則返回的是系統封裝後的Stub.proxy物件
  • asBinder():根據當前呼叫情況返回代理Proxy的Binder物件
  • onTransact():執行伺服器端的Binder執行緒池中,當用戶端發起跨進程請求時,遠端請求會通過系統底層封裝後交由此方法來處理
  • Proxy類:伺服器的本地代理,用戶端通過這個類呼叫伺服器的方法
    Proxy#getBookList:這個方法執行在用戶端,當用戶端遠端呼叫此方法時,它的內部實現:首先建立該方法所需要的輸入型Parcel物件 _data、 輸出型Parcel物件_reply 和返回值物件List;然後把該方法的參數資訊寫入_data 中(如果有參數的話);接着呼叫transact 方法來發起RPC (遠端過程呼叫)請求,同時當前執行緒掛起;然後伺服器端的onTransact方法會被呼叫,直到RPC過程返回後,當前執行緒繼續執行,並從_reply 中取出RPC過程的返回結果;最後返回 _reply 中的數據。
    Proxy#addBook: 這個方法執行在用戶端,它的執行過程和getBookList是一樣的,addBook沒有返回值,所以它不需要從_reply 中取出返回值。
  • transact():執行在用戶端,當用戶端發起遠端請求的同時將當前執行緒掛起。之後呼叫伺服器端的onTransact()直到遠端請求返回,當前執行緒才繼續執行。

編譯的時候可能會報錯,Book類未被發現啥的,這是因爲我們是在src/main/aidl資料夾下建立Book.java的,實際上這將因爲找不到Book.java而報錯,因爲在Android Studio中使用Gradle構建專案時,預設是在src/main/java資料夾中查詢java檔案的,如果把Book.java放在src/main/aidl對應包名下,自然就會找不到這個檔案了,所以需要修改app的build.gradle檔案,在sourceSets下新增對應的原始檔路徑,即src/main/aidl:

 sourceSets {
        main {
            java.srcDirs = ["src/main/java", "src/main/aidl"]
        }
    }

在这里插入图片描述
接着建立一個Service,實現AIDL的介面函數並暴露AIDL介面,這裏就叫做AIDLService吧:

public class AIDLService extends Service {

    private final String TAG = "Server";
    private List<Book> bookList;

    public AIDLService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //因爲AIDL方法是在伺服器端的Binder執行緒池中執行的,當有多個用戶端同時連線時,可能存在多個執行緒同時存取mStuList物件的情況    
        //CopyOnWriteArrayList支援併發讀寫,可以保證執行緒安全
        bookList = new CopyOnWriteArrayList<>();
        initData();
    }


    private void initData() {
        Book book1 = new Book("Android第一行程式碼");
        Book book2 = new Book("Android開發藝術探索");
        Book book3 = new Book("Android羣英傳");
        bookList.add(book1);
        bookList.add(book2);
        bookList.add(book3);
    }

    private final IBookManager.Stub stub = new IBookManager.Stub() {

        @Override
        public List<Book> getBookList() throws RemoteException {
            return bookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            if (book != null) {
                bookList.add(book);
            } else {
                Log.e(TAG, "接收到了一個空物件 InOut");
            }
        }

    };

    @Override
    public IBinder onBind(Intent intent) {
        return stub;
    }

}

這裏我們考慮單個應用多進程的情況,多應用的話之後也會提一下。AIDLService伺服器端另起一個進程,在AndroidManifest.xml組態檔中,宣告android:process=":remote",即可建立一個新的進程實現單應用多進程,從而模擬進程間通訊。

<service android:name=".AIDLService"
            android:process=":remote"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.demo.testaidl.AIDLService"/>
            </intent-filter>
</service>

用戶端

用戶端和伺服器端肯定是在不同的進程中,所以用戶端要想通過AIDL與遠端伺服器端通訊,那麼必須也要有伺服器端的這份AIDL程式碼。這裏分爲兩種情況:

  • 伺服器端與用戶端是兩個獨立應用,也就是多應用
    把伺服器端的aidl資料夾整個複製到用戶端的與java資料夾同級的目錄下,保持用戶端和伺服器端的aidl資料夾的目錄結構一致。這種情況下需要注意的是,如果前面的Book.java檔案是放置src/main/java對應包名路徑下,則在拷貝aidl資料夾到用戶端的同時,也要將對應的Book.java一併拷貝到用戶端相同的包名路徑下。

  • 我們上面考慮的單應用多進程
    這種情況下因爲用戶端與伺服器端同屬一個應用,兩個進程都可以使用這份AIDL程式碼,則不需要拷貝。用戶端進程即主進程,在MainActivity.java中系結遠端AIDLService,就可以向伺服器端進程remote發起請求了,修改MainActivity程式碼:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private Button bind,add,get,unbind;
    private IBookManager mIBookManager;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //獲取遠端服務Binder的代理
            mIBookManager = IBookManager.Stub.asInterface(service);
            if(mIBookManager == null){
                return;
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
        bind.setOnClickListener(this);
        add.setOnClickListener(this);
        get.setOnClickListener(this);
        unbind.setOnClickListener(this);
    }

    private void initViews() {
        bind = findViewById(R.id.bind);
        add = findViewById(R.id.add);
        get = findViewById(R.id.get);
        unbind = findViewById(R.id.unbind);
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.bind:
                bindService();
                break;
            case R.id.add:
                addBook();
                break;
            case R.id.get:
                getBookList();
                break;
            case R.id.unbind:
                unbindBookService();
                break;
            default:
                break;
        }
    }

    public void bindService() {
        Intent intent = new Intent();
        intent.setAction("com.demo.testaidl.AIDLService");
        intent.setPackage("com.demo.testaidl");
        startService(intent);
        bindService(intent, mConnection, BIND_AUTO_CREATE);
    }

    public void addBook() {
        try {
            mIBookManager.addBook(new Book("Android安全權威指南"));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    public void getBookList() {
        try {
            List<Book> books = mIBookManager.getBookList();
            Log.e("Client", "books:" + books.toString());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindBookService();
    }

    private void unbindBookService() {
        unbindService(mConnection);
        mIBookManager = null;
    }

}

修改佈局檔案:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/bind"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="系結服務"/>
    <Button
        android:id="@+id/add"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="新增數據"/>
    <Button
        android:id="@+id/get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="獲取數據"/>
    <Button
        android:id="@+id/unbind"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="解綁服務"/>
</LinearLayout>

效果如下圖所示:
在这里插入图片描述
先點選系結服務,然後獲取數據:
在这里插入图片描述
再點選新增數據後,點選獲取數據:
在这里插入图片描述
使用完後解綁服務即可。

4.Binder執行緒池

如果我們的應用有很多模組,而每一個模組都需要和伺服器端通訊,那麼我們也要爲每一個模組建立特定的aidl檔案,那麼伺服器端service也會產生很多個,顯然,如果aidl介面變多,那麼service也會跟着變多,那麼這樣的使用者體驗就會非常不好。《Android 開發藝術探索》中給出了一個Binder連線池的概念,即利用一個Binder連線池來管理所有Binder,伺服器端只需要管理這個Binder連線池即可,這樣就能實現一個service管理多個Binder,爲不同的模組返回不同的Binder,以實現進程間通訊。簡單來說Binder執行緒池將每個模組的Binder請求統一轉發到一個遠端Service中去執行,從而避免重複建立Service。

具體步驟如下:每個模組建立自己的AIDL介面並實現此介面,然後向伺服器端提供自己的唯一標識和其對應的Binder物件。伺服器端只需要一個Service,伺服器提供一個queryBinder介面,它會根據業務模組的特徵來返回相應的Binder對像,不同的業務模組拿到所需的Binder物件後就可進行遠端方法的呼叫。

詳細程式碼可參考: Android IPC機制 機製(四):細說Binder連線池

本文還參考了以下文章,如有理解錯誤還請指出: