AndroidIPC機制(3)-AIDL
一、概述
AIDL 意思即 Android Interface Definition Language,翻譯過來就是 Android介面定義語言 ,是用於定義伺服器和客戶端通訊介面的一種描述語言,可以拿來生成用於 IPC 的程式碼。從某種意義上說 AIDL 其實是一個模板,因為在使用過程中,實際起作用的並不是 AIDL 檔案,而是據此而生成的一個 IInterface 的例項程式碼,AIDL 其實是為了避免我們重複編寫程式碼而出現的一個模板
設計 AIDL 這門語言的目的就是為了實現程序間通訊。在 Android 系統中,每個程序都執行在一塊獨立的記憶體中,在其中完成自己的各項活動,與其他程序都分隔開來。可是有時候我們又有應用間進行互動的需求,比較傳遞資料或者任務委託等,AIDL 就是為了滿足這種需求而誕生的。通過 AIDL,可以在一個程序中獲取另一個程序的資料和呼叫其暴露出來的方法,從而滿足程序間通訊的需求
通常,暴露方法給其他應用進行呼叫的應用稱為服務端,呼叫其他應用的方法的應用稱為客戶端,客戶端通過繫結服務端的 Service 來進行互動
二、語法
AIDL 的語法十分簡單,與Java語言基本保持一致,需要記住的規則有以下幾點:
- AIDL檔案以 .aidl 為字尾名
- AIDL支援的資料型別分為如下幾種:
- 八種基本資料型別:byte、char、short、int、long、float、double、boolean
- String,CharSequence
- 實現了Parcelable介面的資料型別
- List 型別。List承載的資料必須是AIDL支援的型別,或者是其它宣告的AIDL物件
- Map型別。Map承載的資料必須是AIDL支援的型別,或者是其它宣告的AIDL物件
- AIDL檔案可以分為兩類。一類用來宣告實現了Parcelable介面的資料型別,以供其他AIDL檔案使用那些非預設支援的資料型別。還有一類是用來定義介面方法,宣告要暴露哪些介面給客戶端呼叫,定向Tag就是用來標註這些方法的引數值
- 定向Tag。定向Tag表示在跨程序通訊中資料的流向,用於標註方法的引數值,分為 in、out、inout 三種。其中 in 表示資料只能由客戶端流向服務端, out 表示資料只能由服務端流向客戶端,而 inout 則表示資料可在服務端與客戶端之間雙向流通。此外,如果AIDL方法介面的引數值型別是:基本資料型別、String、CharSequence或者其他AIDL檔案定義的方法介面,那麼這些引數值的定向 Tag 預設是且只能是 in,所以除了這些型別外,其他引數值都需要明確標註使用哪種定向Tag
- 明確導包。在AIDL檔案中需要明確標明引用到的資料型別所在的包名,即使兩個檔案處在同個包名下
現在,我來模擬一種 IPC 的流程
服務端 (com.czy.aidl_server) 向外提供了進行數學計算的能力(其實就是對兩個整數進行相乘)。客戶端 (com.czy.aidl_client) 需要進行計算時就將資料(包含了一個整數值的序列化類)傳遞給服務端進行運算,運算結果會返回給客戶端。注意,服務端和客戶端是兩個不同的應用,因此自然也是處於不同的程序中,以此來進行 IPC
三、服務端
服務端是提供運算操作能力的一方,所以除了需要設定運算引數的格式外,還需要提供運算方法
此處,用 Parameter 類作為運算引數
/** * 作者:leavesC * 時間:2019/4/4 10:46 * 描述:包含一個進行運算操作的 int 型別資料 */ public class Parameter implements Parcelable { private int param; public Parameter(int param) { this.param = param; } public int getParam() { return param; } public void setParam(int param) { this.param = param; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.param); } protected Parameter(Parcel in) { this.param = in.readInt(); } public static final Parcelable.Creator<Parameter> CREATOR = new Parcelable.Creator<Parameter>() { @Override public Parameter createFromParcel(Parcel source) { return new Parameter(source); } @Override public Parameter[] newArray(int size) { return new Parameter[size]; } }; }
相對應的 AIDL 檔案
package leavesc.hello.aidl_server; parcelable Parameter;
此外,還需要一個向外暴露運算方法的 AIDL 介面
package leavesc.hello.aidl_server; import leavesc.hello.aidl_server.Parameter; interface IOperationManager { //接收兩個引數,並將運算結果返回給客戶端 Parameter operation(in Parameter parameter1 , in Parameter parameter2); }
然後,在 Service 中進行實際的運算操作,並將運算結果返回
/** * 作者:葉應是葉 * 時間:2018/3/18 17:35 * 描述:https://github.com/leavesC */ public class AIDLService extends Service { private static final String TAG = "AIDLService"; private IOperationManager.Stub stub = new IOperationManager.Stub() { @Override public Parameter operation(Parameter parameter1, Parameter parameter2) throws RemoteException { Log.e(TAG, "operation 被呼叫"); int param1 = parameter1.getParam(); int param2 = parameter2.getParam(); return new Parameter(param1 * param2); } }; public AIDLService() { } @Override public IBinder onBind(Intent intent) { return stub; } }
這樣,服務端的介面就設計好了,檔案目錄如下所示

四、客戶端
將服務端的兩個 AILD 檔案以及 Parameter 類複製到客戶端,保持檔案路徑(包名)不變
檔案目錄如下所示

指定服務端的包名和 Service 路徑,繫結服務,向其傳遞兩個待運算引數並將運算結果展示出來
/** * 作者:葉應是葉 * 時間:2018/3/18 17:51 * 描述:https://github.com/leavesC * 客戶端 */ public class MainActivity extends AppCompatActivity { private EditText et_param1; private EditText et_param2; private EditText et_result; private IOperationManager iOperationManager; private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { iOperationManager = IOperationManager.Stub.asInterface(service); } @Override public void onServiceDisconnected(ComponentName name) { iOperationManager = null; } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); bindService(); } private void bindService() { Intent intent = new Intent(); intent.setClassName("com.czy.aidl_server", "com.czy.aidl_server.AIDLService"); bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); } private void initView() { et_param1 = findViewById(R.id.et_param1); et_param2 = findViewById(R.id.et_param2); et_result = findViewById(R.id.et_result); Button btn_operation = findViewById(R.id.btn_operation); btn_operation.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { if (TextUtils.isEmpty(et_param1.getText()) || TextUtils.isEmpty(et_param2.getText())) { return; } int param1 = Integer.valueOf(et_param1.getText().toString()); int param2 = Integer.valueOf(et_param2.getText().toString()); Parameter parameter1 = new Parameter(param1); Parameter parameter2 = new Parameter(param2); if (iOperationManager != null) { try { Parameter resultParameter = iOperationManager.operation(parameter1, parameter2); et_result.setText("運算結果: " + resultParameter.getParam()); } catch (RemoteException e) { e.printStackTrace(); } } } }); } @Override protected void onDestroy() { super.onDestroy(); if (serviceConnection != null) { unbindService(serviceConnection); } } }
執行結果如下所示

可以看到,得到了正確的運算結果了,這就完成了一次簡單的 IPC :客戶端將引數傳遞給了服務端,服務端接收引數並進行計算,並將計算結果返回給客戶端
五、註冊回撥函式
在上一節的例子裡的運算操作只是將引數進行乘法操作,當然能夠很快獲得返回值,但如果是要進行耗時操作,那這種方式就不太合適了,所以可以以註冊回撥函式的方式來獲取運算結果。即客戶端向服務端註冊一個回撥函式用於接收運算結果,而不用傻乎乎地一直等待返回值
因此,首先需要先宣告一個 AIDL 介面 IOnOperationCompletedListener
,用於傳遞運算結果
package com.czy.aidl_server; import com.czy.aidl_server.Parameter; interface IOnOperationCompletedListener { void onOperationCompleted(in Parameter result); }
將 IOperationManager
的 operation
方法改為無返回值,新增註冊回撥函式和解除註冊函式的方法
package com.czy.aidl_server; import com.czy.aidl_server.Parameter; import com.czy.aidl_server.IOnOperationCompletedListener; interface IOperationManager { void operation(in Parameter parameter1 , in Parameter parameter2); void registerListener(in IOnOperationCompletedListener listener); void unregisterListener(in IOnOperationCompletedListener listener); }
在 operation
方法中讓執行緒休眠五秒,模擬耗時操作,然後再將運算結果傳遞出去
/** * 作者:葉應是葉 * 時間:2018/3/18 17:35 * 描述:https://github.com/leavesC */ public class AIDLService extends Service { private static final String TAG = "AIDLService"; private CopyOnWriteArrayList<IOnOperationCompletedListener> copyOnWriteArrayList; private IOperationManager.Stub stub = new IOperationManager.Stub() { @Override public void operation(Parameter parameter1, Parameter parameter2) throws RemoteException { try { Log.e(TAG, "operation 被呼叫,延時5秒,模擬耗時計算"); Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } int param1 = parameter1.getParam(); int param2 = parameter2.getParam(); Parameter result = new Parameter(param1 * param2); for (IOnOperationCompletedListener listener : copyOnWriteArrayList) { listener.onOperationCompleted(result); } Log.e(TAG, "計算結束"); } @Override public void registerListener(IOnOperationCompletedListener listener) throws RemoteException { Log.e(TAG, "registerListener"); if (!copyOnWriteArrayList.contains(listener)) { Log.e(TAG, "註冊回撥成功"); copyOnWriteArrayList.add(listener); } else { Log.e(TAG, "回撥之前已註冊"); } } @Override public void unregisterListener(IOnOperationCompletedListener listener) throws RemoteException { Log.e(TAG, "unregisterListener"); if (copyOnWriteArrayList.contains(listener)) { copyOnWriteArrayList.remove(listener); Log.e(TAG, "解除註冊回撥成功"); } else { Log.e(TAG, "該回調沒有被註冊過"); } } }; public AIDLService() { copyOnWriteArrayList = new CopyOnWriteArrayList<>(); } @Override public IBinder onBind(Intent intent) { return stub; } }
客戶端這邊一樣要修改相應的 AIDL 檔案
新增兩個按鈕用於註冊和解除註冊回撥函式,並在回撥函式中展示運算結果
/** * 作者:葉應是葉 * 時間:2018/3/18 17:51 * 描述:https://github.com/leavesC * 客戶端 */ public class MainActivity extends AppCompatActivity { private static final String TAG = "MainActivity"; private EditText et_param1; private EditText et_param2; private EditText et_result; private IOperationManager iOperationManager; private IOnOperationCompletedListener completedListener = new IOnOperationCompletedListener.Stub() { @Override public void onOperationCompleted(Parameter result) throws RemoteException { et_result.setText("運算結果: " + result.getParam()); } }; private ServiceConnection serviceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { iOperationManager = IOperationManager.Stub.asInterface(service); } @Override public void onServiceDisconnected(ComponentName name) { iOperationManager = null; } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); initView(); bindService(); } private void bindService() { Intent intent = new Intent(); intent.setClassName("com.czy.aidl_server", "com.czy.aidl_server.AIDLService"); bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE); } private void initView() { et_param1 = findViewById(R.id.et_param1); et_param2 = findViewById(R.id.et_param2); et_result = findViewById(R.id.et_result); Button btn_registerListener = findViewById(R.id.btn_registerListener); Button btn_unregisterListener = findViewById(R.id.btn_unregisterListener); Button btn_operation = findViewById(R.id.btn_operation); View.OnClickListener clickListener = new View.OnClickListener() { @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_registerListener: { if (iOperationManager != null) { try { iOperationManager.registerListener(completedListener); } catch (RemoteException e) { e.printStackTrace(); } } break; } case R.id.btn_unregisterListener: { if (iOperationManager != null) { try { iOperationManager.unregisterListener(completedListener); } catch (RemoteException e) { e.printStackTrace(); } } break; } case R.id.btn_operation: { if (TextUtils.isEmpty(et_param1.getText()) || TextUtils.isEmpty(et_param2.getText())) { return; } int param1 = Integer.valueOf(et_param1.getText().toString()); int param2 = Integer.valueOf(et_param2.getText().toString()); Parameter parameter1 = new Parameter(param1); Parameter parameter2 = new Parameter(param2); if (iOperationManager != null) { try { iOperationManager.operation(parameter1, parameter2); } catch (RemoteException e) { e.printStackTrace(); } } break; } } } }; btn_registerListener.setOnClickListener(clickListener); btn_unregisterListener.setOnClickListener(clickListener); btn_operation.setOnClickListener(clickListener); } @Override protected void onDestroy() { super.onDestroy(); if (serviceConnection != null) { unbindService(serviceConnection); } } }
執行結果如下所示:

六、正確使用 AIDL 回撥介面
在上面的程式碼中我提供了一個按鈕用於解除回撥函式,但當點選按鈕時,Logcat 卻會打印出如下資訊

該回調沒有被註冊過?但在註冊回撥函式和解除回撥函式時,使用的都是同個物件啊!其實,這是因為回撥函式被序列化了的原因,Binder 會把客戶端傳過來的物件序列化後轉為一個新的物件傳給服務端,即使客戶端使用的一直是同個物件,但對服務端來說前後兩個回撥函式其實都是兩個完全不相關的物件,物件的跨程序傳輸本質上都是序列化與反序列化的過程
為了能夠無誤地註冊和解除註冊回撥函式,系統為開發者提供了 RemoteCallbackList
,RemoteCallbackList 是一個泛型類,系統專門提供用於刪除跨程序回撥函式,支援管理任意的 AIDL 介面,因為所有的 AIDL 介面都繼承自 IInterface
,而 RemoteCallbackList 對於泛型型別有限制
public class RemoteCallbackList<E extends IInterface>
RemoteCallbackList 在內部有一個 ArrayMap 用於 儲存所有的 AIDL 回撥介面
ArrayMap<IBinder, Callback> mCallbacks= new ArrayMap<IBinder, Callback>();
其中 Callback 封裝了真正的遠端回撥函式,因為即使回撥函式經過序列化和反序列化後會生成不同的物件,但這些物件的底層 Binder 物件是同一個。利用這個特徵就可以通過遍歷 RemoteCallbackList 的方式刪除註冊的回撥函數了
此外,當客戶端程序終止後,RemoteCallbackList 會自動移除客戶端所註冊的回撥介面。而且 RemoteCallbackList 內部自動實現了執行緒同步的功能,所以我們使用它來註冊和解註冊時,不需要進行執行緒同步
以下就來修改程式碼,改為用 RemoteCallbackList 來儲存 AIDL 介面
//宣告 private RemoteCallbackList<IOnOperationCompletedListener> callbackList;
註冊介面和解除註冊介面
@Override public void registerListener(IOnOperationCompletedListener listener) throws RemoteException { callbackList.register(listener); Log.e(TAG, "registerListener 註冊回撥成功"); } @Override public void unregisterListener(IOnOperationCompletedListener listener) throws RemoteException { callbackList.unregister(listener); Log.e(TAG, "unregisterListener 解除註冊回撥成功"); }
遍歷回撥介面
//在操作 RemoteCallbackList 前,必須先呼叫其 beginBroadcast 方法 //此外,beginBroadcast 必須和 finishBroadcast配套使用 int count = callbackList.beginBroadcast(); for (int i = 0; i < count; i++) { IOnOperationCompletedListener listener = callbackList.getBroadcastItem(i); if (listener != null) { listener.onOperationCompleted(result); } } callbackList.finishBroadcast();
按照上面的程式碼來修改後,客戶端就可以正確地解除所註冊的回撥函數了
還有一個地方需要強調下,是關於遠端方法呼叫時的執行緒問題。客戶端在呼叫遠端服務的方法時,被呼叫的方法是執行在服務端的 Binder 執行緒池中,同時客戶端執行緒會被掛起,這時如果服務端方法執行比較耗時,就會導致客戶端執行緒被堵塞。就如果上一節我為了模擬耗時計算,使執行緒休眠了五秒,當點選按鈕時就可以明顯看到按鈕有一種被“卡住了”的反饋效果,這就是因為 UI 執行緒被堵塞了,這可能會導致 ANR。所以如果確定遠端方法是耗時的,就要避免在 UI 執行緒中去呼叫遠端方法。
所以,客戶端呼叫遠端方法 operation
的操作可以放到子執行緒中進行
new Thread(new Runnable() { @Override public void run() { Parameter parameter1 = new Parameter(param1); Parameter parameter2 = new Parameter(param2); if (iOperationManager != null) { try { iOperationManager.operation(parameter1, parameter2); } catch (RemoteException e) { e.printStackTrace(); } } } }).start();
此外,客戶端的 ServiceConnection
物件的 onServiceConnected
和 onServiceDisconnected
都是執行在 UI 執行緒中,所以也不能用於呼叫耗時的遠端方法。而由於服務端的方法本身就執行在服務端的 Binder 執行緒池中,所以服務端方法本身就可以用於執行耗時方法,不必再在服務端方法中開執行緒去執行非同步任務
同理,當服務端需要呼叫客戶端的回撥介面中的方法時,被呼叫的方法也執行在客戶端的 Binder 執行緒池中,所以一樣不可以在服務端中呼叫客戶端的耗時方法
最後,我們還需要考慮一個問題,那就是安全問題。假設有人反編譯了服務端應用的程式碼,取得了 AIDL 介面,知道了應用的包名以及 Service 路徑名後,就可以直接通過 AIDL 直接呼叫服務端的遠端方法了,這當然不是應用開發者所希望面對的,因此服務端就需要對請求連線的客戶端進行許可權驗證了
Android 平臺下的許可權驗證機制我在以前的文章中有介紹過,這裡不再贅述,可以參考這兩篇文章的內容:
這裡提供本系列文章所有的 IPC 示例程式碼: IPCSamples