什麼是 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 是我們定義的實體類,它實現了 這樣這樣類別才能在進程間傳輸。
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. http://www.manongjc.com/article/1501.html */ 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. http://www.manongjc.com/article/1500.html */ @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; } }
對產生的這三個類別的說明如下:
IBook 這個類別是我們定義的介面,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 /* http://www.manongjc.com/article/1547.html */ 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 vari modor species the specor sspeced 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.
是翻譯一下可以透過翻譯一下進行訊息傳遞的一個容器。一個 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 其實就是參數 out,還記得 Book 中的這個方法的實作嗎? 我們是將 Book 的字段一個個打包至 Parcel 中的。
List
當然這兩個方法中的 _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; /* http://www.manongjc.com/article/1499.html */ } 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 /* http://www.manongjc.com/article/1496.html */ public IBinder onBind(Intent intent) { return mBookManager.asBinder(); } }
服务端我们定义了 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="http://www.manongjc.com/article/1495.html" 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 注册事件:每一次点击都会向服务端增加一本书,并且将图书馆现有的图书数量显示出来。
现在我们看看程序的运行效果:
每当我们点击按钮,我们就成功的向服务端添加了一本书,说明我们通过 AIDL 跨进程通信成功了。