1. 程式人生 > >Android:學習AIDL,這一篇文章就夠了(下)

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

前言

上一篇博文介紹了關於AIDL是什麼,為什麼我們需要AIDL,AIDL的語法以及如何使用AIDL等方面的知識,這一篇博文將順著上一篇的思路往下走,接著介紹關於AIDL的一些更加深入的知識。強烈建議大家在看這篇博文之前先看一下上一篇博文:

注:文中所有程式碼均源自上一篇博文中的例子。
另:在看這篇博文之前,建議先將上一篇博文中的程式碼下載下來或者敲一遍,然後確定可以正常執行後再接著看。因為文中有大量對於具體程式碼的分析以及相關程式碼片段之間的跳轉,如果你手頭沒有一份完整程式碼的話很容易看得一頭霧水,最後浪費了你的時間也浪費了這篇博文。

正文

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

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

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

1.1,這個檔案在哪兒?

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

它在這兒

它的完整路徑是: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 結構圖:

AIDL的結構

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

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 的框架,地址在這裡:。這套框架的核心還是 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 機制的利用的一部分——這也是為什麼文中其實有很多地方沒有深入下去講,而是匆匆忙忙的給出了結論,因為再往下就不是應用層的東西了,講起來比較麻煩,而且容易把人看煩。

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

謝謝大家。

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