學習AIDL,這一篇文章就夠了

yangxi_001發表於2016-12-28

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來制定一些規則,規定它們能進行哪些交流——比如,它們可以在我們制定的規則下傳輸一些特定規格的資料。

總之,通過這門語言,我們可以愉快的在一個程式訪問另一個程式的資料,甚至呼叫它的一些方法,當然,只能是特定的方法。

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

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() {}

}

然後 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();
}

像上面這樣新增了 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;
    }
}

至此,關於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;
// 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);
}

注意: 這裡又有一個坑! 大家可能注意到了,在 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']
     }
    }
    也就是把 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;
    }
}

整體的程式碼結構很清晰,大致可以分為三塊:第一塊是 初始化 。在 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>

到這裡我們的服務端程式碼就編寫完畢了,如果你對裡面的一些地方感覺有些陌生或者根本不知所云的話,說明你對 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;
        }
    };
}

同樣很清晰,首先建立連線,然後在 ServiceConnection 裡面獲取 BookManager 物件,接著通過它來呼叫服務端的方法。

4.6、開始通訊吧!

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

//服務端的 log 資訊,我把無用的資訊頭去掉了,然後給它編了個號
1,on bind,intent = Intent { act=com.lypeer.aidl pkg=com.lypeer.ipcserver }
2invoking 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]

客戶端的資訊是這樣的:

//客戶端的 log 資訊
1,service connected
2,[name : Android開發藝術探索 , price : 28]
3,name : APP研發錄In , price : 2333

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

結語

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

  • 為什麼要設計AIDL語言?
  • AIDL的語法是什麼?
  • 如何使用AIDL語言完成跨程式通訊?

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

1、原始碼分析:AIDL檔案是怎麼工作的?

進行到上一篇文章的最後一步,我們已經學會了AIDL的全部用法,接下來讓我們透過現象看本質,研究一下究竟AIDL是如何幫助我們進行跨程式通訊的。

我們在上一篇提到過,在寫完AIDL檔案後,編譯器會幫我們自動生成一個同名的 .java 檔案——也許大家已經發現了,在我們實際編寫客戶端和服務端程式碼的過程中,真正協助我們工作的其實是這個檔案,而 .aidl 檔案從頭到尾都沒有出現過。這樣一來我們就很容易產生一個疑問:難道我們寫AIDL檔案的目的其實就是為了生成這個檔案麼?答案是肯定的。事實上,就算我們不寫AIDL檔案,直接按照它生成的 .java 檔案那樣寫一個 .java 檔案出來,在服務端和客戶端中也可以照常使用這個 .java 類來進行跨程式通訊。所以說AIDL語言只是在簡化我們寫這個 .java 檔案的工作而已,而要研究AIDL是如何幫助我們進行跨程式通訊的,其實就是研究這個生成的 .java 檔案是如何工作的。

1.1、這個檔案在哪兒?

要研究它,首先我們就需要找到它,那麼它在哪兒呢?在這裡:

Android:學習AIDL,這一篇文章就夠了(下)

它在這兒

它的完整路徑是:app->build->generated->source->aidl->debug->com->lypeer->ipcclient->BookManager.java(其中 com.lypeer.ipcclient 是包名,相對應的AIDL檔案為 BookManager.aidl )。在Android Studio裡面目錄組織方式由預設的 Android 改為 Project 就可以直接按照資料夾結構訪問到它。

1.2、從應用看原理

和我一貫的分析方式一樣,我們先不去看那些冗雜的原始碼,先從它在實際中的應用著手,輔以思考分析,試圖尋找突破點。首先從服務端開始,刨去其他與此無關的東西,從巨集觀上我們看看它幹了些啥:

private final BookManager.Stub mBookManager = new BookManager.Stub() {
    @Override
    public List<Book> getBooks() throws RemoteException {
        // getBooks()方法的具體實現
    }

    @Override
    public void addBook(Book book) throws RemoteException {
         // addBook()方法的具體實現
    }
};

public IBinder onBind(Intent intent) {
    return mBookManager;
}

可以看到首先我們是對 BookManager.Stub 裡面的抽象方法進行了重寫——實際上,這些抽象方法正是我們在 AIDL 檔案裡面定義的那些。也就是說,我們在這裡為我們之前定義的方法提供了具體實現。接著,在 onBind() 方法裡我們將這個 BookManager.Stub 作為返回值傳了過去。

接著看看客戶端:

private BookManager mBookManager = null;

private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) 
        mBookManager = BookManager.Stub.asInterface(service);
        //省略
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
       //省略
    }
};

public void addBook(View view) {
   //省略
   mBookManager.addBook(book);
}

簡單的來說,客戶端就做了這些事:獲取 BookManager 物件,然後呼叫它裡面的方法。

現在結合服務端與客戶端做的事情,好好思考一下,我們會發現這樣一個怪事情:它們配合的如此緊密,以至於它們之間的互動竟像是同一個程式中的兩個類那麼自然!大家可以回想下平時專案裡的介面回撥,基本流程與此一般無二。明明是在兩個執行緒裡面,資料不能直接互通,何以他們能交流的如此愉快呢?答案在 BookManager.java 裡。

1.3、從客戶端開始

一點開 BookManager.java ,我發現的第一件事是:BookManager 是一個介面類!一看到它是個介面,我就知道,突破口有了。為什麼呢?介面意味著什麼?方法都沒有具體實現。但是明明在客戶端裡面我們呼叫了 mBookManager.addBook() !那麼就說明我們在客戶端裡面用到的 BookManager 絕不僅僅是 BookManager,而是它的一個實現類!那麼我們就可以從這個實現類入手,看看在我們的客戶端呼叫 addBook() 方法的時候,究竟 BookManager 在背後幫我們完成了哪些操作。首先看下客戶端的 BookManager 物件是怎麼來的:

public void onServiceConnected(ComponentName name, IBinder service) 
    mBookManager = BookManager.Stub.asInterface(service);
}

在這裡我首先注意到的是方法的傳參:IBinder service 。這是個什麼東西呢?通過除錯,我們可以發現,這是個 BinderProxy 物件。但隨後我們會驚訝的發現:Java中並沒有這個類!似乎研究就此陷入了僵局——其實不然。在這裡我們沒辦法進一步的探究下去,那我們就先把這個問題存疑,從後面它的一些應用來推測關於它的更多的東西。

接下來順藤摸瓜去看下這個 BookManager.Stub.asInterface() 是怎麼回事:

public static com.lypeer.ipcclient.BookManager asInterface(android.os.IBinder obj) {
    //驗空
    if ((obj == null)) {
        return null;
    }
    //DESCRIPTOR = "com.lypeer.ipcclient.BookManager",搜尋本地是否已經
    //有可用的物件了,如果有就將其返回
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.lypeer.ipcclient.BookManager))) {
        return ((com.lypeer.ipcclient.BookManager) iin);
    }
    //如果本地沒有的話就新建一個返回
    return new com.lypeer.ipcclient.BookManager.Stub.Proxy(obj);
}

方法裡首先進行了驗空,這個很正常。第二步操作是呼叫了 queryLocalInterface() 方法,這個方法是 IBinder 介面裡面的一個方法,而這裡傳進來的 IBinder 物件就是上文我們提到過的那個 service 物件。由於對 service 物件我們還沒有一個很清晰的認識,這裡也沒法深究這個queryLocalInterface() 方法:它是 IBinder 介面裡面的一個方法,那麼顯然,具體實現是在 service 的裡面的,我們無從窺探。但是望文生義我們也能體會到它的作用,這裡就姑且這麼理解吧。第三步是建立了一個物件返回——很顯然,這就是我們的目標,那個實現了 BookManager 介面的實現類。果斷去看這個 BookManager.Stub.Proxy 類:

private static class Proxy implements com.lypeer.ipcclient.BookManager {
    private android.os.IBinder mRemote;

    Proxy(android.os.IBinder remote) {
        //此處的 remote 正是前面我們提到的 IBinder service
        mRemote = remote;
    }

    @Override
    public java.util.List<com.lypeer.ipcclient.Book> getBooks() throws android.os.RemoteException {
        //省略
    }

    @Override
    public void addBook(com.lypeer.ipcclient.Book book) throws android.os.RemoteException {
        //省略
    }
    //省略部分方法
}

看到這裡,我們幾乎可以確定:Proxy 類確實是我們的目標,客戶端最終通過這個類與服務端進行通訊。

那麼接下來看看 getBooks() 方法裡面具體做了什麼:

@Override
public java.util.List<com.lypeer.ipcclient.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.lypeer.ipcclient.Book> _result;
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        //呼叫 transact() 方法將方法id和兩個 Parcel 容器傳過去
        mRemote.transact(Stub.TRANSACTION_getBooks, _data, _reply, 0);
        _reply.readException();
        //從_reply中取出服務端執行方法的結果
        _result = _reply.createTypedArrayList(com.lypeer.ipcclient.Book.CREATOR);
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    //將結果返回
    return _result;
}

在這段程式碼裡有幾個需要說明的地方,不然容易看得雲裡霧裡的:

  • 關於 _data 與 _reply 物件:一般來說,我們會將方法的傳參的資料存入_data 中,而將方法的返回值的資料存入 _reply 中——在沒涉及定向 tag 的情況下。如果涉及了定向 tag ,情況將會變得稍微複雜些,具體是怎麼回事請參見這篇博文:你真的理解AIDL中的in,out,inout麼?
  • 關於 Parcel :簡單的來說,Parcel 是一個用來存放和讀取資料的容器。我們可以用它來進行客戶端和服務端之間的資料傳輸,當然,它能傳輸的只能是可序列化的資料。具體 Parcel 的使用方法和相關原理可以參見這篇文章:Android中Parcel的分析以及使用
  • 關於 transact() 方法:這是客戶端和服務端通訊的核心方法。呼叫這個方法之後,客戶端將會掛起當前執行緒,等候服務端執行完相關任務後通知並接收返回的 _reply 資料流。關於這個方法的傳參,這裡有兩點需要說明的地方:
    • 方法 ID :transact() 方法的第一個引數是一個方法 ID ,這個是客戶端與服務端約定好的給方法的編碼,彼此一一對應。在AIDL檔案轉化為 .java 檔案的時候,系統將會自動給AIDL檔案裡面的每一個方法自動分配一個方法 ID。
    • 第四個引數:transact() 方法的第四個引數是一個 int 值,它的作用是設定進行 IPC 的模式,為 0 表示資料可以雙向流通,即 _reply 流可以正常的攜帶資料回來,如果為 1 的話那麼資料將只能單向流通,從服務端回來的 _reply 流將不攜帶任何資料。
      注:AIDL生成的 .java 檔案的這個引數均為 0。

上面的這些如果要去一步步探究出結果的話也不是不可以,但是那將會涉及到 Binder 機制裡比較底層的東西,一點點說完勢必會將文章的重心帶偏,那樣就不好了——所以我就直接以上帝視角把結論給出來了。

另外的那個 addBook() 方法我就不去分析了,殊途同歸,只是由於它涉及到了定向 tag ,所以有那麼一點點的不一樣,有興趣的讀者可以自己去試著閱讀一下。接下來我總結一下在 Proxy 類的方法裡面一般的工作流程:

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

縱觀客戶端的所有行為,我們不難發現,其實一開始我們不能理解的那個 IBinder service 恰恰是客戶端與服務端通訊的靈魂人物——正是通過用它呼叫的 transact() 方法,我們得以將客戶端的資料和請求傳送到服務端去。從這個角度來看,這個 service 就像是服務端在客戶端的代理一樣——你想要找服務端?要傳資料過去?行啊!你來找我,我給你把資料送過去——而 BookManager.java 中的那個 Proxy 類,就只能淪為二級代理了,我們在外部通過它來調動 service 物件。

至此,客戶端在 IPC 中進行的工作已經分析完了,接下來我們看一下服務端。

1.4,接著看服務端

前面說了客戶端通過呼叫 transact() 方法將資料和請求傳送過去,那麼理所當然的,服務端應當有一個方法來接收這些傳過來的東西:在 BookManager.java 裡面我們可以很輕易的找到一個叫做 onTransact() 的方法——看這名字就知道,多半和它脫不了關係,再一看它的傳參

(int code, android.os.Parcel data, android.os.Parcel reply, int flags) ——和 transact() 方法的傳參是一樣的!如果說他們沒有什麼 py 交易把我眼珠子挖出來當泡踩!下面來看看它是怎麼做的:

@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_getBooks: {
            //省略
            return true;
        }
        case TRANSACTION_addBook: {
            //省略
            return true;
        }
    }
    return super.onTransact(code, data, reply, flags);
}

可以看到,它在接收了客戶端的 transact() 方法傳過來的引數後,什麼廢話都沒說就直接進入了一個 switch 選擇:根據傳進來的方法 ID 不同執行不同的操作。接下來看一下每個方法裡面它具體做了些什麼,以 getBooks() 方法為例:

case TRANSACTION_getBooks: {
    data.enforceInterface(DESCRIPTOR);
    //呼叫 this.getBooks() 方法,在這裡開始執行具體的事務邏輯
    //result 列表為呼叫 getBooks() 方法的返回值
    java.util.List<com.lypeer.ipcclient.Book> _result = this.getBooks();
    reply.writeNoException();
    //將方法執行的結果寫入 reply ,
    reply.writeTypedList(_result);
    return true;
}

非常的簡單直了,直接呼叫服務端這邊的具體方法實現,然後獲取返回值並將其寫入 reply 流——當然,這是由於這個方法沒有傳入引數並且不涉及定向 tag 的關係,不然還會涉及到將傳入引數從 data 中讀取出來,以及針對定向 tag 的操作,具體的可以參考這篇博文:你真的理解AIDL中的in,out,inout麼?

另外,還有一個問題,有些讀者可能會疑惑,為什麼這裡沒有看到關於將 reply 回傳到客戶端的相關程式碼?事實上,在客戶端我們也沒有看到它將相關引數傳向服務端的相關程式碼——它只是把這些引數都傳入了一個方法,其中過程同樣是對我們隱藏的——服務端也同樣,在執行完 return true 之後系統將會把 reply 流傳回客戶端,具體是怎麼做的就不足為外人道也了。不知道大家發現了沒有,通過隱藏了這些細節,我們在 transact() 與 onTransact() 之間的呼叫以及資料傳送看起來就像是發生在同一個程式甚至同一個類裡面一樣。我們的操作就像是在一條直線上面走,根本感受不出來其中原來有過曲折——也許這套機制在設計之初,就是為了達到這樣的目的。

分析到這裡,服務端的工作我們也分析的差不多了,下面我們總結一下服務端的一般工作流程:

  • 1,獲取客戶端傳過來的資料,根據方法 ID 執行相應操作。
  • 2,將傳過來的資料取出來,呼叫本地寫好的對應方法。
  • 3,將需要回傳的資料寫入 reply 流,傳回客戶端。

1.5,總結

現在我們已經完成了 BookManager.java 幾乎所有的分析工作,接下來我想用兩張圖片來做一個總結。第一張是它的 UML 結構圖:

Android:學習AIDL,這一篇文章就夠了(下)

AIDL的結構

第二張是客戶端與服務端使用其進行 IPC 的工作流程:

Android:學習AIDL,這一篇文章就夠了(下)

AIDL的工作流程

剩下的就大家自己體味一下吧——如果前面的東西你看懂了,這裡有沒有我說的幾句總結都差不多;如果前面你看的似懂非懂,看看這兩張圖片也就懂了;如果前面你幾乎沒有看懂,那麼我寫幾句總結你還是看不懂。。。

2、為什麼要這樣設計?

這個問題可以拆分成兩個子問題:

  • 為什麼AIDL的語法要這樣設計?
  • 為什麼它生成的 .java 檔案的結構要這樣設計?

首先我有一個總的觀點:在程式設計領域,任何的解決方案,無非是基於需求和效能兩方面的考慮。首先是保證把需求完成,在這個大前提下保證效能最佳——這裡的效能,就包括了程式碼的健壯性,可維護性等等林林總總的東西。

關於AIDL的語法為什麼要這麼設計,其實沒有太大的研究的必要——因為他的語法實際上和 Java 沒有多大區別,區別的地方也很容易想通,多是因為一些很顯然的原因而不得不那樣做。接下來我主要分析一下 BookManager.java 的設計之道。首先我們要明確需求:

  • 基本需求當然是實現 IPC 。
  • 在此基礎上要儘可能的對開發者友好,即使用方便,且最好讓開發者有那種在同一個程式中呼叫方法傳輸資料的爽感。

既然要實現 IPC ,一些核心的要素就不能少,比如客戶端接收到的 IBinder service ,比如 transact() 方法,比如 onTransact() 方法——但是能讓開發者察覺到這些這些東西的存在甚至自己寫這些東西麼?不能。為什麼?因為這些東西做的事情其實非常的單調,無非就是那麼幾步,但是偏偏又涉及到很多對資料的寫入讀出的操作——涉及到資料流的東西一般都很繁瑣。把這些東西暴露出去顯然是不合適的,還是建立一套模板把它封裝起來比較的好。但是歸根結底,我們實現 IPC 是需要用到它們的,所以我們需要有一種途徑去訪問它們——在這個時候,代理-樁的設計理念就初步成型了。為了達到我們的目的,我們可以在客戶端建立一個服務端的代理,在服務端建立一個客戶端的樁,這樣一來,客戶端有什麼需求可以直接跟代理說,代理跟它說你等等,我馬上給你處理,然後它就告訴樁,客戶端有這個需求了,樁就馬上讓服務端開始執行相應的事件,在執行結束後再通過樁把結果告訴代理,代理最後把結果給客戶端。這樣一來,客戶端以為代理就是服務端,並且事實上它也只與代理進行了互動,而客戶端與代理是在同一個程式中的,在服務端那邊亦然——通過這種方式,我們就可以讓客戶端與服務端的通訊看上去簡單無比,像是從頭到尾我們都在一個程式中工作一樣。

在上面的設計思想指導之下,BookManager.java 為什麼是我們看到的這個樣子就很清楚明白了。

3、有沒有更好的方式來完成 IPC ?

首先我要闡述的觀點是:如果你對這篇文章中上面敘述的那些內容有一定的掌握與理解了的話,完全脫離AIDL來手動書寫客戶端與服務端的相關檔案來進行 IPC 是絕對沒有問題的。並且在瞭解了 IPC 得以進行的根本之後,你甚至完全沒有必要照著 BookManager.java 來寫,只要那幾個點在,你想怎麼寫就怎麼寫。

但是要說明的是,相較於使用AIDL來進行IPC,手動實現基本上是沒有什麼優勢的。畢竟AIDL是一門用來簡化我們的工作的語言,用它確實可以省很多事。

那麼現在除了AIDL與自己手動寫,有沒有其他的方式來進行 IPC 呢?答案是:有的。前段時間餓了麼(這不算打廣告吧。。。畢竟沒有利益相關,只是純粹的討論技術)的一個工程師開源了一套 IPC 的框架,地址在這裡:Hermes。這套框架的核心還是 IBinder service , transact() ,onTransact() 那些東西(事實上,任何和IPC有關的操作最終都還是要落在這些東西上面),但是他採取了一種巧妙的方式來實現:在服務端開啟了一條預設程式,讓這條程式來負責所有針對服務端的請求,同時採用註解的方式來註冊類和方法,使得客戶端能用這種形式和服務端建立約定,並且,這個框架對繫結service的那些細節隱藏的比較好,我們甚至都不需要在服務端寫service,在客戶端呼叫 bindService了——三管齊下,使得我們可以遠離以前那些煩人的有關service的操作了。但是也並不是說這套框架就完全超越了AIDL,在某些方面它也有一些不足。比如,不知道是他的那個 Readme 寫的太晦澀了還是怎麼回事,我覺得使用它需要付出的學習成本還是比較大的;另外,在這套框架裡面是將所有傳向服務端的資料都放在一個 Mail 類裡面的,而這個類的傳輸方式相當於AIDL裡面定向 tag 為 in 的情況——也就是說,不要再想像AIDL裡面那樣客戶端資料還能在服務端完成操作之後同步變化了。更多的東西我也還沒看出來,還沒用過這個框架,只是簡單的看了下它的原始碼,不過總的來說能過看出來的是作者寫的很用心,作者本身的Android功底也很強大,至少不知道比我強大到哪裡去了......另外,想微微的吐槽一下,為什麼這個框架用來進行IPC的核心類 IHermesService 裡面長得和AIDL生成的 .java 一模一樣啊一模一樣......

總之,我想說的就是,雖然已經有AIDL了,但是並不意味著就不會出現比它更好的實現了——不止在這裡是這樣,這個觀點可以推廣到所有領域。

結語

這篇文章說是學習AIDL的,其實大部分的內容都是在通過AIDL生成的那個.java 檔案講 IPC 相關的知識——其實也就是 Binder 機制的利用的一部分——這也是為什麼文中其實有很多地方沒有深入下去講,而是匆匆忙忙的給出了結論,因為再往下就不是應用層的東西了,講起來比較麻煩,而且容易把人看煩。

講到這裡,基本上關於Android裡面 IPC 相關的東西都已經講得差不多了,如果你是從我寫的Android中的Service:默默的奉獻者 (1) --> Android中的Service:Binder,Messenger,AIDL(2) --> Android:學習AIDL,這一篇文章就夠了(上) --> 現在這篇,這樣一路看下來,並且是認真的看下來的話,基本上這一塊的問題都難不倒你了。

另外,除了知識,我更希望通過我的博文傳遞的是一些解決問題分析問題的思路或者說是方法,所以我的很多博文都重在敘述思考過程而不是闡述結果——這樣有好處也有壞處,好處是如果看懂了,能夠收穫更多,壞處是,大部分人都沒有那個耐性慢慢的來看懂它,畢竟這需要思考,而當前很多的人都已經沒有思考的時間,甚至喪失思考的能力了。

謝謝大家。

另:關於脫離AIDL自己寫IPC的程式碼,我自己寫了一份,大家可以聊作參考,傳送門

相關文章