Was ist AIDL?
AIDL steht für Android Interface Definition Language, also die Beschreibungssprache der Android-Schnittstelle. Es klingt sehr tiefgreifend, aber im Wesentlichen ist es ein Hilfswerkzeug zum Generieren von Kommunikationsschnittstellen zwischen Prozessen. Es liegt in Form einer .aidl-Datei vor. Der Entwickler muss lediglich die Schnittstelle für die prozessübergreifende Kommunikation in der Datei definieren. Beim Kompilieren generiert die IDE eine .java-Datei, die vom Projekt verwendet werden kann auf unserer .aidl-Schnittstellendatei, die etwas dem ähnelt, was wir „syntaktischen Zucker“ nennen.
Die Syntax von AIDL entspricht der Syntax von Java, es gibt jedoch einige subtile Unterschiede beim Paketimport. Wenn sich in Java zwei Klassen im selben Paket befinden, muss das Paket nicht importiert werden, in AIDL muss jedoch die Paketimportdeklaration vorgenommen werden.
Detaillierte Erläuterung von AIDL
Stellen Sie sich ein Szenario vor: Wir haben ein Bibliotheksverwaltungssystem, das über den CS-Modus implementiert wird. Spezifische Verwaltungsfunktionen werden vom Serverprozess implementiert, und der Client muss nur die entsprechende Schnittstelle aufrufen.
Definieren Sie dann zunächst die ADIL-Schnittstelle dieses Managementsystems.
Wir erstellen ein neues Aidl-Paket in /rc. Das Paket enthält drei Dateien: Book.java, Book.aidl und 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; } ... }
Paket com.example.aidl;
Parcelable Book;
package com.example.aidl; import com.example.aidl.Book; inteface IBookManager { List<Book> getBookList(); void addBook(in Book book); }
Diese drei Dateien werden unten beschrieben:
Book.java ist die von uns definierte Entitätsklasse, die die Parcelable-Schnittstelle implementiert, sodass die Book-Klasse zwischen Prozessen übertragen werden kann.
Book.aidl ist die Deklaration dieser Entitätsklasse in AIDL.
IBookManager ist die Schnittstelle für die Kommunikation zwischen dem Server und dem Client. (Beachten Sie, dass in der AIDL-Schnittstelle zusätzlich zu den Grundtypen die Richtung vor den Parametern hinzugefügt werden muss, in stellt Eingabeparameter dar, out stellt Ausgabeparameter dar und inout stellt Eingabe- und Ausgabeparameter dar.)
Nach dem Compiler Kompiliert, Android Studio Eine .java-Datei wurde automatisch für unser Projekt generiert. Diese drei Klassen sind alle statische Typen wie folgt:
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; } }
Die Beschreibung der drei generierten Klassen lautet wie folgt:
IBookManager Diese Klasse ist die von uns definierte Schnittstelle. Android Studio fügt ihr eine übergeordnete Klasse hinzu und lässt sie von der android.os.interface-Schnittstelle erben. Diese Schnittstelle hat nur eine Methode IBinder asBinder(), also IBookManager Dort Es gibt drei implementierte Methoden in . Es handelt sich um ein Fenster für die Kommunikation zwischen dem Serverprozess und dem Clientprozess.
Stub ist eine abstrakte Klasse. Diese Klasse erbt von der Klasse android.os.Binder und implementiert die IBookManager-Schnittstelle. In Stub wurde die Schnittstellenmethode asBinder() implementiert, und es gibt zwei AIDL-Schnittstellenmethoden, die wir für die Implementierung in Unterklassen definiert haben, die sie erben. Es wird serverseitig verwendet, daher muss der Server diese beiden Methoden implementieren.
Proxy ist, wie der Name schon sagt, ein Proxy für den Server auf dem Client. Er implementiert auch die IBookManager-Schnittstelle und implementiert alle Methoden in IBookManager. Es wird auf der Clientseite verwendet und ist der Proxy des Servers auf der Clientseite.
Jetzt analysieren wir diese drei Klassen nacheinander:
Zur IBookManager-Klasse gibt es nicht viel zu sagen. Sie erbt einfach die asInterface-Schnittstelle und ihre Funktion besteht darin, IBookManager in IBinder zu konvertieren.
Die Proxy-Klasse wurde oben erwähnt. Es handelt sich um eine Kapselungsklasse des Interprozess-Kommunikationsmechanismus. Ihr interner Implementierungsmechanismus ist Binder, den wir anhand der Konstruktionsmethode leicht erkennen können. Sein Konstruktor akzeptiert einen IBinder-Typparameter namens remote, der offensichtlich den Server darstellt. Werfen wir einen Blick auf die Methoden addBook() und getBookList() in dieser Klasse:
@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; }
Sie werden vom Compiler automatisch implementiert, die hier offenbart werden können: Dies Zwei Methoden sind die Fenster, in denen der Clientprozess den Serverprozess aufruft. Zu Beginn dieser beiden Methoden definieren beide zwei Parcel-Objekte (chinesische Übersetzung: Paket). Die Parcel-Klasse kommt uns bekannt vor. Ja, die Parameter von writeToParcel() in der Book-Klasse und createFromParcel() im CREATOR sind vom Typ Parcel. Die Dokumentation zu dieser Klasse wird wie folgt erklärt:
Container für eine Nachricht (Daten und Objektreferenzen), die über einen IBinder gesendet werden kann. Ein Paket kann sowohl reduzierte Daten enthalten, die auf der anderen Seite des IPC nicht reduziert werden (unter Verwendung der verschiedenen Methoden hier zum Schreiben bestimmter Typen). oder die allgemeine {@link Parcelable}-Schnittstelle) und Verweise auf Live-{@link IBinder}-Objekte, die dazu führen, dass die andere Seite einen Proxy-IBinder erhält, der mit dem ursprünglichen IBinder im Parcel verbunden ist.
Übersetzen: Proxy ist ein Container, der Nachrichten über IBinder weiterleiten kann. Ein Paket kann serialisierbare Daten enthalten, die am anderen Ende des IPC deserialisiert werden. Es kann auch einen Verweis auf ein IBinder-Objekt enthalten, wodurch das andere Ende ein Proxy-Objekt vom Typ IBinder empfängt das ursprüngliche IBinder-Objekt in Parcel.
Das folgende Diagramm wird zur intuitiven Veranschaulichung verwendet:
Wie in der Abbildung gezeigt, können wir intuitiv erkennen, dass der Server Parcel als Datenpaket verwendet Verlassen Sie sich bei der Kommunikation auf Binder und den Client. Das Datenpaket ist das Objekt nach der Serialisierung.
Wie oben erwähnt, definieren diese beiden Methoden zwei Parcel-Objekte mit den Namen _data und _reply. Aus Sicht des Clients ist _data das Datenpaket, das der Client an den Server sendet vom Server an den Client gesendet.
Danach haben wir begonnen, diese beiden Objekte für die Kommunikation mit dem Server zu verwenden. Wir können beobachten, dass beide Methoden einen solchen Methodenaufruf haben, der vier Parameter hat eines Parameters später. Der zweite Parameter _data ist dafür verantwortlich, Datenpakete wie die Parameter von Schnittstellenmethoden an den Server zu senden. Der dritte Parameter _reply ist dafür verantwortlich, Datenpakete wie die Rückgabewerte von Schnittstellenmethoden vom Server zu empfangen. Diese Codezeile enthält nur einen einfachen Methodenaufruf, ist jedoch der Kernbestandteil der AIDL-Kommunikation. Sie führt tatsächlich einen Remote-Methodenaufruf durch (der Client ruft die Stub-Methode des Servers mit demselben Namen über die vom lokalen Proxy bereitgestellte Schnittstellenmethode auf ), damit Sie sich vorstellen können, dass es sich um einen zeitaufwändigen Vorgang handelt.
In unserem Beispiel:
void addBook(Book book) muss _data verwenden, um den Parameter Book:book an den Server zu senden. Die Möglichkeit, ihn zu senden, ist die Methode writeToParcel(Parcel out ). Wie Sie sich vorstellen können, ist _data tatsächlich der Parameter out. Erinnern Sie sich an die Implementierung dieser Methode in Book? Wir verpacken die Felder von Book einzeln in Parcel.
List
Natürlich übergeben _data und _reply in diesen beiden Methoden nicht nur das Objekt, sondern auch einige Verifizierungsinformationen. Wir müssen nicht darauf eingehen, aber es sollte beachtet werden, dass die Paketverpackungsreihenfolge und Die Auspackreihenfolge muss unbedingt übereinstimmen. Wenn der erste gepackte Wert beispielsweise int:i ist, sollte der erste entpackte Wert ebenfalls dieser ganzzahlige Wert sein. Das heißt, wenn der erste Aufruf beim Packen Parcel.writeInt(int) ist, sollte der erste Aufruf beim Entpacken Parcel.readInt() sein.
An dieser Stelle wurde der Proxy des Clients erklärt. Werfen wir einen Blick auf den Stub des Servers.
Stub implementiert eine der Methoden von IBookManager. Es gibt einfach sich selbst zurück, da Stub selbst von Binder erbt und Binder von IBinder erbt, sodass es kein Problem gibt. Sie fragen sich vielleicht: Gibt es zwei Methoden, die nicht implementiert wurden? Bei diesen beiden Methoden handelt es sich um die von uns definierten Schnittstellenmethoden, deren Implementierung dem Serverprozess überlassen bleibt. Mit anderen Worten: Wir müssen einen Stub-Implementierer im Serverprozess definieren. Im Folgenden finden Sie eine Analyse zweier wichtiger Methoden in 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 跨进程通信成功了。