Android IPC之AIDL的使用

前言

在決定用這個標題之前甚是忐忑,主要是擔心自己對AIDL的理解不夠深入,到時候大家看了之後說——你這是什麼玩意兒,就這麼點東西就敢說夠了?簡直是坐井觀天不知所謂——那樣就很尷尬了。不過又轉念一想,我輩年輕人自當有一種一往無前的銳氣,標題大氣一點豈不更好?並且大家都是文明人,總歸更多的是理解與補充而不是侮辱與謾罵?所以最終還是厚顏用了這麼一個不怎麼有恥的標題。

好了,接下來進入正題,談談我對AIDL的理解和認識。

正文

1,概述

AIDL是一個縮寫,全稱是Android Interface Definition Language,也就是Android接口定義語言。是的,首先我們知道的第一點就是:AIDL是一種語言。既然是一種語言,那麼相應的就很自然的衍生出了一些問題:

  • 爲什麼要設計出這麼一門語言?
  • 它有哪些語法?
  • 我們應該如何使用它?
  • 再深入一點,我們可以思考,我們是如何通過它來達到我們的目的的?
  • 更深入一點,爲什麼要這麼設計這門語言?會不會有更好的方式來實現我們的目的?

接下來,我們就一步步的來解答上面的這些問題。

ps:1,在研究AIDL相關的東西之前,一些必要的知識儲備是要有的。一方面是關於Android中service相關的知識,要了解的比較通透才行,關於這方面的東西可以參考 Android中的Service:默默的奉獻者 (1)Android中的Service:Binder,Messenger,AIDL(2) 這兩篇博文。另一方面是關於Android中序列化的相關知識,這方面的東西文中會簡單提及,但是如果想要深入的研究一下的話最好還是去找一些這方面的資料看一下。 2,我的編譯環境爲Android Studio2.1.2,SDK Version 23,JDK 1.7。

2,爲什麼要設計這門語言?

設計這門語言的目的是爲了實現進程間通信,尤其是在涉及多進程併發情況下的進程間通信

每一個進程都有自己的Dalvik VM實例,都有自己的一塊獨立的內存,都在自己的內存上存儲自己的數據,執行着自己的操作,都在自己的那片狹小的空間裏過完自己的一生。每個進程之間都你不知我,我不知你,就像是隔江相望的兩座小島一樣,都在同一個世界裏,但又各自有着自己的世界。而AIDL,就是兩座小島之間溝通的橋樑。相對於它們而言,我們就好像造物主一樣,我們可以通過AIDL來制定一些規則,規定它們能進行哪些交流——比如,它們可以在我們制定的規則下傳輸一些特定規格的數據。

總之,通過這門語言,我們可以愉快的在一個進程訪問另一個進程的數據,甚至調用它的一些方法,當然,只能是特定的方法。

但是,如果僅僅是要進行跨進程通信的話,其實我們還有其他的一些選擇,比如 BroadcastReceiver , Messenger 等,但是 BroadcastReceiver 佔用的系統資源比較多,如果是頻繁的跨進程通信的話顯然是不可取的;Messenger 進行跨進程通信時請求隊列是同步進行的,無法併發執行,在有些要求多進程的情況下不適用——這種時候就需要使用 AIDL 了。如果想要了解它們更詳細的區別的話,可以去我的另一篇博文看看:Android中的Service:Binder,Messenger,AIDL(2)

3,它有哪些語法?

其實AIDL這門語言非常的簡單,基本上它的語法和 Java 是一樣的,只是在一些細微處有些許差別——畢竟它只是被創造出來簡化Android程序員工作的,太複雜不好——所以在這裏我就着重的說一下它和 Java 不一樣的地方。主要有下面這些點:

  • 文件類型:用AIDL書寫的文件的後綴是 .aidl,而不是 .java。
  • 數據類型:AIDL默認支持一些數據類型,在使用這些數據類型的時候是不需要導包的,但是除了這些類型之外的數據類型,在使用之前必須導包,就算目標文件與當前正在編寫的 .aidl 文件在同一個包下——在 Java 中,這種情況是不需要導包的。比如,現在我們編寫了兩個文件,一個叫做 Book.java ,另一個叫做 BookManager.aidl,它們都在 com.lypeer.aidldemo 包下 ,現在我們需要在 .aidl 文件裏使用 Book 對象,那麼我們就必須在 .aidl 文件裏面寫上 import com.lypeer.aidldemo.Book; 哪怕 .java 文件和 .aidl 文件就在一個包下。
    默認支持的數據類型包括:
    • Java中的八種基本數據類型,包括 byte,short,int,long,float,double,boolean,char。
    • String 類型。
    • CharSequence類型。
    • List類型:List中的所有元素必須是AIDL支持的類型之一,或者是一個其他AIDL生成的接口,或者是定義的parcelable(下文關於這個會有詳解)。List可以使用泛型。
    • Map類型:Map中的所有元素必須是AIDL支持的類型之一,或者是一個其他AIDL生成的接口,或者是定義的parcelable。Map是不支持泛型的。
  • 定向tag:這是一個極易被忽略的點——這裏的“被忽略”指的不是大家都不知道,而是很少人會正確的使用它。在我的理解裏,定向 tag 是這樣的:AIDL中的定向 tag 表示了在跨進程通信中數據的流向,其中 in 表示數據只能由客戶端流向服務端, out 表示數據只能由服務端流向客戶端,而 inout 則表示數據可在服務端與客戶端之間雙向流通。其中,數據流向是針對在客戶端中的那個傳入方法的對象而言的。in 爲定向 tag 的話表現爲服務端將會接收到一個那個對象的完整數據,但是客戶端的那個對象不會因爲服務端對傳參的修改而發生變動;out 的話表現爲服務端將會接收到那個對象的的空對象,但是在服務端對接收到的空對象有任何修改之後客戶端將會同步變動;inout 爲定向 tag 的情況下,服務端將會接收到客戶端傳來對象的完整信息,並且客戶端將會同步服務端對該對象的任何變動。具體的分析大家可以移步我的另一篇博文:你真的理解AIDL中的in,out,inout麼?
    另外,Java 中的基本類型和 String ,CharSequence 的定向 tag 默認且只能是 in 。還有,請注意,請不要濫用定向 tag ,而是要根據需要選取合適的——要是不管三七二十一,全都一上來就用 inout ,等工程大了系統的開銷就會大很多——因爲排列整理參數的開銷是很昂貴的。
  • 兩種AIDL文件:在我的理解裏,所有的AIDL文件大致可以分爲兩類。一類是用來定義parcelable對象,以供其他AIDL文件使用AIDL中非默認支持的數據類型的。一類是用來定義方法接口,以供系統使用來完成跨進程通信的。可以看到,兩類文件都是在“定義”些什麼,而不涉及具體的實現,這就是爲什麼它叫做“Android接口定義語言”。
    注:所有的非默認支持數據類型必須通過第一類AIDL文件定義才能被使用。

下面是兩個例子,對於常見的AIDL文件都有所涉及:

// Book.aidl
//第一類AIDL文件的例子
//這個文件的作用是引入了一個序列化對象 Book 供其他的AIDL文件使用
//注意:Book.aidl與Book.java的包名應當是一樣的
package com.lypeer.ipcclient;

//注意parcelable是小寫
parcelable Book;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
// BookManager.aidl
//第二類AIDL文件的例子
package com.lypeer.ipcclient;
//導入所需要使用的非默認支持數據類型的包
import com.lypeer.ipcclient.Book;

interface BookManager {

    //所有的返回值前都不需要加任何東西,不管是什麼數據類型
    List<Book> getBooks();
    Book getBook();
    int getBookCount();

    //傳參時除了Java基本類型以及String,CharSequence之外的類型
    //都需要在前面加上定向tag,具體加什麼量需而定
    void setBookPrice(in Book book , int price)
    void setBookName(in Book book , String name)
    void addBookIn(in Book book);
    void addBookOut(out Book book);
    void addBookInout(inout Book book);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

4,如何使用AIDL文件來完成跨進程通信?

在進行跨進程通信的時候,在AIDL中定義的方法裏包含非默認支持的數據類型與否,我們要進行的操作是不一樣的。如果不包含,那麼我們只需要編寫一個AIDL文件,如果包含,那麼我們通常需要寫 n+1 個AIDL文件( n 爲非默認支持的數據類型的種類數)——顯然,包含的情況要複雜一些。所以我接下來將只介紹AIDL文件中包含非默認支持的數據類型的情況,至於另一種簡單些的情況相信大家是很容易從中觸類旁通的。

4.1,使數據類實現 Parcelable 接口

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

而通常,在我們通過AIDL進行跨進程通信的時候,選擇的序列化方式是實現 Parcelable 接口。關於實現 Parcelable 接口之後裏面具體有那些方法啦,每個方法是幹嘛的啦,這些我就不展開來講了,那並非這篇文章的重點,我下面主要講一下如何快速的生成一個合格的可序列化的類(以Book.java爲例)。

注:若AIDL文件中涉及到的所有數據類型均爲默認支持的數據類型,則無此步驟。因爲默認支持的那些數據類型都是可序列化的。

4.1.1,編譯器自動生成

我當前用的編譯器是Android Studio 2.1.2,它是自帶了 Parcelable 接口的模板的,只需要我們敲幾下鍵盤就可以輕鬆的生成一個可序列化的 Parcelable 實現類。

首先,創建一個類,正常的書寫其成員變量,建立getter和setter並添加一個無參構造,比如:

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

    private String name;

    private int price;

    public Book() {}

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

然後 implements Parcelable ,接着 as 就會報錯,將鼠標移到那裏,按下 alt+enter(as默認的自動解決錯誤的快捷鍵,如果你們的as有修改過快捷鍵的話以修改後的爲準) 讓它自動解決錯誤,這個時候它會幫你完成一部分的工作:

Parcelable實現第一步

在彈出來的框裏選擇所有的成員變量,然後確定。你會發現類裏多了一些代碼,但是現在還是會報錯,Book下面仍然有一條小橫線,再次將鼠標移到那裏,按下 alt+enter 讓它自動解決錯誤:

Parcelable實現第二步

這次解決完錯誤之後就不會報錯了,這個 Book 類也基本上實現了 Parcelable 接口,可以執行序列化操作了。

但是請注意,這裏有一個坑:默認生成的模板類的對象只支持爲 in 的定向 tag 。爲什麼呢?因爲默認生成的類裏面只有 writeToParcel() 方法,而如果要支持爲 out 或者 inout 的定向 tag 的話,還需要實現 readFromParcel() 方法——而這個方法其實並沒有在 Parcelable 接口裏面,所以需要我們從頭寫。具體爲什麼大家可以去看看:你真的理解AIDL中的in,out,inout麼?

那麼這個 readFromParcel() 方法應當怎麼寫呢?這樣寫:

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

/**
 * 參數是一個Parcel,用它來存儲與傳輸數據
 * @param dest
 */
public void readFromParcel(Parcel dest) {
    //注意,此處的讀值順序應當是和writeToParcel()方法中一致的
    name = dest.readString();
    price = dest.readInt();
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

像上面這樣添加了 readFromParcel() 方法之後,我們的 Book 類的對象在AIDL文件裏就可以用 out 或者 inout 來作爲它的定向 tag 了。

此時,完整的 Book 類的代碼是這樣的:

package com.lypeer.ipcclient;

import android.os.Parcel;
import android.os.Parcelable;

/**
 * Book.java
 *
 * Created by lypeer on 2016/7/16.
 */
public class Book implements Parcelable{
    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;
    }

    private String name;
    private int price;
    public Book(){}

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

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

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

    /**
     * 參數是一個Parcel,用它來存儲與傳輸數據
     * @param dest
     */
    public void readFromParcel(Parcel dest) {
        //注意,此處的讀值順序應當是和writeToParcel()方法中一致的
        name = dest.readString();
        price = dest.readInt();
    }

    //方便打印數據
    @Override
    public String toString() {
        return "name : " + name + " , price : " + price;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75

至此,關於AIDL中非默認支持數據類型的序列化操作就完成了。

4.1.2,插件生成

我不是很清楚 Eclipse 或者較低版本的 as 上會不會像 as 2.1.2 這樣幫我們在實現 Parcelable 接口的過程中做如此多的操作,但是就算不會,我們還有其他的招數——通過插件來幫我們實現 Parcelable 接口。

具體的實現方式和實現過程大家可以參見這篇文章:告別手寫parcelable

4.2,書寫AIDL文件

首先我們需要一個 Book.aidl 文件來將 Book 類引入使得其他的 AIDL 文件其中可以使用 Book 對象。那麼第一步,如何新建一個 AIDL 文件呢?Android Studio已經幫我們把這個集成進去了:

新建AIDL文件

鼠標移到app上面去,點擊右鍵,然後 new->AIDL->AIDL File,按下鼠標左鍵就會彈出一個框提示生成AIDL文件了。生成AIDL文件之後,項目的目錄會變成這樣的:

建立AIDL文件後的項目目錄

比起以前多了一個叫做 aidl 的包,而且他的層級是和 java 包相同的,並且 aidl 包裏默認有着和 java 包裏默認的包結構。那麼如果你用的是 Eclipse 或者較低版本的 as ,編譯器沒有這個選項怎麼辦呢?沒關係,我們也可以自己寫。打開項目文件夾,依次進入 app->src->main,在 main 包下新建一個和 java 文件夾平級的 aidl 文件夾,然後我們手動在這個文件夾裏面新建和 java 文件夾裏面的默認結構一樣的文件夾結構,再在最裏層新建 .aidl 文件就可以了:

自己新建AIDL文件的目錄

注意看圖中的文件目錄。

Ok,如何新建AIDL文件說的差不多了,接下來就該寫AIDL文件的內容了。內容的話如果上一節有認真看的話基本上是沒什麼問題的。在這裏,我們需要兩個AIDL文件,我是這樣寫的:

// Book.aidl
//第一類AIDL文件
//這個文件的作用是引入了一個序列化對象 Book 供其他的AIDL文件使用
//注意:Book.aidl與Book.java的包名應當是一樣的
package com.lypeer.ipcclient;

//注意parcelable是小寫
parcelable Book;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
// BookManager.aidl
//第二類AIDL文件
//作用是定義方法接口
package com.lypeer.ipcclient;
//導入所需要使用的非默認支持數據類型的包
import com.lypeer.ipcclient.Book;

interface BookManager {

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

    //傳參時除了Java基本類型以及String,CharSequence之外的類型
    //都需要在前面加上定向tag,具體加什麼量需而定
    void addBook(in Book book);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

注意:這裏又有一個坑!大家可能注意到了,在 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 包裏面去。接下來我詳細的講一下這兩種方式具體應該怎麼做:

  • 修改 build.gradle 文件:在 android{} 中間加上下面的內容:
sourceSets {
    main {
        java.srcDirs = ['src/main/java', 'src/main/aidl']
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 1
  • 2
  • 3
  • 4
  • 5

也就是把 java 代碼的訪問路徑設置成了 java 包和 aidl 包,這樣一來系統就會到 aidl 包裏面去查找 java 文件,也就達到了我們的目的。只是有一點,這樣設置後 Android Studio 中的項目目錄會有一些改變,我感覺改得挺難看的。

  • 把 java 文件放到 java 包下去:把 Book.java 放到 java 包裏任意一個包下,保持其包名不變,與 Book.aidl 一致。只要它的包名不變,Book.aidl 就能找到 Book.java ,而只要 Book.java 在 java 包下,那麼系統也是能找到它的。但是這樣做的話也有一個問題,就是在移植相關 .aidl 文件和 .java 文件的時候沒那麼方便,不能直接把整個 aidl 文件夾拿過去完事兒了,還要單獨將 .java 文件放到 java 文件夾裏去。

我們可以用上面兩個方法之一來解決找不到 .java 文件的坑,具體用哪個就看大家怎麼選了,反正都挺簡單的。

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

4.3,移植相關文件

我們需要保證,在客戶端和服務端中都有我們需要用到的 .aidl 文件和其中涉及到的 .java 文件,因此不管在哪一端寫的這些東西,寫完之後我們都要把這些文件複製到另一端去。如果是用的上面兩個方法中的第一個解決的找不到 .java 文件的問題,那麼直接將 aidl 包複製到另一端的 main 目錄下就可以了;如果是使用第二個方法的話,就除了把把整個 aidl 文件夾拿過去,還要單獨將 .java 文件放到 java 文件夾裏去。

4.4,編寫服務端代碼

通過上面幾步,我們已經完成了AIDL及其相關文件的全部內容,那麼我們究竟應該如何利用這些東西來進行跨進程通信呢?其實,在我們寫完AIDL文件並 clean 或者 rebuild 項目之後,編譯器會根據AIDL文件爲我們生成一個與AIDL文件同名的 .java 文件,這個 .java 文件纔是與我們的跨進程通信密切相關的東西。事實上,基本的操作流程就是:在服務端實現AIDL中定義的方法接口的具體邏輯,然後在客戶端調用這些方法接口,從而達到跨進程通信的目的。

接下來我直接貼上我寫的服務端代碼:

/**
 * 服務端的AIDLService.java
 * <p/>
 * Created by lypeer on 2016/7/17.
 */
public class AIDLService extends Service {

    public final String TAG = this.getClass().getSimpleName();

    //包含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.e(TAG, "invoking getBooks() method , now the list is : " + 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.e(TAG, "Book is null in In");
                    book = new Book();
                }
                //嘗試修改book的參數,主要是爲了觀察其到客戶端的反饋
                book.setPrice(2333);
                if (!mBooks.contains(book)) {
                    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("Android開發藝術探索");
        book.setPrice(28);
        mBooks.add(book);   
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.e(getClass().getSimpleName(), String.format("on bind,intent = %s", intent.toString()));
        return mBookManager;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63

整體的代碼結構很清晰,大致可以分爲三塊:第一塊是初始化。在 onCreate() 方法裏面我進行了一些數據的初始化操作。第二塊是重寫 BookManager.Stub 中的方法。在這裏面提供AIDL裏面定義的方法接口的具體實現邏輯。第三塊是重寫 onBind() 方法。在裏面返回寫好的 BookManager.Stub 。

接下來在 Manefest 文件裏面註冊這個我們寫好的 Service ,這個不寫的話我們前面做的工作都是無用功:

<service
    android:name=".service.AIDLService"
    android:exported="true">
        <intent-filter>
            <action android:name="com.lypeer.aidl"/>
            <category android:name="android.intent.category.DEFAULT"/>
        </intent-filter>
</service>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

到這裏我們的服務端代碼就編寫完畢了,如果你對裏面的一些地方感覺有些陌生或者根本不知所云的話,說明你對 Service 相關的知識已經有些遺忘了,建議再去看看這兩篇博文:Android中的Service:默默的奉獻者 (1)Android中的Service:Binder,Messenger,AIDL(2)

4.5,編寫客戶端代碼

前面說過,在客戶端我們要完成的工作主要是調用服務端的方法,但是在那之前,我們首先要連接上服務端,完整的客戶端代碼是這樣的:

/**
 * 客戶端的AIDLActivity.java
 * 由於測試機的無用debug信息太多,故log都是用的e
 * <p/>
 * Created by lypeer on 2016/7/17.
 */
public class AIDLActivity extends AppCompatActivity {

    //由AIDL文件生成的Java類
    private BookManager mBookManager = null;

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

    //包含Book對象的list
    private List<Book> mBooks;

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

    /**
     * 按鈕的點擊事件,點擊之後調用服務端的addBookIn方法
     *
     * @param view
     */
    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研發錄In");
        book.setPrice(30);
        try {
            mBookManager.addBook(book);
            Log.e(getLocalClassName(), book.toString());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * 嘗試與服務端建立連接
     */
    private void attemptToBindService() {
        Intent intent = new Intent();
        intent.setAction("com.lypeer.aidl");
        intent.setPackage("com.lypeer.ipcserver");
        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.e(getLocalClassName(), "service connected");
            mBookManager = BookManager.Stub.asInterface(service);
            mBound = true;

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

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.e(getLocalClassName(), "service disconnected");
            mBound = false;
        }
    };
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99

同樣很清晰,首先建立連接,然後在 ServiceConnection 裏面獲取 BookManager 對象,接着通過它來調用服務端的方法。

4.6,開始通信吧!

通過上面的步驟,我們已經完成了所有的前期工作,接下來就可以通過AIDL來進行跨進程通信了!將兩個app同時運行在同一臺手機上,然後調用客戶端的 addBook() 方法,我們會看到服務端的 logcat 信息是這樣的:

//服務端的 log 信息,我把無用的信息頭去掉了,然後給它編了個號
1,on bind,intent = Intent { act=com.lypeer.aidl pkg=com.lypeer.ipcserver }
2,invoking getBooks() method , now the list is : [name : Android開發藝術探索 , price : 28]
3,invoking addBooks() method , now the list is : [name : Android開發藝術探索 , price : 28, name : APP研發錄In , price : 2333]
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

客戶端的信息是這樣的:

//客戶端的 log 信息
1,service connected
2,[name : Android開發藝術探索 , price : 28]
3,name : APP研發錄In , price : 30
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

所有的 log 信息都很正常並且符合預期——這說明我們到這裏爲止的步驟都是正確的,按照上面說的來做是能夠正確的使用AIDL來進行跨進程通信的。

結語

這一篇文章主要介紹了我們在概述裏提到的前三個問題,即:

  • 爲什麼要設計AIDL語言?
  • AIDL的語法是什麼?
  • 如何使用AIDL語言完成跨進程通信?

本來我是準備在這篇文章裏把我那五個問題都講完的,結果寫到這裏發現篇幅已經有些長了,再寫的話可能就少有人有這個耐性讀下去了——那麼寫在後面的這些又有什麼意義呢?於是就乾脆從這裏截斷,將AIDL的工作原理和它的設計思想以及我對於它的這種設計的一些看法放在下一篇博文裏來講述——剛好,有那麼點基礎篇和提高篇的意思,哈哈。

文中相關代碼可點擊 傳送門 下載。

謝謝大家。

(function () { ('pre.prettyprint code').each(function () { var lines = (this).text().split(\n).length;var numbering = $('
    ').addClass('pre-numbering').hide(); (this).addClass(hasnumbering).parent().append( numbering); for (i = 1; i
    發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章