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

前言

上一篇博文介紹了關於AIDL是什麼,爲何咱們須要AIDL,AIDL的語法以及如何使用AIDL等方面的知識,這一篇博文將順着上一篇的思路往下走,接着介紹關於AIDL的一些更加深刻的知識。強烈建議你們在看這篇博文以前先看一下上一篇博文:Android:學習AIDL,這一篇文章就夠了(上)java

注:文中全部代碼均源自上一篇博文中的例子。 
另:在看這篇博文以前,建議先將上一篇博文中的代碼下載下來或者敲一遍,而後肯定能夠正常運行後再接着看。由於文中有大量對於具體代碼的分析以及相關代碼片斷之間的跳轉,若是你手頭沒有一份完整代碼的話很容易看得一頭霧水,最後浪費了你的時間也浪費了這篇博文。android

正文

1,源碼分析:AIDL文件是怎麼工做的?

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

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

1.1,這個文件在哪兒?

要研究它,首先咱們就須要找到它,那麼它在哪兒呢?在這裏:app

它在這兒

它的完整路徑是:app->build->generated->source->aidl->debug->com->lypeer->ipcclient->BookManager.java(其中com.lypeer.ipcclient 是包名,相對應的AIDL文件爲 BookManager.aidl )。在Android Studio裏面目錄組織方式由默認的 Android 改成 Project 就能夠直接按照文件夾結構訪問到它。框架

1.2,從應用看原理

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

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 的框架,地址在這裏: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的代碼,我本身寫了一份,你們能夠聊做參考,傳送門

相關文章
相關標籤/搜索