歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux編程 >> Linux編程

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 文件,這個文件包含三個類,這三個類分別是 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.  http://www.linuxidc.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.linuxidc.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;
            }
       }

對生成的這三個類的說明如下:

  • 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 /* http://www.linuxidc.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 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; /*  http://www.linuxidc.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.linuxidc.com/article/1496.html  */
    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="http://www.linuxidc.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>

我們的客戶端就是一個 ActivityonCreate() 中進行了服務的綁定,bindService() 方法中有一參數 ServiceConnection:conn,因為綁定服務是異步進行的,這個參數的作用就是綁定服務成功後回調的接口,它有兩個回調方法:一個是連接服務成功後回調,另一個在與服務端斷開連接後回調。我們現在關心的主要是 onServiceConnected() 方法,在這裡我們只做了一件事:將服務端轉換過來的 IBinder 對象轉換成 AIDL 接口,我們定義 IBookManager:bookManager 字段來保持對其的引用。這樣的話,我們就可以通過這個 bookManager 來進行方法的遠程調用。我們給客戶端的 Button 注冊事件:每一次點擊都會向服務端增加一本書,並且將圖書館現有的圖書數量顯示出來。

現在我們看看程序的運行效果:

每當我們點擊按鈕,我們就成功的向服務端添加了一本書,說明我們通過 AIDL 跨進程通信成功了。

更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11

Copyright © Linux教程網 All Rights Reserved