天天看點

Android AIDL源碼分析

前言

上一篇博文介紹了關于AIDL是什麼,為什麼我們需要AIDL,AIDL的文法以及如何使用AIDL等方面的知識,這一篇博文将順着上一篇的思路往下走,接着介紹關于AIDL的一些更加深入的知識。強烈建議大家在看這篇博文之前先看一下上一篇博文:Android:學習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, );
        _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,這一篇文章就夠了(上) --> 現在這篇,這樣一路看下來,并且是認真的看下來的話,基本上這一塊的問題都難不倒你了。

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

謝謝大家。

轉載位址:http://www.open-open.com/lib/view/open1469494852171.html