Android Interface Definition Language (AIDL)
英文原文:http://developer.android.com/guide/components/aidl.html
采集日期:2014-12-31
另一位兄弟的早期博文(不准確,供參考):http://www.cnblogs.com/over140/archive/2011/03/08/1976890.html
Android 接口定義語言 AIDL(Android Interface Definition Language)與其他已有的 IDL 很類似。 客戶端和服務端可以通過由它定義的編程接口來達成共識,以便通過進程間通訊(IPC)完成相互通訊。 在 Android 系統中,通常一個進程不允許直接訪問另一個進程的內存。 因此為了能夠實現對話,進程需要把對象分解為操作系統可以識別的原生數據,在跨越進程邊界后再組裝起來。 實現組裝的代碼非常枯燥無趣,因此 Android 通過 AIDL 可有助於完成這一過程。
注意: 僅當允許其他應用程序通過 IPC 方式訪問服務,並且服務需要多線程運行時,才必須用到 AIDL。 如果不需要進行跨越多個應用的並發 IPC,就應該用 實現綁定 實現接口。或者要進行 IPC 但不需要多線程運行,則可 使用 Messenger 來實現接口。 無論如何,在實現 AIDL 之前,請確認已經理解了 Bound 服務 中的內容。
在開始設計 AIDL 接口之前,請務必注意,對 AIDL 接口的調用是直接函數調用(direct function call)。 發起調用的線程根本無法預料。 根據發起調用的線程是在本地進程還是在遠程進程,處理方式也將不同。 具體來說:
- 如果從本地進程發起,則調用將在發起線程中運行。 如果是主 UI 線程發起的,則其將繼續運行 AIDL 接口。 如果是其他線程發起的,則為運行服務代碼的線程。 因此,如果只有本地線程需要訪問服務,則可以完全控制在哪個線程運行(但如果是這樣,就根本不應該使用 AIDL 了,而應該通過 實現 Binder 來創建接口)。
- 從遠程進程發起的調用,將會由系統維護的本地進程內部的線程池分發。 必須准備好迎接來自未知線程的調用,並且還可能同時收到多個調用。 換句話說, AIDL接口的實現必須完全是線程安全的。
- 關鍵字
oneway會改變遠程調用的處理方式。 使用該關鍵字時,遠程調用將不會阻塞,在發送完交易數據它就會立即返回。 接口的實現代碼最終將這種遠程調用視同由Binder線程池發起的調用一樣接收。 如果在本地調用中使用了oneway,則不起作用,調用仍舊是同步執行的。
定義 AIDL 接口
只能在.aidl文件中使用 Java 語法定義 AIDL 接口, 並且在服務所在應用和其他需要綁定服務的應用中,均保存為源代碼文件(在src/目錄下)。
在編譯包含.aidl文件的應用時, Android SDK 工具會根據 .aidl文件生成一個 IBinder 接口,並將它保存在項目的gen/目錄下。 服務端必須根據需要實現 IBinder。 然后客戶端應用就能與服務綁定,並利用 IBinder 調用方法實現 IPC 。
要利用 AIDL 創建一個可被綁定的服務,請按照以下步驟進行:
- 創建 .aidl 文件
該文件定義了包含方法聲明(Method Signature)的編程接口
- 實現接口
Android SDK 工具包會根據
.aidl文件生成 Java 語言格式的接口。 這個接口有一個名為Stub的內部抽象類,它擴展自Binder,並用於實現 AIDL 接口所要求的方法。 必須擴展這個Stub類並實現這些方法。 - 向客戶端公布接口
警告: 為了避免其他應用程序與服務的使用中斷,在應用程序發布之后,對 AIDL 接口 所做的所有修改都必須保證與之前的定義兼容。 也就是說,因為.aidl必須復制給其他應用程序以便其訪問服務接口,所以必須維持對以前接口的支持。
1. 創建 .aidl 文件
AIDL 的語法很簡單,聲明一個帶有若干方法的接口即可,這些方法都可帶有參數和返回值。 參數和返回值可以為任意類型,甚至可以是另一個由 AIDL 生成的接口。
.aidl 文件必須用 Java 語言編寫。 每個.aidl文件中必須也只能定義一個接口,且只能包含接口的定義和方法聲明。
默認情況下, AIDL 支持以下數據類型:
- Java 語言的所有簡單類型(如
int、long、char、boolean等等) StringCharSequenceList該
List中的所有數據只能是這里列出的類型、其他某個基於 AIDL 生成的接口、已聲明的自定義 Parcelable 類。List還可被用作“泛型”(generic)類(如List<String>)。 雖然方法是通過List接口生成的,但是實際收到的實體類其實會是一個ArrayList。Map該
Map中的所有數據只能是這里列出的類型、其他某個基於 AIDL 生成的接口、已聲明的自定義 Parcelable 類。 這里不支持 Map 泛型(比如Map<String,Integer>的形式)。 雖然方法是通過Map接口生成的,但實際收到的實體類其實會是一個HashMap。
對於未列入上述列表的類型,即便是定義於接口所在的包中,也必須包含 import 語句。
在定義服務接口時,請注意:
- 方法可以有多個參數,也可以不帶參數;可有一個返回值,或無返回值(void)。
- 所有非簡單類型的參數都需要帶有一個指明數據方向的標志。 可以是
in、out或inout(參閱下述例子)。簡單類型的參數默認是
in,且不能是其他方向值。注意: 請按照實際需求來限定參數的方向,因為參數的組裝過程開銷很大。
-
.aidl文件中的代碼注釋都會一並放入最終生成的IBinder接口中( import 和 package 語句之前的注釋除外)。 - AIDL 中只支持方法,不能聲明靜態成員。
下面是一個 .aidl 文件示例:
1 // IRemoteService.aidl 2 package com.example.android; 3 // 在這里用 import 語句對所有非默認類型進行聲明 4 5 /** 服務接口示例 */ 6 interface IRemoteService { 7 /** 獲取服務的進程 ID 並完成一些任務(do evil things)*/ 8 int getPid(); 9 10 /** 這里給出一些可供 AIDL 的參數和返回值使用的基本類型。*/ 11 void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, 12 double aDouble, String aString); 13 }
要把 .aidl 文件保存到項目的 src/ 目錄下, 在編譯應用程序時, SDK 工具就會在項目的 gen/ 目錄中生成 IBinder 接口文件。文件名稱與 .aidl 的相對應,只是后綴名變成了 .java (比如 IRemoteService.aidl 會生成 IRemoteService.java)。
如果使用 Eclipse 開發,那么增量編譯器(Incremental Build)幾乎是即時生成 Binder 類。 如果不是使用 Eclipse 開發的,則 Ant 工具會在下次編譯時生成 Binder 類—— 請在 .aidl 文件的編寫完成后及時用 ant debug(或 ant release )編譯項目, 以便其他代碼可以鏈接編譯完畢的類。
2. 實現接口
在編譯應用程序時, Android SDK 工具會以 .aidl 文件名生成一個 .java 接口文件。 生成完畢的接口包含一個名為 Stub 的子類,它是其父接口類的抽象實現(例如 YourInterface.Stub ), 它聲明了 .aidl 文件定義的所有方法。
注意: Stub 還定義了一些助手方法,最值得一題的是 asInterface(), 它的參數是 IBinder (通常就是傳給客戶端 onServiceConnected() 方法的那個),返回值是一個 stub 接口的實例。 關於這一轉換的細節,請參閱調用 IPC 方法一節。
要實現由 .aidl 文件生成的接口,請擴展已生成的 Binder 接口(比如YourInterface.Stub),並實現由 .aidl 文件繼承而來的方法。
下面給出了一個名為 IRemoteService 的代碼示例,它由上面的示例 IRemoteService.aidl 所定義。 這里通過一個匿名實例來實現:
1 private final IRemoteService.Stub mBinder = new IRemoteService.Stub() { 2 public int getPid(){ 3 return Process.myPid(); 4 } 5 public void basicTypes(int anInt, long aLong, boolean aBoolean, 6 float aFloat, double aDouble, String aString) { 7 // 不做任何處理 8 } 9 };
這里的 mBinder 是一個 Stub 類的實例( Binder ),用於定義 RPC 服務接口。 下一步,將向客戶端公布該實例,以便它們與服務進行交互。
實現 AIDL 接口時,請注意以下幾點:
- 調用不一定是在主線程中運行,因此從一開始就要考慮多線程運行,並保證服務是按照線程安全的模式編寫的。
- 默認情況下, RPC 調用是以同步方式運行的。 如果事先知道服務處理完一次請求需要若干毫秒的時間,就不應該在 Activity 的主線程中發起調用。 因為這可能會導致應用程序掛起( Android 可能會顯示“應用程序停止響應”的對話框)。 ——通常,這時應該在客戶端的單獨線程中發起調用。
- 所有異常都不會(Exception)發還給調用者。
3. 向客戶端公布接口
在實現了服務的接口之后,就需要將它公布給客戶端以供綁定。 要為服務發布接口,請擴展 Service 類並實現 onBind() 方法,以便返回自定義 Stub 類的實例(如上所述)。 下面是將接口 IRemoteService 公布給客戶端的示例。
1 public class RemoteService extends Service { 2 @Override 3 public void onCreate() { 4 super.onCreate(); 5 } 6 7 @Override 8 public IBinder onBind(Intent intent) { 9 // 返回接口 10 return mBinder; 11 } 12 13 private final IRemoteService.Stub mBinder = new IRemoteService.Stub() { 14 public int getPid(){ 15 return Process.myPid(); 16 } 17 public void basicTypes(int anInt, long aLong, boolean aBoolean, 18 float aFloat, double aDouble, String aString) { 19 // 不做處理 20 } 21 }; 22 }
現在,如果客戶端(比如某個 Activity)調用了 bindService() 連接到該服務,客戶端的 onServiceConnected() 方法將會收到服務的 onBind() 方法返回的 mBinder 實例。
客戶端還必須有權限訪問接口類。 因此,假如客戶端和服務端位於不同的應用,客戶端的應用必須在其 src/ 目錄下擁有一份 .aidl 文件的拷貝。 用於生成供客戶端訪問 AIDL 方法的 android.os.Binder 接口,
在接收到回調方法 onServiceConnected() 中的 IBinder 時,客戶端必須調用 YourServiceInterface.Stub.asInterface(service) 來把返回的參數轉換為 YourServiceInterface 類型。 例如:
1 IRemoteService mIRemoteService; 2 private ServiceConnection mConnection = new ServiceConnection() { 3 // 當已與服務連接時調用 4 public void onServiceConnected(ComponentName className, IBinder service) { 5 // 根據上述 AIDL 接口的示例,獲取 IRemoteInterface 的一個實例, 6 // 以便后續的調用 7 mIRemoteService = IRemoteService.Stub.asInterface(service); 8 } 9 10 // 當與服務的連接被意外斷開時調用 11 public void onServiceDisconnected(ComponentName className) { 12 Log.e(TAG, "Service has unexpectedly disconnected"); 13 mIRemoteService = null; 14 } 15 };
更多示例代碼,請參閱 ApiDemos 中的 RemoteService.java 類。
通過 IPC 傳遞對象
如果要跨進程傳遞某個類,可以通過 IPC 接口來實現。 不過,請務必確保在 IPC 通道的對端可以識別該類的代碼,該類必須支持 Parcelable 接口。支持 Parcelable 接口非常重要,因為這使得 Android 系統可將對象分解為能夠跨進程組裝的原生數據。
可按以下步驟創建支持 Parcelable 協議的類:
- 必須實現
Parcelable接口。 - 實現
writeToParcel方法,參數為當前對象的狀態,並寫入一個Parcel中。 - 在類中添加一個名為
CREATOR的靜態成員變量,即為一個實現了Parcelable.Creator接口的對象。 - 最后,創建
.aidl文件,聲明該 Parcelable 類(如下述Rect.aidl文件所示)。如果采用自定義編譯方式,請不要把
.aidl文件加入編譯項目。 與 C 語言的頭文件類似,.aidl文件不會被編譯。
AIDL 利用上述方法和成員變量來分解和組裝對象。
以下是創建 Rect 類的 Rect.aidl 文件示例,該類支持打包操作(Parcelable):
1 package android.graphics; 2 3 // 聲明 Rect,以便 AIDL 識別並知道它已實現了 Parcelable 協議。 4 parcelable Rect;
下面是 Rect 類如何實現 Parcelable 協議的代碼示例:
1 import android.os.Parcel; 2 import android.os.Parcelable; 3 4 public final class Rect implements Parcelable { 5 public int left; 6 public int top; 7 public int right; 8 public int bottom; 9 10 public static final Parcelable.Creator<Rect> CREATOR = new 11 Parcelable.Creator<Rect>() { 12 public Rect createFromParcel(Parcel in) { 13 return new Rect(in); 14 } 15 16 public Rect[] newArray(int size) { 17 return new Rect[size]; 18 } 19 }; 20 21 public Rect() { 22 } 23 24 private Rect(Parcel in) { 25 readFromParcel(in); 26 } 27 28 public void writeToParcel(Parcel out) { 29 out.writeInt(left); 30 out.writeInt(top); 31 out.writeInt(right); 32 out.writeInt(bottom); 33 } 34 35 public void readFromParcel(Parcel in) { 36 left = in.readInt(); 37 top = in.readInt(); 38 right = in.readInt(); 39 bottom = in.readInt(); 40 } 41 }
Rect 類的分解過程非常簡單。 請查看一下 Parcel 的其他方法,即可了解還有哪些可寫入 Parcel 的數據類型。
警告: 請不要忘記從其他進程讀取數據所導致的安全風險。 比如這里的 Rect 從 Parcel 中讀取了4個數字,但必須考慮是否要限制調用者只能讀到這些值。 關於如何防止惡意代碼對應用程序的破壞,詳情請參閱 安全與權限
調用 IPC 方法
要調用 AIDL 定義的遠程接口,必須按照以下步驟進行:
- 在項目的
src/目錄下包含.aidl文件。 - 聲明
IBinder接口的實例(基於 AIDL 生成的) - 實現
ServiceConnection。 - 調用
Context.bindService(),傳入上面已實現的ServiceConnection。 - 在
onServiceConnected()代碼中,接收到IBinder實例(供調用的service)。 調用YourInterfaceName.Stub.asInterface((IBinder)service)將返回的參數轉換為 YourInterface 類型。 - 調用自定義接口中的方法。請確保捕獲
DeadObjectException異常,當連接中斷時會拋出該異常,而且是遠程方法唯一可能會拋出的異常。 - 調用自定義接口實例的
Context.unbindService()方法斷開連接。
調用 IPC 服務需要注意:
- 對象引用是跨進程計數的。
- 可以將匿名對象作為方法的參數發送。
關於綁定服務的更多信息,請參閱文檔 Bound 類型的服務
下面給出了部分示例代碼,演示了如何調用由 AIDL 創建的服務,摘自 ApiDemos 項目的 Remote Service 例程。
1 public static class Binding extends Activity { 2 /** 調用服務的主接口 */ 3 IRemoteService mService = null; 4 /** 使用服務的另一個接口 */ 5 ISecondary mSecondaryService = null; 6 7 Button mKillButton; 8 TextView mCallbackText; 9 10 private boolean mIsBound; 11 12 /** 13 * Activity 的標准初始化方法。Standard initialization of this activity. Set up the UI, then wait 14 * 建立界面,等待用戶點擊后再執行動作。 15 */ 16 @Override 17 protected void onCreate(Bundle savedInstanceState) { 18 super.onCreate(savedInstanceState); 19 20 setContentView(R.layout.remote_service_binding); 21 22 // 監視按鈕的點擊事件 23 Button button = (Button)findViewById(R.id.bind); 24 button.setOnClickListener(mBindListener); 25 button = (Button)findViewById(R.id.unbind); 26 button.setOnClickListener(mUnbindListener); 27 mKillButton = (Button)findViewById(R.id.kill); 28 mKillButton.setOnClickListener(mKillListener); 29 mKillButton.setEnabled(false); 30 31 mCallbackText = (TextView)findViewById(R.id.callback); 32 mCallbackText.setText("Not attached."); 33 } 34 35 /** 36 * 與服務的主接口進行交互的類 37 */ 38 private ServiceConnection mConnection = new ServiceConnection() { 39 public void onServiceConnected(ComponentName className, 40 IBinder service) { 41 // 當與服務建立連接后將會被調用,獲取可與之交互的服務對象。 42 // 與服務的通訊是通過 IDL 接口來完成的,因此需要在客戶端獲得一個代表原始服務的對象。 43 mService = IRemoteService.Stub.asInterface(service); 44 mKillButton.setEnabled(true); 45 mCallbackText.setText("Attached."); 46 47 // 在保持連接時需要一直對服務進行監視 48 try { 49 mService.registerCallback(mCallback); 50 } catch (RemoteException e) { 51 // 此時服務已崩潰,也就無法使用了, 52 // 很快連接將會斷開(如果服務可被重啟則會重新建立連接), 53 // 所以,這里不需要進行任何處理。 54 } 55 56 // 作為示例,下面把當前狀態通知用戶。 57 Toast.makeText(Binding.this, R.string.remote_service_connected, 58 Toast.LENGTH_SHORT).show(); 59 } 60 61 public void onServiceDisconnected(ComponentName className) { 62 // 當與服務的連接意外終止時將會被調用, 63 // 也就是說,服務的進程崩潰了。 64 mService = null; 65 mKillButton.setEnabled(false); 66 mCallbackText.setText("Disconnected."); 67 68 // 作為示例,下面把當前狀態通知用戶。 69 Toast.makeText(Binding.this, R.string.remote_service_disconnected, 70 Toast.LENGTH_SHORT).show(); 71 } 72 }; 73 74 /** 75 * 與服務的第二個接口進行交互的類 76 */ 77 private ServiceConnection mSecondaryConnection = new ServiceConnection() { 78 public void onServiceConnected(ComponentName className, 79 IBinder service) { 80 // 與第二個接口的連接方法類似於其他接口 81 mSecondaryService = ISecondary.Stub.asInterface(service); 82 mKillButton.setEnabled(true); 83 } 84 85 public void onServiceDisconnected(ComponentName className) { 86 mSecondaryService = null; 87 mKillButton.setEnabled(false); 88 } 89 }; 90 91 private OnClickListener mBindListener = new OnClickListener() { 92 public void onClick(View v) { 93 // 通過與接口名稱綁定,與服務建立起兩個連接。 94 // 這樣,如果某個應用程序實現了相同接口,就可以在安裝后替換掉相應的遠程服務。 95 bindService(new Intent(IRemoteService.class.getName()), 96 mConnection, Context.BIND_AUTO_CREATE); 97 bindService(new Intent(ISecondary.class.getName()), 98 mSecondaryConnection, Context.BIND_AUTO_CREATE); 99 mIsBound = true; 100 mCallbackText.setText("Binding."); 101 } 102 }; 103 104 private OnClickListener mUnbindListener = new OnClickListener() { 105 public void onClick(View v) { 106 if (mIsBound) { 107 // 如果已接收到服務並進行了注冊, 108 // 就在這里進行注銷。 109 if (mService != null) { 110 try { 111 mService.unregisterCallback(mCallback); 112 } catch (RemoteException e) { 113 // 如果服務已經崩潰了,就不需要再做什么處理了。 114 } 115 } 116 117 // 斷開已有連接 118 unbindService(mConnection); 119 unbindService(mSecondaryConnection); 120 mKillButton.setEnabled(false); 121 mIsBound = false; 122 mCallbackText.setText("Unbinding."); 123 } 124 } 125 }; 126 127 private OnClickListener mKillListener = new OnClickListener() { 128 public void onClick(View v) { 129 // 為了殺死服務所在的進程,需要知道其 PID。 130 // 好在該服務中現成就有一個返回 PID 的方法了。 131 if (mSecondaryService != null) { 132 try { 133 int pid = mSecondaryService.getPid(); 134 // 請記住,雖然用下面的 API 可以請求殺死指定 PID 的進程, 135 // 但系統核心仍然會按照標准的權限標准來核查指定的 PID 是否可被殺死。 136 // 通常,只有運行本應用的進程及應用創建的進程才能被殺死, 137 // UID 相同的多個包也可以互相殺死各自的進程。 138 Process.killProcess(pid); 139 mCallbackText.setText("Killed service process."); 140 } catch (RemoteException ex) { 141 // 體面地處理服務進程已被殺死的情況 142 // 作為示例,這里彈出一個通知。 143 Toast.makeText(Binding.this, 144 R.string.remote_call_failed, 145 Toast.LENGTH_SHORT).show(); 146 } 147 } 148 } 149 }; 150 151 // ---------------------------------------------------------------------- 152 // 處理回調方法的代碼 153 // ---------------------------------------------------------------------- 154 155 /** 156 * 接收遠程服務的回調方法 157 */ 158 private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() { 159 /** 160 * 本方法將由遠程服務定期調用,改變值。 161 * 請注意, IPC 調用是通過每一個進程內部的線程池發起的,所以這里的代碼將不會運行在主線程中。 162 * 因此,如果要更新 UI,需要使用 Handler 跳出去運行。 163 */ 164 public void valueChanged(int value) { 165 mHandler.sendMessage(mHandler.obtainMessage(BUMP_MSG, value, 0)); 166 } 167 }; 168 169 private static final int BUMP_MSG = 1; 170 171 private Handler mHandler = new Handler() { 172 @Override public void handleMessage(Message msg) { 173 switch (msg.what) { 174 case BUMP_MSG: 175 mCallbackText.setText("Received from service: " + msg.arg1); 176 break; 177 default: 178 super.handleMessage(msg); 179 } 180 } 181 182 }; 183 }
