Android 程式通訊機制之 AIDL

lwenkun發表於2016-10-30

什麼是 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 檔案,這個檔案包含三個類,這三個類分別是 IBookManagerStub 和 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>

我們的客戶端就是一個 ActivityonCreate() 中進行了服務的繫結,bindService() 方法中有一引數 ServiceConnection:conn,因為繫結服務是非同步進行的,這個引數的作用就是繫結服務成功後回撥的介面,它有兩個回撥方法:一個是連線服務成功後回撥,另一個在與服務端斷開連線後回撥。我們現在關心的主要是 onServiceConnected() 方法,在這裡我們只做了一件事:將服務端轉換過來的 IBinder 物件轉換成 AIDL 介面,我們定義 IBookManager:bookManager 欄位來保持對其的引用。這樣的話,我們就可以通過這個 bookManager 來進行方法的遠端呼叫。我們給客戶端的 Button 註冊事件:每一次點選都會向服務端增加一本書,並且將圖書館現有的圖書數量顯示出來。

現在我們看看程式的執行效果:

每當我們點選按鈕,我們就成功的向服務端新增了一本書,說明我們通過 AIDL 跨程式通訊成功了。

相關文章