Android 程式通訊機制之 AIDL
什麼是 AIDL
AIDL 全稱 Android Interface Definition Language,即 安卓介面描述語言。聽起來很深奧,其實它的本質就是生成程式間通訊介面的輔助工具。它的存在形式是一種 .aidl
檔案,開發者需要做的就是在該檔案中定義程式間通訊的介面,編譯的時候 IDE 就會根據我們的 .aidl
介面檔案生成可供專案使用的 .java
檔案,這和我們說的“語法糖”有些類似。
AIDL 的語法就是 java 的語法,就是導包上有點細微差別。java 中如果兩個類在相同的包中,是不需要進行導包操作的,但是在 AIDL 中,則必須進行導包宣告。
AIDL 詳解
構想一個場景:我們有一個圖書管理系統,這個系統的通過 CS 模式來實現。具體的管理功能由服務端程式來實現,客戶端只需要呼叫相應的介面就可以。
那麼首先定義這個管理系統的 ADIL 介面。
我們在 /rc 新建 aidl
包,包中有三個檔案 Book.java 、Book.aidl、IBookManager.aidl 三個檔案。
package com.example.aidl book public class Book implements Parcelable { int bookId; String bookName; public Book(int bookId, String bookName) { this.bookId = bookId; this.bookName = bookName; } ... }
package com.example.aidl; Parcelable Book;
package com.example.aidl; import com.example.aidl.Book; inteface IBookManager { List<Book> getBookList(); void addBook(in Book book); }
下面對這三個檔案分別進行說明:
Book.java
是我們定義的實體類,它實現了Parcelable
介面,這樣Book
類才能在程式間傳輸。Book.aidl
是這個實體類在 AIDL 中的宣告。IBookManager
是服務端和客戶端通訊的介面。(注意,在 AIDL 介面中除基本型別外,引數前須加方向,in
表示輸入型引數,out
表示輸出型引數,inout
表示輸入輸出型引數)
編譯器編譯後,android studio 為我們的專案自動生成了一個 .java
檔案,這個檔案包含三個類,這三個類分別是 IBookManager
, Stub
和 Proxy
,這三個類都是靜態型別,我們完全可以把他們分開來,三個類定義如下:
IBookManager
public interface IBookManager extends android.os.IInterface { public void addBook(net.bingyan.library.Book book) throws android.os.RemoteException; public java.util.List<net.bingyan.library.Book> getBookList() throws android.os.RemoteException; }
Stub
public static abstract class Stub extends android.os.Binder implements net.bingyan.library.IBookManager { private static final java.lang.String DESCRIPTOR = "net.bingyan.library.IBookManager"; static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1); /** * Construct the stub at attach it to the interface. */ public Stub() { this.attachInterface(this, DESCRIPTOR); } /** * Cast an IBinder object into an net.bingyan.library.IBookManager interface, * generating a proxy if needed. */ public static net.bingyan.library.IBookManager asInterface(android.os.IBinder obj) { if ((obj == null)) { return null; } android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); if (((iin != null) && (iin instanceof net.bingyan.library.IBookManager))) { return ((net.bingyan.library.IBookManager) iin); } return new net.bingyan.library.IBookManager.Stub.Proxy(obj); } @Override public android.os.IBinder asBinder() { return this; } @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { switch (code) { case INTERFACE_TRANSACTION: { reply.writeString(DESCRIPTOR); return true; } case TRANSACTION_addBook: { data.enforceInterface(DESCRIPTOR); net.bingyan.library.Book _arg0; if ((0 != data.readInt())) { _arg0 = net.bingyan.library.Book.CREATOR.createFromParcel(data); } else { _arg0 = null; } this.addBook(_arg0); reply.writeNoException(); return true; } case TRANSACTION_getBookList: { data.enforceInterface(DESCRIPTOR); java.util.List<net.bingyan.library.Book> _result = this.getBookList(); reply.writeNoException(); reply.writeTypedList(_result); return true; } } return super.onTransact(code, data, reply, flags); } }
Proxy
private static class Proxy implements net.bingyan.library.IBookManager { private android.os.IBinder mRemote; Proxy(android.os.IBinder remote) { mRemote = remote; } @Override public android.os.IBinder asBinder() { return mRemote; } public java.lang.String getInterfaceDescriptor() { return DESCRIPTOR; } /** * Demonstrates some basic types that you can use as parameters * and return values in AIDL. */ @Override public void addBook(net.bingyan.library.Book book) throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); try { _data.writeInterfaceToken(DESCRIPTOR); if ((book != null)) { _data.writeInt(1); book.writeToParcel(_data, 0); } else { _data.writeInt(0); } mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0); _reply.readException(); } finally { _reply.recycle(); _data.recycle(); } } @Override public java.util.List<net.bingyan.library.Book> getBookList() throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); java.util.List<net.bingyan.library.Book> _result; try { _data.writeInterfaceToken(DESCRIPTOR); mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0); _reply.readException(); _result = _reply.createTypedArrayList(net.bingyan.library.Book.CREATOR); } finally { _reply.recycle(); _data.recycle(); } return _result; } }
對生成的這三個類的說明如下:
IBookManager
這個類是我們定義的介面,android studio 給它新增了一個父類,讓它繼承自 android.os.interface 這個介面,這個介面只有一個方法IBinder asBinder()
,這樣IBookManager
中就有三個帶實現的方法了,它是服務端程式和客戶端程式通訊的視窗。Stub
是個抽象類,這個類繼承自android.os.Binder
類,並且實現的了IBookManager
這個介面。在Stub
中,已經實現了asBinder()
這個介面方法,還有兩個是我們定義的 AIDL 介面方法留給繼承它的子類去實現。它用在服務端,因此服務端需要實現這兩個方法。Proxy
顧名思義是一個代理類,它是服務端在客戶端的一個代理,它也實現了IBookManager
介面,並且實現了IBookManager
中的所有方法。它用在客戶端,是服務端在客戶端的代理。
現在我們對這三個類逐個分析:
IBookManager
這個類沒什麼好說的,它只是簡單繼承了asInterface
這個介面,作用就是將IBookManager
轉換成IBinder
。Proxy
這個類上面已經提到過了,它就是程式間通訊機制的一個封裝類,他的內部實現機制就是Binder
,通過構造方法我們也容易看出來。它的構造方法接受一個IBinder
型別的引數,引數名為remote
,顯然,它代表著服務端。我們看看這個類中的方法addBook()
和getBookList()
:
@Override public void addBook(net.bingyan.library.Book book) throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); try { _data.writeInterfaceToken(DESCRIPTOR) if ((book != null)) { _data.writeInt(1); book.writeToParcel(_data, 0); } else { _data.writeInt(0); } mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0); _reply.readException(); } finally { _reply.recycle(); _data.recycle(); } }
@Override public java.util.List<net.bingyan.library.Book> getBookList() throws android.os.RemoteException { android.os.Parcel _data = android.os.Parcel.obtain(); android.os.Parcel _reply = android.os.Parcel.obtain(); java.util.List<net.bingyan.library.Book> _result; try { _data.writeInterfaceToken(DESCRIPTOR); mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0); _reply.readException(); _result = _reply.createTypedArrayList(net.bingyan.library.Book.CREATOR); } finally { _reply.recycle(); _data.recycle(); } return _result; }
它們是編譯器自動實現的,這兩個方法有很多類似之處,可以現在這裡透露下:這兩個方法就是客戶端程式呼叫服務端程式的視窗。在這兩個方法的開始,它們都定義了兩個 Parcel
(中文譯名:包裹)物件。Parcel
這個類我們看上去很眼熟,是的,Book
類中的 writeToParcel()
和 CREATOR
中的 createFromParcel()
的引數就是 Parcel
型別的,關於這個類文件中解釋如下:
Container for a message (data and object references) that can be sent through an IBinder. A Parcel can contain both flattened data that will be unflattened on the other side of the IPC (using the various methods here for writing specific types, or the general {@link Parcelable} interface), and references to live {@link IBinder} objects that will result in the other side receiving a proxy IBinder connected with the original IBinder in the Parcel.
翻譯一下:Proxy
是一個可以通過 IBinder
進行訊息傳遞的一個容器。一個 Parcel
可以包含可序列化的資料,這些資料會在 IPC
的另一端被反序列化;它也可以包含指向 IBinder
物件的引用,這會使得另一端接收到一個 IBinder
型別的代理物件,這個代理物件連線著 Parcel
中的原始 IBinder
物件。
下面用圖來直觀的說明:
如圖,我們可以很直觀的看到服務端以 Parcel
作為資料包裹依靠 Binder
和客戶端進行通訊。資料包裹就是序列化之後的物件。
如上所述,這兩個方法都定義了兩個 Parcel
物件,分別叫做 _data
和 _reply
,形象的來說,從客戶端的角度來看,_data
就是客戶端傳送給服務端的資料包裹,_reply
服務端傳送給客戶端的資料包裹。
之後便開始用這兩個物件來和服務端進行通訊了,我們能夠觀察到,兩個方法中都有這麼個方法呼叫 mRemote.transact()
,它有四個引數,第一個引數的意義我們後面再講,第二個引數 _data
負責向服務端傳送資料包裹比如介面方法的引數,第三個引數 _reply
負責從服務端接收資料包裹比如介面方法的返回值。這行程式碼只有一句簡單的方法呼叫,但是卻是 AIDL 通訊的最核心部分,它其實進行了一次遠端方法呼叫(客戶端通過本地代理 Proxy
暴露的介面方法呼叫服務端 Stub
同名方法),所以能想到它是一個耗時操作。
在我們的例子中:
void addBook(Book book)
需要藉助_data
向服務端傳送引數Book:book
,傳送的方式就是把Book
通過其實現的writeToParcel(Parcel out)
方法打包至_data
中,正如你能想到的,_data
其實就是引數out
,還記得Book
中的這個方法的實現嗎? 我們是將Book
的欄位一個個打包至Parcel
中的。List<Book> getBookList()
需要藉助_reply
從服務端接收返回值List<Book>:books
,方法中的做法是將Book
中的CREATOR
這個靜態欄位作為引數傳入_reply
的createTypedArrayList()
方法中,還記得Book
中的CREATOR
嗎?當時你是不是好奇這個靜態欄位應該怎麼用呢?現在一切明瞭了,我們需要靠這個物件(便於理解我們可以叫它”反序列化器“)來對服務端的資料反序列化從而重新生成可序列化的物件或者物件陣列。很明顯CREATOR
藉助_reply
生成了List<Book>:books
。
當然這兩個方法中的 _data
和 _reply
不僅傳遞了物件,還傳遞了一些校驗資訊,這個我們可以不必深究,但應注意的是,Parcel
打包順序和解包順序要嚴格對應。例如,第一個打包的是 int:i
,那麼第一解包的也應該是這個整型值。也即打包時第一次呼叫的如果是 Parcel.writeInt(int)
,解包時第一次呼叫的應該是 Parcel.readInt()
。
到此,客戶端的 Proxy
講解完了,下面我們看看服務端的 Stub。
Stub
中實現了IBookManager
的其中一個方法,這個很簡單,就是簡單的將自身返回,因為Stub
本身就繼承自Binder
,而Binder
繼承自IBinder
,所以沒有任何問題。你會問:還有兩個方法沒實現呢?這兩個方法就是我們定義的介面方法,它們留給服務端程式去實現,也就是說,到時候我們在服務端程式中需要定義一個Stub
的實現者。下面對Stub
中的兩個重要方法進行分析:
IBookManager asInterface(IBinder obj)
public static net.bingyan.library.IBookManager asInterface(android.os.IBinder obj) { if ((obj == null)) { return null; } android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); if (((iin != null) && (iin instanceof net.bingyan.library.IBookManager))) { return ((net.bingyan.library.IBookManager) iin); } return new net.bingyan.library.IBookManager.Stub.Proxy(obj); }
這個方法的作用是將 Stub
類轉換成 IBookManager
這個介面,方法中有個判斷:如果我們的服務端程式和客戶端程式是同一程式,那麼就直接將 Stub
類通過型別轉換轉成 IBookManager
;如果不是同一程式,那麼就通過代理類 Proxy
將 Stub
轉換成 IBookManager
。為什麼這麼做,我們知道如果服務端程式和客戶端程式不是同一程式,那麼它們的記憶體就不能共享,就不能通過一般的方式進行通訊,但是我們如果自己去實現程式間通訊方式,對於普通開發者來說成本太大,因此編譯器幫我們生成了一個封裝了了程式間通訊的工具,也就是這個 Proxy
,這個類對底層的程式通訊機制進行了封裝只同時暴露出介面方法,客戶端只需要呼叫這兩個方法實現程式間通訊(其實就是方法的遠端呼叫)而不需要了解其中的細節。
有了這個方法,我們在客戶端可以藉助其將一個 IBinder
型別的變數轉換成我們定義的介面 IBookManager
,它的使用場景我們會在後面的例項中進行講解。
onTransact(int code, Parcel data, Parcel reply, int flags)
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException { switch (code) { case INTERFACE_TRANSACTION: { reply.writeString(DESCRIPTOR); return true; } case TRANSACTION_addBook: { data.enforceInterface(DESCRIPTOR); net.bingyan.library.Book _arg0; if ((0 != data.readInt())) { _arg0 = net.bingyan.library.Book.CREATOR.createFromParcel(data); } else { _arg0 = null; } this.addBook(_arg0); reply.writeNoException(); return true; } case TRANSACTION_getBookList: { data.enforceInterface(DESCRIPTOR); java.util.List<net.bingyan.library.Book> _result = this.getBookList(); reply.writeNoException(); reply.writeTypedList(_result); return true; } } return super.onTransact(code, data, reply, flags); }
這個方法我們是不是也很熟悉呢?我們在 Proxy
中也看到一個類似得方法 transact(int, Parcel, Parcel, int)
,它們的引數一樣,而且它們都是 Binder
中的方法,那麼它們有什麼聯絡呢?
前面說了,transact()
執行了一個遠端呼叫,如果說 transact()
是遠端呼叫的發起,那麼 onTransact()
就是遠端呼叫的響應。真實過程是客戶端發器遠端方法呼叫,android 系統通過底層程式碼對這個呼叫進行響應和處理,之後回撥服務端的 onTransact()
方法,從資料包裹中取出方法引數,交給服務端實現的同名方法呼叫,最後將返回值打包返回給客戶端。
需要注意的是 onTransact()
是在服務端程式的 Binder
執行緒池中進行的,這就意味著如果我們的要在 onTransact()
方法的中更新 UI,就必須藉助 Handler
。
這兩個方法的第一個引數的含義是 AIDL 介面方法的標識碼,在 Stub
中,定義了兩個常量作為這兩個方法的標示:
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0); static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
如果 code == TRANSACTION_addBook
,那麼說明客戶端呼叫的是 addBook()
;如果 code == TRANSACTION_getBookList
,那麼客戶端呼叫的是 getBookList()
,然後交由相應的服務端方法處理。 用一張圖來表示整個通訊過程:
瞭解了 AIDL 的整個過程,接下來就是 AIDL 在安卓程式中的應用了。
AIDL 的使用
相信大家應該都和清楚 Service
的使用了吧,Service
雖然稱作“服務”,並且執行於後臺,但是它們預設還是執行在預設程式的主執行緒中。其實讓 Service
執行在預設程式中,有點大材小用了。android 的很多系統服務都執行於單獨的程式中,供其他應用呼叫,比如視窗管理服務。這樣做的好處是可以多個應用共享同一個服務,節約了資源,也便於集中管理各個客戶端,要注意問題的就是執行緒安全問題。
那麼接下來我們就用 AIDL 實現一個簡單的 CS 架構的圖書管理系統。
首先我們定義服務端:
BookManagerService
public class BookManagerService extends Service { private final List<Book> mLibrary = new ArrayList<>(); private IBookManager mBookManager = new IBookManager.Stub() { @Override public void addBook(Book book) throws RemoteException { synchronized (mLibrary) { mLibrary.add(book); Log.d("BookManagerService", "now our library has " + mLibrary.size() + " books"); } } @Override public List<Book> getBookList() throws RemoteException { return mLibrary; } }; @Override public IBinder onBind(Intent intent) { return mBookManager.asBinder(); } }
<service android:process=":remote" android:name=".BookManagerService"/>
服務端我們定義了 BookManagerService
這個類,在它裡面我們建立了服務端的 Stub
物件,並且實現了需要實現的兩個 AIDL 介面方法來定義服務端的圖書管理策略。在 onBind()
方法中我們將 IBookManager
物件作為 IBinder
返回。我們知道,當我們繫結一個服務時,系統會呼叫 onBinder()
方法得到服務端的 IBinder
物件,並將其轉換成客戶端的 IBinder
物件傳給客戶端,雖然服務端的 IBinder
和 客戶端的 IBinder
是兩個 IBinder
物件,但他們在底層都是同一個物件。我們在 xml 中註冊 Service
時給它指定了程式名,這樣 Service
就能執行在單獨的程式中了。
接下來看看客戶端的實現:
Client
public class Client extends AppCompatActivity { private TextView textView; private IBookManager bookManager; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.library_book_manager_system_client); Intent i = new Intent(Client.this, BookManagerService.class); bindService(i, conn, BIND_AUTO_CREATE); Button addABook = (Button) findViewById(R.id.button); addABook.setOnClickListener(v -> { if (bookManager == null) return; try { bookManager.addBook(new Book(0, "book")); textView.setText(getString(R.string.book_management_system_book_count, String.valueOf(bookManager.getBookList().size()))); } catch (RemoteException e) { e.printStackTrace(); } }); textView = (TextView) findViewById(R.id.textView); } private ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { Log.d("Client -->", service.toString()); bookManager = IBookManager.Stub.asInterface(service); } @Override public void onServiceDisconnected(ComponentName name) { Log.d("Client", name.toString()); } }; }
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:weightSum="1" android:gravity="center"> <Button android:text="add a book" android:layout_width="111dp" android:layout_height="wrap_content" android:id="@+id/button" /> <TextView android:layout_marginTop="10dp" android:text="@string/book_management_system_book_count" android:layout_width="231dp" android:gravity="center" android:layout_height="wrap_content" android:id="@+id/textView" /> </LinearLayout>
我們的客戶端就是一個 Activity
,onCreate()
中進行了服務的繫結,bindService()
方法中有一引數 ServiceConnection:conn
,因為繫結服務是非同步進行的,這個引數的作用就是繫結服務成功後回撥的介面,它有兩個回撥方法:一個是連線服務成功後回撥,另一個在與服務端斷開連線後回撥。我們現在關心的主要是 onServiceConnected()
方法,在這裡我們只做了一件事:將服務端轉換過來的 IBinder
物件轉換成 AIDL 介面,我們定義 IBookManager:bookManager
欄位來保持對其的引用。這樣的話,我們就可以通過這個 bookManager
來進行方法的遠端呼叫。我們給客戶端的 Button
註冊事件:每一次點選都會向服務端增加一本書,並且將圖書館現有的圖書數量顯示出來。
現在我們看看程式的執行效果:
相關文章
- Android程式間通訊之AIDLAndroidAI
- 從AIDL開始談Android程式間Binder通訊機制AIAndroid
- Android跨程式通訊之非AIDL(二)AndroidAI
- Android探索之AIDL實現程式間通訊AndroidAI
- android-IPC/Binder/D-BUS(Binder/Messager/AIDL)程式間通訊(訊息機制)AndroidAI
- Android程式間通訊,AIDL工作原理AndroidAI
- Android 程式間通訊 AIDL詳解AndroidAI
- 從AIDL看Android跨程式通訊AIAndroid
- Binder機制之AIDLAI
- Android系統之Binder通訊機制Android
- Android IPC程式間通訊之AIDL和Messenger的使用AndroidC程式AIMessenger
- Android程式間通訊–訊息機制及IPC機制實現薦Android
- Android下AIDL機制詳解AndroidAI
- Android 之訊息機制Android
- Linux程式通訊機制Linux
- android之 Android訊息機制Android
- [Android]你不知道的Android程式化(4)--程式通訊AIDL框架AndroidAI框架
- Android AIDL SERVICE 雙向通訊 詳解AndroidAI
- Android中AIDL實現程式通訊(附原始碼下載)AndroidAI原始碼
- Android Studio 建立aidl檔案,用於程式間通訊AndroidAI
- Android 程式之間通訊Android
- Android的IPC機制(一)——AIDL的使用AndroidAI
- 【Android原始碼】Binder機制和AIDL分析Android原始碼AI
- 4-AIII–Service跨程式通訊:aidlAI
- Aidl程式間通訊詳細介紹AI
- 使用AIDL實現程式間的通訊AI
- 快速入門android AIDL(開啟多程式並進行通訊)AndroidAI
- 藉助 AIDL 理解 Android Binder 機制——AIDL 的使用和原理分析AIAndroid
- Binder通訊機制
- Spark原始碼分析之BlockManager通訊機制Spark原始碼BloC
- Android訊息傳遞之Handler訊息機制Android
- Binder通訊機制與IPC通訊.md
- 深入探索Android訊息機制之HandlerAndroid
- android AIDL程式間通訊(只介紹了簡單資料型別)AndroidAI資料型別
- Android 系統原始碼-2:Binder 通訊機制Android原始碼
- 使用AIDL實現程式間的通訊之複雜型別傳遞AI型別
- Android 之 Binder與程式間通訊Android
- React Native原理之跨端通訊機制React Native跨端