1. 程式人生 > >Android之IPC2————AIDL

Android之IPC2————AIDL

Android之IPC2————AIDL

文章目錄

一.AIDL概述

在上一篇部落格中,我們討論Service繫結時,所用的三種方法,即擴充套件的binder類,Messenger類,還有AIDL,上一章部落格中當時我們只是簡單的介紹了一下。在這裡我們就詳細的來看一看。

1.AIDL是什麼

AIDL即Android介面定義語言,是IDL語言的一種。主要用來定義跨程序通訊時都讓雙方都認可的程式設計介面。

IDL是Interface description language的縮寫,指介面描述語言.IDL通常用於遠端呼叫軟體。在這種情況下,一般是由遠端客戶終端呼叫不同作業系統上的物件元件,並且這些物件元件可能是由不同計算機語言編寫的。IDL建立起了兩個不同作業系統間通訊的橋樑。

2.使用場景

在Android中,一個程序通常無法訪問另一個程序的記憶體,所以程序需要將其物件分解成作業系統可以識別的原語,並將物件編組成可以跨界.所以在Android中,它常常被用來進行跨程序通訊。

3.一些語法

在使用AIDL語言時,需要建立一個.aidl檔案,此時AndroidSDK工具都會生成一個.aidl檔案的IBinder介面,並且其報存在gen/目錄中,Service視情況實現IBinder介面。然後將客戶端與Service進行繫結,此時就可以呼叫IBindr的方法來執行IPC。

建立AIDL時,可以通過可帶引數和返回值的一個或多個方法來宣告介面。引數和返回值可以時任意型別,甚至可以是其他AIDL生成的介面

預設情況下,AIDL支援下列資料類似:

  • JAVA中所有原語型別(如int,long,char,boolean等等)
  • String
  • CharSequence
  • List:List中所有元素都必須是以上列表支援的資料型別,或者其他AIDL生成的介面或者宣告的可打包型別。
  • Map:同List一樣,他也要求所有元素都必須是以上列表支援的資料型別,或者其他AIDL生成的介面或者宣告的可打包型別。

對於不是預設資料的型別,應該使其實現Parcelable介面,並編寫對應的AIDL檔案。

定義介面時,注意:

  • 方法可以帶零個或者多個引數,返回值或者NULl
  • 所有原語引數都需要指示資料走向方向標記,可以是in,out或者inout,原語預設是in,慎用inout引數,它會導致系統開銷非常大。
  • aidl 檔案中包括的所有程式碼註釋都包含在生成的 IBinder 介面中(import 和 package 語句之前的註釋除外)
  • 只客戶端呼叫其中的方法,不支援呼叫其中的AIDL 中的靜態欄位。

二.AIDL實現跨程序通訊

將一個Book類,在不同程序的客戶端和服務中進行傳遞。

因為Book類並不預設型別,所以首先讓它實現Parcelable介面(序列化)

關於序列化的問題,下一篇部落格中進行分析。

1.Book實現 Parcelable 介面

建立一個Book類,建立getter和setter

public class Book {

    private String name;
    private int price;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "書名:"+name +",價格"+price;
    }

    public Book() {
    }

}

讓其繼承 Parcelable 介面,並根據AS的錯誤提醒,自動補全。或者自己手動補全。

public class Book implements Parcelable {

    private String name;
    private int price;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "書名:"+name +",價格"+price;
    }

    public Book() {
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(price);
    }

    protected Book(Parcel in) {
        name = in.readString();
        price = in.readInt();
    }

    public static final Creator<Book> CREATOR = new Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };
}

2.生成AIDL檔案

前面我們說過,因為Book類不是預設型別,所以也要生成關於Book類的AIDL檔案。

滑鼠移到app上面去,點選右鍵,然後 new->AIDL->AIDL File,按下滑鼠左鍵就會彈出一個框提示生成AIDL檔案了。此時在java包層級下,多了一個aidl資料夾,在裡面就可以新建aidl檔案。

Book.aidl檔案

// Book.aidl
//這個檔案的作用是引入了一個序列化物件 Book 供其他的AIDL檔案使用
//注意:Book.aidl與Book.java的包名應當是一樣的

package com.heshucheng.servicedemo;

//注意parcelable是小寫
parcelable Book;

BookManager.aidl檔案

// BookManager.aidl
//作用是定義方法介面

package com.heshucheng.servicedemo;
//匯入所需要使用的非預設支援資料型別的包
import com.heshucheng.servicedemo.Book;
// Declare any non-default types here with import statements

interface  BookManager{

 //所有的返回值前都不需要加任何東西,不管是什麼資料型別
   List<Book> getBooks();
   //傳參時除了Java基本型別以及String,CharSequence之外的型別
   //都需要在前面加上定向tag,具體加什麼量需而定
   void addBook(in Book book);

}

在BookManager中,定義了兩個介面,一個活的書的list,一個是新增書。

3.在Service中實現相關的介面

在AIDL的使用場景中經常在多次執行緒的情景下被呼叫,所以要考慮執行緒安全問題。同時完成請求的時間不止幾毫秒,儘量避免在主執行緒中呼叫相關的介面。

public class AIDLService extends Service {

    private static final String TAG = "AIDLService";

    //包含Book物件的List
    private List<Book> mBooks = new ArrayList<>();

    //由AIDL檔案生成的BookManager
    private final BookManager.Stub mBookManager = new BookManager.Stub() {
        @Override
        public List<Book> getBooks() throws RemoteException { //實現對應的介面
            synchronized (this) {  //確保執行緒安全
                Log.w(TAG, "getBooks: " + mBooks.toString());
                if (mBooks != null){
                    return mBooks;
                }
                return new ArrayList<>();
            }

        }

        @Override
        public void addBook(Book book) throws RemoteException {//實現對應的介面
            synchronized (this){ //確保執行緒安全
                if (mBooks == null){
                    mBooks = new ArrayList<>();
                }

                if (book == null){
                    Log.w(TAG, "addBook: " );
                    book = new Book();
                }

                //嘗試修改book的引數,主要是為了觀察其客戶端的反饋
                book.setPrice(2333);
                if (!mBooks.contains(book)){
                    mBooks.add(book);
                }

                //列印mBooks列表,觀察客戶端傳過來的值
                Log.w(TAG, "addBook: "+mBooks.toString());
            }
        }
    };

    public AIDLService() {

    }

    @Override
    public void onCreate() {
        super.onCreate();
        Book book = new Book();
        book.setName("Android藝術開發探索");
        book.setPrice(28);
        mBooks.add(book);
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.w(TAG, "onBind: " +intent.toString());
        return mBookManager;
    }
}

在Service中,實現相關的介面,同時確保執行緒安全,並在onBind類中將對於的BookManager傳第過去

4.在客戶端中呼叫相關的介面

注意,如果介面完成的時間不止幾毫秒,儘量避免在主執行緒中呼叫相關的介面。

public class AIDLActivity extends AppCompatActivity {

    private static final String TAG = "AIDLActivity";
    //由AIDL檔案生成的Java類
    private BookManager mBookManager = null;

    //標誌當前與服務端連線狀況的情況
    private boolean mBound = false;

    private List<Book> mBooks;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_aidl);
    }

    //按鈕的點選事件
    public void addBook(View view){
        if(!mBound){
            attemptToBindService();
            Toast.makeText(this,"當前與服務端處於未連線狀態,正在嘗試重連,請稍後再試", Toast.LENGTH_SHORT).show();
            return;
        }

        if (mBookManager == null)
            return;

        Book book = new Book();
        book.setName("App研發錄");
        book.setPrice(30);

        try {
            mBookManager.addBook(book); //呼叫對應的介面
            Log.w(TAG, "addBook: "+book.toString());
        }catch (RemoteException e){
            e.printStackTrace();
        }
    }

    /**
     * 嘗試與服務端建立連線
     */
    private void attemptToBindService(){
        Intent intent = new  Intent();

        intent.setAction("com.heshucheng.aidl");
        intent.setPackage("com.heshucheng.servicedemo");
        bindService(intent,mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStart() {
        super.onStart();
        if(!mBound){
            attemptToBindService();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if(mBound){
            unbindService( mServiceConnection);
            mBound =false;
        }
    }

    //繫結介面
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.w(TAG, "繫結成功" );
            mBookManager = BookManager.Stub.asInterface(service);
            mBound = true;

            if (mBookManager !=null){
                try {
                    mBooks = mBookManager.getBooks();//呼叫介面
                    Log.w(TAG,"獲取成功"+mBooks.toString());
                }catch (RemoteException e){
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(TAG, "連線中斷");
            mBound = false;
        }
    };
}

在Activity中呼叫對應的介面。

5.執行結果

客戶端
在這裡插入圖片描述

服務端
在這裡插入圖片描述

三.AIDL生成Binder類分析

在上面的程式碼中,我們發現,在客戶端和服務端中都沒有出現aidl檔案,但依然通過BookManager完成相應的工作,而它就是aidl生成的java檔案,它的完整路徑在app->build->generated->source->aidl->debug->com->包名->BookManager.java

在上文中,我們在Service中實現相應的介面,並將其傳遞給客戶端,在客戶端中直接呼叫相關的介面。好像並沒有關於程序方面的操作,但兩個不同程序的相互呼叫,肯定需要進行IPC,那這一部分在哪實現的?

答案就是在BookManager.java中。

1.asInterface

我們在獲得BookManager類時,是通過這個語句

 mBookManager = BookManager.Stub.asInterface(service);

通過asIbterface獲得BookManager。我們進入asInterface方法中。

         /**
         * Cast an IBinder object into an com.heshucheng.servicedemo.BookManager interface,
         * generating a proxy if needed.
         *將IBinder物件轉換為BookManager介面,根據需要生成代理。
         */
        public static com.heshucheng.servicedemo.BookManager asInterface(android.os.IBinder obj) {
            //判斷是否為空
            if ((obj == null)) {
                return null;
            }

            //搜尋本地是否有可用物件,如果有就將其返回
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.heshucheng.servicedemo.BookManager))) {
                return ((com.heshucheng.servicedemo.BookManager) iin);
            }

            //如果沒有,就新建一個
            return new com.heshucheng.servicedemo.BookManager.Stub.Proxy(obj);
        }

在上面的程式碼中,首先先進行了判空,然後呼叫了 queryLocalInterface() 方法,這個方法是 IBinder 接口裡面的一個方法,它具體的原始碼涉及到IBinder相關內容,我們暫且不去深究。只說明它的作用,它就是去本地搜尋是否有可以的物件

當本地沒有BookManager物件時。會去通過Proxy來獲得一個BookManager物件。讓我們繼續來看Proxy中的原始碼

2.proxy

        private static class Proxy implements com.heshucheng.servicedemo.BookManager {
            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;
            }

            @Override
            public java.util.List<com.heshucheng.servicedemo.Book> getBooks() throws android.os.RemoteException {
                //_data存心客戶端流向服務的資料流
                //_reply儲存服務流向客戶端的資料流
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.heshucheng.servicedemo.Book> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);

                    //呼叫transact() 方法將方法id和兩個 Parcel 容器傳過去Service
                    mRemote.transact(Stub.TRANSACTION_getBooks, _data, _reply, 0);
                    _reply.readException();
                    //從_reply取出結果
                    _result = _reply.createTypedArrayList(com.heshucheng.servicedemo.Book.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public void addBook(com.heshucheng.servicedemo.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)) {
                        //book存入
                        _data.writeInt(1);
                        book.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    //呼叫transact() 方法將方法id和兩個 Parcel 容器傳給Service
                    mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }

        static final int TRANSACTION_getBooks = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

說明:

  • 關於 Parcel :簡單的來說,Parcel 是一個用來存放和讀取資料的容器。我們可以用它來進行客戶端和服務端之間的資料傳輸,當然,它能傳輸的只能是可序列化的資料。具體 Parcel 的使用方法和相關原理可以參見這篇文章Android中Parcel的分析以及使用
  • 關於 transact() 方法:這是客戶端和伺服器端通訊的核心方法。呼叫這個方法之後,客戶端會掛起當前執行緒,等待服務端執行完先關任務後通知並接受返回的 _reply 資料流。
  • transact()第一個引數,是方法ID,這個是客戶端和伺服器端約定好的給方法特殊編碼,彼此一一對應,在AIDL檔案轉化為java檔案的時候,系統會自動給AIDL每一個方法自動分配一個方法ID
  • transact()第四個引數是一個int值,他的作用是設定IPC的模式,為0表示資料可以雙向流動,即reply 流可以正常的攜帶資料回來,如果為 1 的話那麼資料將只能單向流通,從服務端回來的 _reply 流將不攜帶任何資料。在AIDL生成的java檔案裡,這個引數均為0.
  • 關於transact()設計到了Binder機制比較低層的東西,博主沒有太深的研究,所以就直接借用別的結論。

總結一下Proxy類方法的一般工作流程

  • 生成_data和_reply資料流,並向_data中存入客戶端的資料
  • 通過transact() 方法將它們傳遞給服務端,並請求服務端呼叫指定方法。
  • 接收_reoly資料流,並從中取出服務端傳回的資料