AIDL淺析

黃燜雞米花發表於2018-05-27

官方說明直達

何時使用AIDL

在 Android 上,一個程式通常無法訪問另一個程式的記憶體。 儘管如此,程式需要將其物件分解成作業系統能夠識別的primitives,並將物件編組成跨越邊界的物件。 編寫執行這一編組操作的程式碼是一項繁瑣的工作,因此 Android 會使用 AIDL 來處理。

簡單地說,在android上跨程式無法直接互動資料資訊,需要經過一系列轉換,讓android底層來實現傳輸。

注:只有在跨程式且服務中處理多執行緒才需要用到AIDL;如果不需要跨程式只是IPC,用Binder就可以;如果不需要多執行緒用message就好了。

方式 條件
AIDL 需要IPC 跨程式,多執行緒
Binder 只有IPC 跨程式,多執行緒
Messager 只有IPC 單程式沒有多執行緒

基本語法&簡單例項

按照google官方的介紹,要使用AIDL需要三個步驟:

  1. 建立 .aidl 檔案
  2. 實現介面
  3. 向客戶端公開該介面

1.建立 .aidl 檔案

新建一個專案AidlTestRemote
AndroidStudio中的目錄結構
-main
|_aidl
|_java

AIDL淺析
示例程式碼: 建立的 IRemoteService.aidl 只含有一個add()方法

// IRemoteService.aidl
package com.example.android;

// Declare any non-default types here with import statements

/** 此處新建的是一個service介面 */
interface IRemoteService {
    /** 請求這個服務的程式ID,用它來搞事情. 
    * int getPid();
    */
    
    /** 這裡可以使用一些基本型別作為引數的抽象方法
     *  並通過AIDL返回
     *
     * void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
     */       
    int add(int num1 , int num2);
}
複製程式碼

2.服務端實現介面

在寫好.aidl檔案後,在 androidstudio 上 make project 專案,如下圖均可以

AIDL淺析
AIDL淺析

點選後androidstudio會在如下目錄自動生成與aidl同名的java檔案。
AIDL淺析
在生成的 IRemoteService.java 中會有一個.Stub的子類用於實現父類的所有方法,包含了在系統底層的互動。 這裡的 IRemoteService.java 自動生成,不同的aidl的抽象實現一致,也禁止修改。
This file is auto-generated. DO NOT MODIFY.
摺疊程式碼可以看到, IRemoteService.java 中實際是由一個Stub子類和之前定義的add方法組成。若要具體實現IRemoteService(此示例中為add(int num1, int num2)方法),則繼承已生成的Binder介面並實現從.aidl檔案繼承的方法。
AIDL淺析
那麼現在建立一個 service 來繼承binder並實現add(int num1, int num2)方法。

AIDL淺析
在新建的 RemoteCalculator service 中使用匿名例項 IRemoteService.Stub() 的介面並具體實現了IRemoteService的add(int num1, int num2)方法

AIDL淺析
現在 RemoteCalculator service 中的 mBinder 是Stub 類的一個例項用於定義服務的 RPC 介面,已經實現的add(int num1, int num2)方法。 當其他客戶端繫結到 RemoteCalculator service 時,會通過 onBind 方法返回得到mBinder,此時通過 IRemoteService.aidl 來傳送add()的引數就可以獲得RemoteCalculator service計算出的結果並返回給客戶端。

3.向客戶端公開該介面

上述完成的服務端的建立,簡單的說,就是先用aidl建一個通道,宣告好客戶端可以傳送什麼,服務端可以接收並返回什麼。服務端通過service來接收客戶端通過aidl傳送來的資料,經過處理後,再通過aidl返回給客戶端。實現了雙向的通訊。

既然服務端用service來處理客戶端的資料,自然客戶端要繫結上服務端的service。
為方便起見此時在專案中,新建一個 Module 名為aidltestclient。 將服務端的 IRemoteService.aidl 複製過來,要注意目錄結構完全一致

AIDL淺析
複製後,再點選 aidltestclient 的 make project,AS會在生成 IRemoteService.java,這與之前服務端完全一致。

AIDL淺析
在客戶端 aidltestclient 中,需要做的是:
1.繫結到服務端的 RemoteCalculator service,呼叫它的 add(int num1, int num2)方法
2.呼叫 add(int num1, int num2) 方法需要客服端傳入兩個int引數,傳參的操作需要通過IRemoteService.aidl實現
3.客戶端完成操作後要解綁service(易忽視)

4.繫結到 RemoteCalculator service

在onCreate方法中執行bindServie()方法:

private void bindServie() {
        Intent intent = new Intent();
        //android新版本上需要顯示發起intent,ComponentName需要傳入pkg name,cls name。注意cls name需要加上包名
        intent.setComponent(new ComponentName("com.wind.fitz.aidltestremote","com.wind.fitz.aidltestremote.RemoteCalculator"));
        //(Intent service, ServiceConnection conn,int flags)
        bindService(intent,conn, Context.BIND_AUTO_CREATE);
    }
複製程式碼

bindService的引數說明:intent不解釋;ServiceConnection 用來接收the service object when it is created and be told if it dies and restarts。簡單的說,ServiceConnection 用來接收繫結到的service的狀態並作出響應,不可為空,看下面conn的實現就很清楚;flags則是繫結服務時的可選操作,設定BIND_AUTO_CREATE可以在繫結時自動建立,也可設0.

因為 ServiceConnection 不可為空,所以:

IRemoteService iRemoteService;
……
private ServiceConnection conn = new ServiceConnection() {
        //繫結上服務
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iRemoteService = IRemoteService.Stub.asInterface(service);
        }
        //服務斷開
        @Override
        public void onServiceDisconnected(ComponentName name) {
            iRemoteService = null;
        }
    };
複製程式碼

在之前有說到,RemoteCalculator service 被繫結上時,會返回一個 mBinder

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return mBinder;
    }
複製程式碼

那麼在 onServiceConnected(ComponentName name, IBinder service) 中的 IBinder service 實質上就是 RemoteCalculator service 中的 mBinder ,那麼在 RemoteCalculator service 中 private Binder mBinder = new IRemoteService.Stub(){}, 所以這麼一操作,conn 中的 IBinder service 就是遠端服務的 service 。
此時接收一下就好了 iRemoteService = IRemoteService.Stub.asInterface(service);(為何如此接收後面分析)
至此客戶端就拿到了遠端服務端的 service 即 iRemoteService。
客戶端想要的結果就可以這麼來獲得 int result = iRemoteService.add(num1,num2);

通過 IPC 傳遞物件

官方說明:通過 IPC 介面把某個類從一個程式傳送到另一個程式是可以實現的。 不過,您必須確保該類的程式碼對 IPC 通道的另一端可用,並且該類必須支援 Parcelable 介面。支援 Parcelable 介面很重要,因為 Android 系統可通過它將物件分解成可編組到各程式的原語。

傳遞自定型別是可以的,但必須支援 Parcelable 介面。

  1. 自定義類實現 Parcelable 介面。
  2. 實現 writeToParcel,它會獲取物件的當前狀態並將其寫入 Parcel。
  3. 自定義類新增一個名為 CREATOR 的靜態欄位,這個欄位是一個實現 Parcelable.Creator 介面的物件。
  4. 最後,建立一個宣告可打包類的 .aidl 檔案。

1.建立自定義類

新建一個 IMyBook 類繼承 Parcelable
包含name,price,year,author四個屬性。 繼承 Parcelable 介面需要實現以下三個方法

    //注意拆包順序要與打包順序一致
    protected IMyBook(Parcel in) {
        this.name = in.readString();
        this.price = in.readInt();
        this.year = in.readInt();
        this.author = in.readString();
    }
    
    public static final Creator<IMyBook> CREATOR = new Creator<IMyBook>() {
        //拆包
        @Override
        public IMyBook createFromParcel(Parcel in) {
            //獲取的in寫入新book物件
            return new IMyBook(in);//返回的新IMyBook物件到上面拆包
        }

        //預設
        @Override
        public IMyBook[] newArray(int size) {
            return new IMyBook[size];
        }
    };
    //預設
    @Override
    public int describeContents() {
        return 0;
    }

    //打包
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        //將book屬性打包
        dest.writeString(name);
        dest.writeInt(price);
        dest.writeInt(year);
        dest.writeString(author);

    }
複製程式碼

AIDL淺析

IMyBook 類也需要建議對應的aidl檔案

AIDL淺析

AIDL淺析

2.建立服務端

新建 IRemote.aidl 注意導包,介面裡寫一個向list新增物件的抽象方法。

AIDL淺析

make project,生成 IMyBook.java 和 IRemote.java 後。建立 service 實現 IRemote 介面

AIDL淺析

3.客戶端繫結服務

客戶端將aidl檔案複製到同級目錄,同時 IMyBook 類也要複製到對應包下

AIDL淺析
和之前一樣,建立 ServiceConnection

AIDL淺析

繫結到服務

AIDL淺析
呼叫服務端service方法,並獲取返回值。完成了通訊。

AIDL淺析

注意事項

客戶端在呼叫遠端方法時,要確保在呼叫前繫結到遠端服務!! 如下錯誤寫法:
在點選事件內繫結服務,會導致服務未繫結完成就開始呼叫 add 方法。

AIDL淺析
則會出現空指標。當然出現空指標也可能是服務啟動失敗,可以先檢查是否繫結錯誤,再深入分析。

AIDL淺析

基本資料類

預設情況下,AIDL 支援下列資料型別:

  1. Java八種基本資料型別(int、char、boolean、double、float、byte、long、string) 但不支援short
  2. String、CharSequence
  3. List和Map
  4. Parcelable
  5. List 中的所有元素都必須是以上列表中支援的資料型別、其他 AIDL 生成的介面或您宣告的可打包型別。 可選擇將 List 用作“通用”類(例如,List)。另一端實際接收的具體類始終是 ArrayList,但生成的方法使用的是 List 介面。
  6. Map 中的所有元素都必須是以上列表中支援的資料型別、其他 AIDL 生成的介面或您宣告的可打包型別。 不支援通用 Map(如 Map<String,Integer> 形式的 Map)。 另一端實際接收的具體類始終是 HashMap,但生成的方法使用的是 Map 介面。

您必須為以上未列出的每個附加型別加入一個 import 語句,即使這些型別是在與您的介面相同的軟體包中定義。

AIDL原理

先放圖

AIDL淺析

看下圖,IRemote.java 是make project後AS自動生成的。看他的結構實際就是一個 Stub 和 add 方法。

add方法就是開發者定義在IRemote.aidl中的方法。可以看到它返回一個list,丟擲RemoteException。
public java.util.List<com.wind.fitz.ipcaidl.IMyBook> add(com.wind.fitz.ipcaidl.IMyBook book) throws android.os.RemoteException;

這個 Stub 子類它繼承自 Binder 並實現IRemote(自身)的介面。

public static abstract class Stub extends android.os.Binder implements com.wind.fitz.ipcaidl.IRemote

AIDL淺析
裡面通過一個Stub()構造方法來連線。

/** Construct the stub at attach it to the interface. */在連線到介面時構建存根
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
複製程式碼

在客戶端 ServiceConnection 中通過如下方式來獲得遠端服務: iRemote = IRemote.Stub.asInterface(service); 那麼 **.Stub.asInterface() 實際上是返回一個 Proxy

/**
 * Cast an IBinder object into an com.wind.fitz.ipcaidl.IRemote interface,
 * generating a proxy if needed.
 */
public static com.wind.fitz.ipcaidl.IRemote asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.wind.fitz.ipcaidl.IRemote))) {
return ((com.wind.fitz.ipcaidl.IRemote)iin);
}
return new com.wind.fitz.ipcaidl.IRemote.Stub.Proxy(obj);  //a
}
複製程式碼

那麼去看 Proxy ,上面的 a 實際走到了 b ,返回一個 mRemote (Binder物件)即 IRemote 的代理給了客戶端的 iRemote。

private static class Proxy implements com.wind.fitz.ipcaidl.IRemote
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;  //b
}
複製程式碼

那麼再看客戶端呼叫服務的方法:
iRemote.add(new IMyBook("aaa",100,2018,"bb")); 已經知道客戶端的 iRemote 實際上是 IRemote.java 中的 mRemote。 iRemote.add 呼叫的是 Proxy 重寫的 add 方法

@Override public java.util.List<com.wind.fitz.ipcaidl.IMyBook> add(com.wind.fitz.ipcaidl.IMyBook book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.wind.fitz.ipcaidl.IMyBook> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);   //c
_reply.readException();
_result = _reply.createTypedArrayList(com.wind.fitz.ipcaidl.IMyBook.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
複製程式碼

在重寫的 add 方法中,是一系列的序列化操作,通過 mRemote.transact 傳送給底層(上述流程在Proxy內)。 之後 Stub onTransact 接收到 Proxy 傳送來的資料。再聯絡到服務端。 不嚴謹的解釋:

client -- [ proxy - stub ] -- server