上一章節,我們講到了Android中的Binder機制,一個Android開發天天用到,但又不明就理的神密存在。這一節接著Binder這個話題,講一講AIDL,讓大家對Binder機制有一個直觀的認識。
面試題:AIDL是什么?你有使用過它嗎,它支持哪些數據類型?
AIDL是Android Interface Definition Language的簡寫,即Android接口定義語言。我們知道Android系統為每一個應用開啟一個獨立的虛擬機,每個應用都運行在各自進程里(默認情況下),彼此之間相互獨立,無法共享內存。當一個應用想要訪問另一個應用的數據或調用其方法,就要用到Android系統提供的IPC機制。而AIDL就是Android實現IPC機制的方式之一。
除了AIDL,Android還提供了Messenger來實現跨進程通信,不過Messenger是以單線程串行方式(消息隊列)來處理來自不同客戶端的訪問的,并不適合多線程并發訪問。當需要提供跨進程以及多線程并發服務時就需要AIDL上場了。
Messenger實際上也是以AIDL作為其底層結構。
其實要應對AIDL相關的面試題,除了了解清楚它的作用和注意的事項外,最有效的手段莫過于自己動手寫幾次。這里給大家定兩個小目標:
- 會使用AIDL進行進程間通信;
- 會手寫AIDL的編碼,加深對Binder機制的理解。
創建AIDL
我們先實現第一個,在Android Studio中創建一個簡單的AIDL項目,實現IPC通信。
Step1. 創建.aidl文件
我們在對應的src的Package下創建一個AIDL文件(Android Studio->File->New->AIDL->AIDL file),創建后Android Studio會自動把這個.aidl文件放到一個aidl的目錄下。
Android SDK Tool會根據我們的.aidl文件自動生成一個同名的.java文件,如:AIDLTest/app/build/generated/source/aidl/debug/net/goeasyway/aidltest/IRemoteService.java
basicTypes方法中給我們展示了AIDL支持的基本數據類型,除此之外,AIDL還支持:CharSequence, List & Map(List和Map中的所有元素都必須是AIDL支持的數據類型、其他AIDL生成的接口或您聲明的可打包類型。)
Step2. 創建一個Service暴露AIDL接口并實現AIDL的接口函數
如下代碼,創建一個Service:
public class RemoteService extends Service {
public RemoteService() {
}
@Override
public IBinder onBind(Intent intent) {
return binder; //暴露給客戶端
}
// 實現AIDL接口
private final IRemoteService.Stub binder = new IRemoteService.Stub() {
@Override
public int getPid() throws RemoteException {
return Process.myPid();
}
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString) throws RemoteException {
}
};
}
然后在MainActivity通過bindService綁定這個服務,即可以獲得AIDL的接口調用的引用。運行前,我們設置一下AndroidManifest.xml文件記這個Service運行在一個單獨的進程中:
<service
android:name=".RemoteService"
android:process=":remote"
android:enabled="true"
android:exported="true"/>
現在來bindService:
public class MainActivity extends AppCompatActivity {
private final static String TAG = "MainActivity";
private IRemoteService remoteService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent();
intent.setClass(this, RemoteService.class);
bindService(intent, connection, Service.BIND_AUTO_CREATE); // 綁定服務
}
private ServiceConnection connection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
remoteService = IRemoteService.Stub.asInterface(service); //獲取AIDL的接口實現引用
try {
Log.i(TAG, "Client pid= " + Process.myPid());
Log.i(TAG, "RemoteService pid= " + remoteService.getPid());
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void onServiceDisconnected(ComponentName className) {
Log.e(TAG, "Service has unexpectedly disconnected");
remoteService = null;
}
};
}
從輸出的日志看到Service和Activity運行在兩個不同的進程中:
02-05 09:51:40.154 18992-18992/net.goeasyway.aidltest I/MainActivity: Client pid= 18992
02-05 09:51:40.154 18992-18992/net.goeasyway.aidltest I/MainActivity: RemoteService pid= 19022
到這里,我們完成了一個AIDL的范例,有幾個地方可能會對我們造成困擾:
- Stub類:Binder的實現類,服務端需要實現這個類來提供服務。
- asInterface函數: 一個靜態函數,用來將IBinder轉換成對應的Binder的引用。先通過queryLocalInterface查詢,如果服務端和客戶端都是在同一個進程,那么就不需要跨進程了,直接將IRemoteService當做普通的對象來使用,否則會返回遠程對象的代理對象(Proxy)。
public static net.goeasyway.aidltest.IRemoteService asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof net.goeasyway.aidltest.IRemoteService))) {
return ((net.goeasyway.aidltest.IRemoteService)iin);
}
return new net.goeasyway.aidltest.IRemoteService.Stub.Proxy(obj);
}
通過IPC傳遞對象
現在,我們加大一下難度,AIDL的接口使用一個我們自己定義的類為參數(或者返回值)。實現步驟如下:
- 添加一個自定義對象類,并且要實現Parcelable接口,如MyProcess.java;
- 在AIDL目錄下的相同Pacage下添加一個同名的AIDL文件,如MyProcess.aidl;
注意:通過“Android Studio->File->New->AIDL->AIDL file”不讓你創建和MyProcess.java同名的AIDL文件,你可以直接用通過“Android Studio->File->New->File”創建一個MyProcess.aidl。
- 在AIDL接口類中添加一個接口函數,使用MyProcess做為參數或者返回值;
其他的細節大家可以直接查看Github上的代碼:https://github.com/goeasyway/AIDL_Test (或者查看提交的說明找到具體每次的代碼區別:https://github.com/goeasyway/AIDL_Test/commits/master )
in、out & inout
這節我們看到“MyProcess getProcess(in MyProcess clientProcess);”這個接口的參數有一個“in”修飾符,這也是一個常見的面試題,可以考察一下對方是否真的寫過AIDL的代碼。
問題:AIDL中的接口函數有時會使用in、out或者inout的參數修飾符,它們各表示什么意思?在什么情況下要使用呢?
in、out和inout表示數據的流向。大家可以把AIDL的客戶端和服務端理解成兩個進程(其實大多數情況也是這樣才會使用AIDL),從客戶端流向服務端用in表示,表示這個對象是從客戶端中傳遞到服務端,在服務端修改這個對象不會對客戶端輸入的對象產生影響。
而out則表示,數據只能從服務端影響客戶端,即客戶端輸入這個參數時,服務端并不能獲取到客戶端的具體實例中的數據,而是生成一個默認數據,但是服務端對這個默認數據的修改會影響到客戶端的這個類對象實例發生相應的改變。
理解了in、out之后,inout自然不需要再解釋了。AIDL默認支持的數據類型使用in修飾符,對于我們自定義的Parcelable對象,一般情況下我們也是使用in,如果沒有必要,應該盡量避免inout。
Intent也是Parcelable實現
也許你會想到,我們在Activity間可以通過Intent攜帶參數,其實你去看的源碼的話會發現Intent也是一個Parcelable的實現類,而且在系統的工程中也有一個Intent.aidl文件(路徑:/frameworks/base/core/java/android/content/Intent.aidl)。所以,它才可以在進程間傳遞。
注:目前Client端和Server端在同一工程中,如果分開在不同的工程的話,Client端所在的工程要把Server端提供的.aidl復制到同名Pacage的AIDL代碼目錄下。
手動方式創建AIDL(不依賴AIDL工具,手寫遠程AIDL的代碼完成跨進程通信)
通過AIDL,可以讓本地調用遠程服務的接口就像調用本地接口那么簡單,讓用戶無需關注內部細節,只需要實現自己的業務邏輯接口,內部復雜的參數序列化發送、接收、客戶端調用服務端的邏輯,用戶并不需要關心。
AIDL的代碼生成器,已經根據.aidl文件自動幫我們生成Proxy、Stub(抽象類)兩個類,并且把客戶端代理mRemote的transact()過程以及服務器端的onTtransact()過程默認實現好了,我們只需要在服務端繼承Stub,實現自己的業務方法即可。
但現在,為了進一步加深對Binder機制的理解,我們來做一個手動實現編寫AIDL相關代碼的練習。
具體的代碼大家可以參考:https://github.com/goeasyway/AIDL_Test/tree/master/app/src/main/java/net/goeasyway/aidltest/diy
這個包里有三個類:IRmote.java為接口,Stub.java為Binder實現類(Service端要實例化它并在onBind返回),Proxy.java為代理類,提供給客戶端使用的,通過Binder驅動和服務端通信。
大家可以看到這個練習不需要.aidl文件。
在這幾個代碼中,大家需要搞清楚這個類(或者接口)的關系:
- Binder
Binder本地對象。 - IBinder
IBinder是一個接口,它代表了一種跨進程傳輸的能力。 - IInterface
IBinder負責數據傳輸,那么client與server端的調用契約呢?這里的IInterface代表的就是遠程server對象具有什么能力。具體來說,就是aidl里面的接口。 - Proxy
代表遠程進程的Binder對象的本地代理,繼承自IBinder,因而具有跨進程傳輸的能力。實際上,在跨越進程的時候,Binder驅動會自動完成代理對象和本地對象的轉換。 - Stub
這個類繼承了Binder, 說明它是一個Binder本地對象,它實現了IInterface接口,表明它具有遠程Server承諾給Client的能力;Stub是一個抽象類,具體的IInterface的相關實現需要我們手動完成。
如果覺得有點難理解的話,不妨先動手寫了再來看。
小結
如何使用AIDL應該是一個高級工程師必備技能,如果你之前不太了解它的話,那么我強烈建議你完成上面的兩個小練習,再回過頭去看它的解說。
之后,你將不再懼怕和AIDL相關的面試題。