1. 程式人生 > >Android的IPC機制--實現AIDL的最簡單例子(上)

Android的IPC機制--實現AIDL的最簡單例子(上)

前言

對於AIDL的介紹, 將主要分為兩部分:

  • 上篇 將介紹 ADIL的使用, 給出對應的demo

  • 下篇 將分析ADIL的實現原理及原始碼分析

一、到底什麼是AIDL

AIDL是一個縮寫,全稱是Android Interface Defination Language, 即Android介面定義語言。它的主要作用是實現跨程序通訊。通過定義我們想要的AIDL檔案, 會自動在生成對應的java程式碼,讓開發者專注於應用層的開發,提升開發效率。

二、為什麼要使用AIDL來跨程序通訊

Android程序中的每個程序都是在自己獨立的記憶體上執行並存儲自己的資料, 兩個程序間想要通訊,就需要使用一些特定的元件, 比如說我們的AIDL,比如說BroadCastReceiver、Messenger、ContentProvider等,那我們為什麼要選擇AIDL來進行跨程序通訊呢? 兩個字:高效! BroadcastReceiver使用簡單, 但是佔用的系統資源比較多, 耗費時間長,如果頻繁的跨程序通訊, 效率就太低了; Messenger跨程序通訊是, 請求佇列時同步的, 無法併發執行,只有前一個任務處理完了, 才能處理下一個任務; ContentProvider呢,主要作用是把資料暴露出來, 別的程序可以來讀取。最最最重要的一點:以上的各種跨程序通訊方式,其實依賴的都是Binder機制,都是對Binder進行的封裝, 但是由於是已經封裝好的系統元件, 它制定的規則已經固定,使用上也就具有一定的侷限性。而AIDL是直接為了讓不同程序間可相互呼叫而最對Binder直接的封裝, 是按照我們想要的功能進行的定製化 封裝, 它雖然使用起來麻煩了一點, 但卻是效率最高的。

當然,看到Binder不要覺得害怕,本文不會深入Binder龐大的底層原始碼去分析, 只會解析它在應用層的使用及原始碼,讓大家瞭解Binder在應用層常見套路。這將在下篇中體現。

三、AIDL基礎知識及語法介紹

首先一定要明確,AIDL的語法很簡單,大家不要怕難。它的語法和java基本上是類似的, 只是有一些細微上的不同,因為它就是用來簡化Android Developer的工作的, 太難了使用成本太高,誰還願意用。

下面主要介紹下它的基礎知識點:

  • 檔案型別:AIDL檔案字尾為 .aidl。
  • 資料型別:AIDL預設支援一些資料型別,使用這些資料型別是不用導包的,除了這些型別以外,必須匯入包, 即便是我們要使用的物件類與當前正在編寫的.aidl檔案在同一個包下, 這是與java的不同點,這個可以參見後面的demo。其中,預設支援的資料型別有:

    1. java中的八種基本資料型別: byte(8), short(16), int(32), long(64), float(32), double(64), boolean(1), char(16), 括號的數字表示基本資料型別的位長。
    2. CharSequence型別(包含了String)
    3. List型別:List中 的所有元素必須是AIDL支援的型別,即預設的資料型別或者是其他AIDL生成的介面,或者是定義的Parcelable。List可以使用泛型。
    4. Map型別:Map中的所有元素必須是AIDL支援的型別,即預設的資料型別或者是其他AIDL生成的介面,或者是定義的Parcelable。Map是不支援泛型。

.

  • 定向tag: AIDL中的定向 tag 表示了在跨程序通訊中資料的流向,其中 in 表示資料只能由客戶端流向服務端, out 表示資料只能由服務端流向客戶端,而 inout 則表示資料可在服務端與客戶端之間雙向流通。其中,資料流向是針對在客戶端中的那個傳入方法的物件而言的。in 為定向 tag 的話表現為服務端將會接收到一個那個物件的完整資料,但是客戶端的那個物件不會因為服務端對傳參的修改而發生變動;out 的話表現為服務端將會接收到那個物件的的空物件,但是在服務端對接收到的空物件有任何修改之後客戶端將會同步變動;inout 為定向 tag 的情況下,服務端將會接收到客戶端傳來物件的完整資訊,並且客戶端將會同步服務端對該物件的任何變動。

    另外, Java 中的基本型別和 String ,CharSequence 的定向 tag 預設且只能是 in 。還有,請注意,請不要濫用定向 tag ,而是要根據需要選取合適的——要是不管三七二十一,全都一上來就用 inout ,等工程大了系統的開銷就會大很多——因為排列整理引數的開銷是很昂貴的。

  • AIDL檔案大致可以分為兩種:

    • 第一類是用來定義parcelable物件,以供其他AIDL檔案使用AIDL中非預設支援的資料型別的。
    • 第二類是用來定義方法介面,以供系統使用來完成跨程序通訊的。

    所有的非預設支援資料型別必須通過第一類AIDL檔案定義才能被使用。同時我們可以看到,我們一直在定義介面, 卻沒有具體的實現, 從這裡可以體會一下為什麼AIDL叫做介面定義語言。

    舉個例子, 下面分別給出兩種AIDL檔案的定義:

    //第一類AIDL檔案的例子
    //定義一個 Book.aidl檔案
    //這個檔案的作用是引入了一個序列化物件 Book 供其他的AIDL檔案使用
    //我們需要先定義一個Book.java, 這個類需要實現parcelable序列化介面

    //注意:下面這一行程式碼是包名,引入Book這個物件,這一點和java類似,
    //Book.aidl與Book.java他們需要放在同一個包下
    package com.example.my_chapter_2.aidl;

    //注意 parcelable 是小寫
    parcelable Book;

//第二類AIDL檔案的例子
//定義一個BookManager.aidl

//同樣, 需要當前檔案的包名
package com.lypeer.ipcclient;

//匯入所需要使用的非預設支援資料型別的包, 和java類似
package com.example.my_chapter_2.aidl.Book;

//介面定義語言, 當然要定義介面啦, 和java介面類似
interface BookManager {

    //所有的返回值前都不需要加任何東西,不管是什麼資料型別
    List<Book> getBooks();


    //傳參時除了Java基本型別以及String,CharSequence之外的型別
    //都需要在前面加上定向tag,具體加什麼量需而定
    void addBook(in Book book);

}

**注意:從以上介紹可以知道, 我們需要建立的AIDL檔案數量至少是 n+1 個, n表示Parcelable物件的數量, 1表示定義介面方法的AIDL檔案數量, 當然也有可能為數量為2,3,4。。。

四、使用AIDL跨程序通訊的Demo—建立AIDL檔案

介紹完AIDL的基礎知識, 下面我們進入實戰吧, 直接上demo, 上面提到, AIDL檔案主要分為兩類,第一種用來定義Parcelable物件,第二種用來定義方法介面。有的AIDL沒有自定義Parcelable物件, 但是比較少,這種簡單例子這裡就略過了,我們按照有Parcelable物件來建立我們的Demo。我們接下來將分三步走

  • 第一步:建立我們需要的類物件
  • 第二步:建立第一種AIDL檔案,定義我們的Parcelable物件
  • 第三步:建立第二種AIDL檔案,定義我們的方法介面
第一步,建立我們需要的類物件

我們先定義一個Parcelable物件。首先建立一個類,簡歷getter和setter方法並新增一個無參建構函式:

public class Book {

    private String name;
    private int price;

    public Book() {

    }

    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;
    }
}

接著, 我們讓這個類實現序列化介面Parcelable介面,實現這個介面後,根據提示,實現它的方法, 補齊程式碼,下面上程式碼:

/**
  * 建立一個AIDL所支援的 資料型別 
  *
  */
public class Book implements Parcelable{

private String name;
private int price;

public Book() {

}

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

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 int describeContents() {
    // 預設返回0 即可
    return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
    // :預設生成的模板類的物件只支援為 in 的定向 tag 。
    //為什麼呢?因為預設生成的類裡面只有 writeToParcel() 方法,而如果要支援為 out 或者 inout 的定向 tag 的話,
    //還需要實現 readFromParcel() 方法

    //把資料寫進parcel中, 後面的讀值順序應當是和writeToParcel()方法中一致的
    dest.writeString(name);
    dest.writeInt(price);
}

/**
 * 自己定義的readFromParcel方法, 這樣才支援 定向tag out
 * @param dest
 */
public void readFromParcel(Parcel dest) {
     //注意,此處的讀值順序應當是和writeToParcel()方法中一致的
    name = dest.readString();
    price = dest.readInt();
}


/**
 * 還必須要有Creator, 這是實現parcel介面 所必須的
 */
public static final Creator<Book> CREATOR = new Creator<Book>() {

    @Override
    public Book[] newArray(int size) {
        // 建立一個 Book陣列
        return new Book[size]; //建立
    }

    @Override
    public Book createFromParcel(Parcel source) {
        // 根據傳來的資料 建立Book
        return new Book(source);
    }
};

@Override
public String toString() {
    return String.format("[price:%s, name:%s]", price, name);
}

}

請注意,這裡有一個坑:預設生成的模板類的物件只支援為 in 的定向 tag 。為什麼呢?因為預設生成的類裡面只有 writeToParcel() 方法,而如果要支援為 out 或者 inout 的定向 tag 的話,還需要實現 readFromParcel() 方法——而這個方法其實並沒有在 Parcelable 接口裡面,所以需要我們自己寫一下, 按照上述程式碼寫入即可。

為什麼要實現序列化介面: 由於不同的程序有著不同的記憶體區域,並且它們只能訪問自己的那一塊記憶體區域,所以我們不能像平時那樣,傳一個物件的引用過去就完事了——引用指向的是一個記憶體區域,現在目標程序根本不能訪問源程序的記憶體,那把它傳過去又有什麼用呢?所以我們必須將要傳輸的資料轉化為能夠在記憶體之間流通的形式。這個轉化的過程就叫做序列化與反序列化。簡單來說是這樣的:比如現在我們要將一個物件的資料從客戶端傳到服務端去,我們就可以在客戶端對這個物件進行序列化的操作,將其中包含的資料轉化為序列化流,然後將這個序列化流傳輸到服務端的記憶體中去,再在服務端對這個資料流進行反序列化的操作,從而還原其中包含的資料——通過這種方式,我們就達到了在一個程序中傳輸資料到另一個程序的資料中的目的。

,

序列化介面Parcelable和Serializable的區別, 為什麼用Parcelable: Serializable介面是java中的序列化介面, 使用簡單一點,但是開銷大,序列化和反序列化過程都需要大量的IO操作。Parcelable介面是Android定製的序列化介面,比起java的序列化介面Serializable,效能上更有優勢,在Android平臺程序間通訊使用, 效率較高, 是Android推薦的序列化方式,缺點是使用的時候稍微麻煩一點點, 要多寫兩個方法。 Parcelable主要用在 在程序間傳輸物件的序列化上, 通過Parcelable序列化的物件儲存到本地或是網路傳輸也是可以的, 但是這個過程會稍微複雜一些,因此這兩種情況下建議大家使用Serializable。

第二步,建立第一種AIDL檔案,定義我們的Parcelable物件

上面我們建立了一個Book.java, 我們需要一個 Book.aidl 檔案來將 Book 類引入使得其他的 AIDL 檔案其中可以使用 Book 物件。那麼第一步,在Book.java的同一個包下,新建一個字尾為.aidl的 Book.aidl檔案, 這個檔案如下:

//Book.aidl

//這個aidl檔案的作用是為了引入一個序列化的 資料型別給 其他aidl檔案使用

//注意:Book.aidl 和 Book.java的包名必須一致
package com.example.my_chapter_2.aidl;

//注意 parcelable 是小寫
parcelable Book;

######第三步,建立第二種AIDL檔案,定義我們的方法介面

檔案如下:

//作用:定義方法介面

//當前包的包名
package com.example.my_chapter_2.aidl;

//匯入需要的自定義資料類
import com.example.my_chapter_2.aidl.Book;

//介面定義語言, 當然要定義介面啦
interface BookManager {

    //定義的第一個方法
    //所有的返回值前都不需要加任何東西,不管是什麼資料型別
    List<Book> getBooks();

    //定義的第二個方法
    //傳參時除了Java基本型別以及String,CharSequence之外的型別
    //都需要在前面加上定向tag,具體加什麼量需而定
    void addBook(in Book book);

}

注意:這裡又有一個坑! 大家可能注意到了,在 Book.aidl 檔案中,我一直在強調:Book.aidl與Book.java的包名應當是一樣的。這似乎理所當然的意味著這兩個檔案應當是在同一個包裡面的——事實上,很多比較老的文章裡就是這樣說的,他們說最好都在 aidl 包裡同一個包下,方便移植——然而在 Android Studio 裡並不是這樣。如果這樣做的話,系統根本就找不到 Book.java 檔案,從而在其他的AIDL檔案裡面使用 Book 物件的時候會報 Symbol not found 的錯誤。為什麼會這樣呢?因為 Gradle 。大家都知道,Android Studio 是預設使用 Gradle 來構建 Android 專案的,而 Gradle 在構建專案的時候會通過 sourceSets 來配置不同檔案的訪問路徑,從而加快查詢速度——問題就出在這裡。Gradle 預設是將 java 程式碼的訪問路徑設定在 java 包下的,這樣一來,如果 java 檔案是放在 aidl 包下的話那麼理所當然系統是找不到這個 java 檔案的。那應該怎麼辦呢?

又要 java檔案和 aidl 檔案的包名是一樣的,又要能找到這個 java 檔案——那麼仔細想一下的話,其實解決方法是很顯而易見的。首先我們可以把問題轉化成:如何在保證兩個檔案包名一樣的情況下,讓系統能夠找到我們的 java 檔案?這樣一來思路就很明確了:要麼讓系統來 aidl 包裡面來找 java 檔案,要麼把 java 檔案放到系統能找到的地方去,也即放到 java 包裡面去。接下來我詳細的講一下這兩種方式具體應該怎麼做:

方法1: 修改 build.gradle 檔案:在 android{} 中間加上下面的內容:

sourceSets {
    main {
        java.srcDirs = ['src/main/java', 'src/main/aidl']
    }
}

也就是把 java 程式碼的訪問路徑設定成了 java 包和 aidl 包,這樣一來系統就會到 aidl 包裡面去查詢 java 檔案,也就達到了我們的目的。只是有一點,這樣設定後 Android Studio 中的專案目錄會有一些改變,我感覺改得挺難看的。

方法2:把 java 檔案放到 java 包下去

把 Book.java 放到 java 包裡任意一個包下,保持其包名不變,與 Book.aidl 一致。只要它的包名不變,Book.aidl 就能找到 Book.java ,而只要 Book.java 在 java 包下,那麼系統也是能找到它的。但是這樣做的話也有一個問題,就是在移植相關 .aidl 檔案和 .java 檔案的時候沒那麼方便,不能直接把整個 aidl 資料夾拿過去完事兒了,還要單獨將 .java 檔案放到 java 資料夾裡 去。
我們可以用上面兩個方法之一來解決找不到 .java 檔案的坑,具體用哪個就看大家怎麼選了,反正都挺簡單的。

到這裡我們就已經將AIDL檔案新建並且書寫完畢了,clean 一下專案,如果沒有報錯,這一塊就算是大功告成了。

五、使用AIDL跨程序通訊的Demo–服務端程式碼

想要實現AIDL通訊,我們需要保證在客戶端和服務端的都有一樣的.aidl檔案和其中涉及到的java類。所以不管寫在哪一端, 我們都需要把這些檔案複製到另一端,並且保持包名一致,前面已經有所介紹。因為這樣編譯器才會給我們自動生成相同的類, 然後我們跨程序的資料就可以正常的序列化及反序列化了。

然後我們編寫服務端程式碼, 通常就是建立一個Service, 然後在Service中實現我們在aidl中定義的介面方法。這樣客戶端呼叫介面後, 服務端就執行對應方法, 然後返回資料(如果有返回值), 下面是服務端程式碼:

public class AIDLService extends Service {
private String TAG = AIDLService.class.getSimpleName();

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

/**
 * 根據AIDL檔案 編譯器為我們生成的BookManager
 */
private final BookManager.Stub mBookManager = new BookManager.Stub() {

    @Override
    public List<Book> getBooks() throws RemoteException {
        //客戶端來獲取資料時, 返回服務端的資料
        synchronized (this) {
            Log.e(TAG, "invoking getBooks() method , now the list is : " + mBooks.toString());
            if(mBooks != null) 
                return mBooks;

            return new ArrayList<Book>();
        }
    }

    @Override
    public void addBook(Book book) throws RemoteException {
        //客戶端發來修改資料的請求, 服務端新增對應資料
        synchronized (this) {
            if(mBooks == null) {
                mBooks = new ArrayList<Book>();
            }

            if(book == null) {
                Log.e(TAG, "service add book: book is null");
                return;
            }
            mBooks.add(book);
             //列印mBooks列表,觀察客戶端傳過來的值
            Log.e(TAG, "invoking addBooks() method , now the list is : " + mBooks.toString());
        }

    }
};

@Override
public void onCreate() {
    super.onCreate();
    //啟動時就去 加一本書
    Book book = new Book();
    book.setName("服務端書籍");
    book.setPrice(100);
    try {
        mBookManager.addBook(book);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

@Override
public IBinder onBind(Intent intent) {
    Log.e(getClass().getSimpleName(), String.format("on bind,intent = %s", intent.toString()));
    return mBookManager;
}

}

程式碼結構很簡單, 主要有 1.建立了一個BookManager.Stub型別的 mBookManager物件; 2.onCreate()生命週期方法, 3.onBind()方法。這也是AIDL使用的程式碼套路,主要實現這三塊即可, 其中BookManager 它是編譯器根據我們的AIDL檔案生成的java類,它的內部類BookManager.Stub這個類是我們用來通訊的關鍵, 這是一個Binder物件,後面會分析。

然後需要在AndroidManifest.xml檔案裡面註冊我們的Service,四大元件大家應該都不陌生吧:

<!-- 千萬不要忘記 設定exported為true, 否則其他APP或程序無法訪問 -->
<service android:name="com.example.my_chapter_2.service.AIDLService"
        android:exported="true">
        <intent-filter >
            <action android:name="com.example.my_chapter_2"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
    </service>

好了, 服務端就編寫好了。

六、使用AIDL跨程序通訊的Demo–客戶端程式碼

客戶端我們要完成的工作主要是呼叫服務端的方法,但是在那之前,我們首先要繫結上服務端,完整的客戶端程式碼是這樣的:

public class MainActivity extends Activity {

    private String TAG = "cilent";

    //由AIDL檔案生成的Java類
    private IBookManager mBookManager = null;

    //標誌當前與服務端連線狀況的布林值,false為未連線,true為連線中
    private boolean mBound = false;

    //包含Book物件的list
    private List<Book> mBooks;

private static int mPrice = 10;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

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

    @Override
    protected void onStop() {
        // TODO Auto-generated method stub
        super.onStop();
        if(mBound) {//如果綁定了, 才能解綁
            unbindService(mServiceConnection);//一定要記得給service解綁
        }
    }

    /**
     * 按鈕的點選事件
     * @param view
     */
    public void addBook(View view) {
        if(!mBound) {
            //沒繫結則去繫結服務端
            attemptToBindService();
            return;
        }

        Book book = new Book();
        book.setName("android 開發藝術探索 " + System.currentTimeMillis());
        book.setPrice(mPrice++);
        try {
            mBookManager.addBook(book);
        } catch (RemoteException e) {
            Log.e(TAG, " addBook RemoteException");
            e.printStackTrace();
        }

    }

    public void getBooks(View view) {
        if(!mBound) {
            //沒繫結則去繫結服務端
            attemptToBindService();
            return;
        }

        try {
            List<Book> books = mBookManager.getBookList();
            Log.e(TAG, " getBooks: "+books.toString());
        } catch (RemoteException e) {
            Log.e(TAG, " addBook RemoteException");
            e.printStackTrace();
        }
    }


    /**
     * 嘗試與服務端進行連線
     */
    private void attemptToBindService() {
        Intent intent = new Intent();
        intent.setAction("com.example.my_chapter_2_mine"); //設定企圖
        intent.setPackage("com.example.my_chapter_2.service");//設定包名

        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);

    }

    ServiceConnection mServiceConnection = new ServiceConnection() {

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //一般不會走到這個回撥, 除非是 service異常終止, 才會回撥到這來
            Log.e(TAG, "service disconnected");
            mBound = false;

        }

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 繫結成功後給的回撥
            Log.e(TAG, "service connected");
            mBookManager = BookManagerImpl.asInterface(service); //初始化mBookManager
            mBound = true;

            if(mBookManager != null) {
                try {
                    mBooks = mBookManager.getBookList();
                     Log.e(TAG, mBooks.toString());
                } catch (RemoteException e) {
                    Log.e(TAG, "RemoteException");
                    e.printStackTrace();
                }
            }

        }
    };

}

程式碼結構也很清晰, 就是我們繫結Service的常規步驟,然後用繫結後得到的本地代理mBookManager去操作服務端資料。

七、測試通訊

客戶端服務端都建立好了, 我們開始通訊吧, 將兩個測試demo都裝到手機上,啟動服務端demo, 然後在客戶端demo進行操作

呼叫addBook()方法,服務端列印日誌:

//服務端的 log 資訊,我把無用的資訊頭去掉了,然後給它編了個號
on bind,intent = Intent { act=com.example.my_chapter_2 pkg=com.example.my_chapter_2.service }
invoking addBooks() method , now the list is : [name : 服務端資料 , price : 100,name : Android開發藝術探索 , price : 10]

客戶端日誌:

//客戶端的 log 資訊
service connected
[name : Android開發藝術探索 , price : 10]
七、結語

對於AIDL的使用流程到這裡就講完了, 我們再次總結一下, 主要分為一下步驟:

  • 1、定義我們需要的Parcelable物件,即實現Parcelable介面的java類
  • 2、定義第一類AIDL檔案, 即Parcelable物件對應的AIDL檔案
  • 3、定義第二類AIDL檔案, 即我們的介面方法 檔案
  • 4、把AIDL檔案拷貝到服務端/客戶端 相同的包名下
  • 5、Service相關程式碼:服務端註冊Service、客戶端繫結Service
  • 6、啟動服務端客戶端,開始通訊

    下一篇, 我們將介紹AIDL實現原理及對應的應用層原始碼分析

    謝謝大家, 喜歡的朋友麻煩點個贊吧~