Android 進階7:程序通訊之 AIDL 的使用
讀完本文你將瞭解:
記得 2015 年實習面試,筆試題裡就有這道題:請介紹下 AIDL。
當時的我是懵逼的,只好老老實實空著。沒想到後來面試時面試官大哥嘿嘿一笑說他也沒用過這玩意,真是夠實誠的。
筆試完查了這個知識點,似懂非懂也沒深究。去年看《安卓開發藝術探索》時也學了這部分內容,但是可能當時水平不夠,或者只是看起來努力,沒有真正理解精髓,沒多久就又忘了個七八成。
這次複習,還是老老實實敲出來,總結成文字吧,方便以後回顧。
AIDL 是什麼
AIDL(Android 介面定義語言) 是 Android 提供的一種程序間通訊 (IPC) 機制。
我們可以利用它定義客戶端與服務使用程序間通訊 (IPC) 進行相互通訊時都認可的程式設計介面。
在 Android 上,一個程序通常無法訪問另一個程序的記憶體。 儘管如此,程序需要將其物件分解成作業系統能夠識別的原語,並將物件編組成跨越邊界的物件。
編寫執行這一編組操作的程式碼是一項繁瑣的工作,因此 Android 會使用 AIDL 來處理。
通過這種機制,我們只需要寫好 aidl 介面檔案,編譯時系統會幫我們生成 Binder 介面。
AIDL 支援的資料型別
共 4 種:
- Java 的基本資料型別
- List 和 Map
- 元素必須是 AIDL 支援的資料型別
- Server 端具體的類裡則必須是 ArrayList 或者 HashMap
- 其他 AIDL 生成的介面
- 實現 Parcelable 的實體
AIDL 如何編寫
AIDL 的編寫主要為以下三部分:
- 建立 AIDL
- 建立要操作的實體類,實現
Parcelable
介面,以便序列化/反序列化 - 新建 aidl 資料夾,在其中建立介面 aidl 檔案以及實體類的對映 aidl 檔案
- Make project ,生成 Binder 的 Java 檔案
- 建立要操作的實體類,實現
- 服務端
- 建立 Service,在其中建立上面生成的 Binder 物件例項,實現介面定義的方法
- 在
onBind()
中返回
- 客戶端
- 實現
ServiceConnection
介面,在其中拿到 AIDL 類 bindService()
- 呼叫 AIDL 類中定義好的操作請求
- 實現
AIDL 例項
下面以例項程式碼演示一個 AIDL 的編寫。
1.建立 AIDL
①建立要操作的實體類,實現 Parcelable
介面,以便序列化/反序列化
package net.sxkeji.shixinandroiddemo2.bean;
import android.os.Parcel;
import android.os.Parcelable;
public class Person implements Parcelable {
private String mName;
public Person(String name) {
mName = name;
}
protected Person(Parcel in) {
mName = in.readString();
}
public static final Creator<Person> CREATOR = new Creator<Person>() {
@Override
public Person createFromParcel(Parcel in) {
return new Person(in);
}
@Override
public Person[] newArray(int size) {
return new Person[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(mName);
}
@Override
public String toString() {
return "Person{" +
"mName='" + mName + '\'' +
'}';
}
}
實現 Parcelable 介面是為了後序跨程序通訊時使用。
注意 實體類所在的包名。
②新建 aidl 資料夾,在其中建立介面 aidl 檔案以及實體類的對映 aidl 檔案
在 main 資料夾下新建 aidl 資料夾,使用的包名要和 java 資料夾的包名一致:
先建立實體類的對映 aidl 檔案,Person.aidl:
// Person.aidl
package net.sxkeji.shixinandroiddemo2.bean;
//還要和宣告的實體類在一個包裡
parcelable Person;
在其中宣告對映的實體類名稱與型別
注意,這個 Person.aidl 的包名要和實體類包名一致。
然後建立介面 aidl 檔案,IMyAidl.aidl:
// IMyAidl.aidl
package net.sxkeji.shixinandroiddemo2;
// Declare any non-default types here with import statements
import net.sxkeji.shixinandroiddemo2.bean.Person;
interface IMyAidl {
/**
* 除了基本資料型別,其他型別的引數都需要標上方向型別:in(輸入), out(輸出), inout(輸入輸出)
*/
void addPerson(in Person person);
List<Person> getPersonList();
}
在介面 aidl 檔案中定義將來要在跨程序進行的操作,上面的介面中定義了兩個操作:
- addPerson: 新增 Person
- getPersonList:獲取 Person 列表
需要注意的是:
- 非基本型別的資料需要匯入,比如上面的 Person,需要匯入它的全路徑。
- 這裡的 Person 我理解的是 Person.aidl,然後通過 Person.aidl 又找到真正的實體 Person 類。
- 方法引數中,除了基本資料型別,其他型別的引數都需要標上方向型別
- in(輸入), out(輸出), inout(輸入輸出)
③Make Project ,生成 Binder 的 Java 檔案
AIDL 真正的強大之處就在這裡,通過簡單的定義 aidl 介面,然後編譯,就會為我們生成複雜的 Java 檔案。
點選 Build
-> Make Project
,然後等待構建完成。
然後就會在 build/generated/source/aidl/你的 flavor/
下生成一個 Java 檔案:
現在我們有了跨程序 Client 和 Server 的通訊媒介,接著就可以編寫客戶端和服務端程式碼了。
我們先跑通整個過程,這個檔案的內容下篇文章介紹。
2.編寫服務端程式碼
建立 Service,在其中建立上面生成的 Binder 物件例項,實現介面定義的方法;然後在 onBind()
中返回
建立將來要執行在另一個程序的 Service,在其中實現了 AIDL 介面中定義的方法:
public class MyAidlService extends Service {
private final String TAG = this.getClass().getSimpleName();
private ArrayList<Person> mPersons;
/**
* 建立生成的本地 Binder 物件,實現 AIDL 制定的方法
*/
private IBinder mIBinder = new IMyAidl.Stub() {
@Override
public void addPerson(Person person) throws RemoteException {
mPersons.add(person);
}
@Override
public List<Person> getPersonList() throws RemoteException {
return mPersons;
}
};
/**
* 客戶端與服務端繫結時的回撥,返回 mIBinder 後客戶端就可以通過它遠端呼叫服務端的方法,即實現了通訊
* @param intent
* @return
*/
@Nullable
@Override
public IBinder onBind(Intent intent) {
mPersons = new ArrayList<>();
LogUtils.d(TAG, "MyAidlService onBind");
return mIBinder;
}
}
上面的程式碼中,建立的物件是一個 IMyAidl.Stub()
,它是一個 Binder,具體為什麼是它我們下篇文章介紹。
別忘記在 Manifest 檔案中宣告:
<service
android:name="net.sxkeji.shixinandroiddemo2.service.MyAidlService"
android:enabled="true"
android:exported="true"
android:process=":aidl"/>
服務端實現了介面,在 onBind()
中返回這個 Binder,客戶端拿到就可以操作資料了。
3.編寫客戶端程式碼
這裡我們以一個 Activity 為客戶端。
①實現 ServiceConnection
介面,在其中拿到 AIDL 類
private IMyAidl mAidl;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//連線後拿到 Binder,轉換成 AIDL,在不同程序會返回個代理
mAidl = IMyAidl.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mAidl = null;
}
};
在 Activity 中建立一個服務連線物件,在其中呼叫 IMyAidl.Stub.asInterface()
方法將 Binder 轉為 AIDL 類。
②接著繫結服務
Intent intent1 = new Intent(getApplicationContext(), MyAidlService.class);
bindService(intent1, mConnection, BIND_AUTO_CREATE);
要執行 IPC,必須使用 bindService()
將應用繫結到服務上。
注意:
5.0 以後要求顯式呼叫 Service,所以我們無法通過 action 或者 filter 的形式呼叫 Service,具體內容可以看這篇文章 Android 進階:Service 的一些細節。
③拿到 AIDL 類後,就可以呼叫 AIDL 類中定義好的操作,進行跨程序請求
@OnClick(R.id.btn_add_person)
public void addPerson() {
Random random = new Random();
Person person = new Person("shixin" + random.nextInt(10));
try {
mAidl.addPerson(person);
List<Person> personList = mAidl.getPersonList();
mTvResult.setText(personList.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
執行結果
可以看到,Activity 與 另外一個程序的 Service 通訊成功了。
總結
這篇文章介紹了 AIDL 的簡單編寫流程,其中也踩過一些坑,比如檔案所在包的路徑不統一,繫結服務收不到回撥等問題。
到最後雖然跨程序通訊成功,但是我們還是有很多疑問的,比如:
- AIDL 生成的檔案內容?
- 什麼是 Binder?
- 為什麼要這麼寫?
知其然還要知其所以然,這一切都要從 Binder 講起,且聽下一回合介紹。