原文地址:zhuanlan.zhihu.com/p/35519585java
原文做者:張磊程序員
Binder 之複雜遠遠不是一篇文章就能說清楚的,本文想站在一個更高的維度來俯瞰 Binder 的設計,最終幫助你們造成一個完整的概念。對於應用層開發的同窗來講,理解到本文這個程度也就差很少了。面試
簡單介紹下什麼是 Binder。Binder 是一種進程間通訊機制,基於開源的 OpenBinder 實現;OpenBinder 起初由 Be Inc. 開發,後由 Plam Inc. 接手。從字面上來解釋 Binder 有膠水、粘合劑的意思,顧名思義就是粘和不一樣的進程,使之實現通訊。對於 Binder 更全面的定義,等咱們介紹完 Binder 通訊原理後再作詳細說明。編程
做爲 Android 工程師的你,是否是經常會有這樣的疑問:瀏覽器
這些問題的背後都與 Binder 有莫大的關係,要弄懂上面這些問題理解 Bidner 通訊機制是必須的。緩存
咱們知道 Android 應用程序是由 Activity、Service、Broadcast Receiver 和 Content Provide 四大組件中的一個或者多個組成的。有時這些組件運行在同一進程,有時運行在不一樣的進程。這些進程間的通訊就依賴於 Binder IPC 機制。不只如此,Android 系統對應用層提供的各類服務如:ActivityManagerService、PackageManagerService 等都是基於 Binder IPC 機制來實現的。Binder 機制在 Android 中的位置很是重要,絕不誇張的說理解 Binder 是邁向 Android 高級工程的第一步。安全
Android 系統是基於 Linux 內核的,Linux 已經提供了管道、消息隊列、共享內存和 Socket 等 IPC 機制。那爲何 Android 還要提供 Binder 來實現 IPC 呢?主要是基於性能、穩定性和安全性幾方面的緣由。服務器
首先說說性能上的優點。Socket 做爲一款通用接口,其傳輸效率低,開銷大,主要用在跨網絡的進程間通訊和本機上進程間的低速通訊。消息隊列和管道採用存儲-轉發方式,即數據先從發送方緩存區拷貝到內核開闢的緩存區中,而後再從內核緩存區拷貝到接收方緩存區,至少有兩次拷貝過程。共享內存雖然無需拷貝,但控制複雜,難以使用。Binder 只須要一次數據拷貝,性能上僅次於共享內存。 注:各類IPC方式數據拷貝次數,此表來源於Android Binder 設計與實現 - 設計篇markdown
IPC方式 | 數據拷貝次數 |
---|---|
共享內存 | 0 |
Binder | 1 |
Socket/管道/消息隊列 | 2 |
再說說穩定性,Binder 基於 C/S 架構,客戶端(Client)有什麼需求就丟給服務端(Server)去完成,架構清晰、職責明確又相互獨立,天然穩定性更好。共享內存雖然無需拷貝,可是控制負責,難以使用。從穩定性的角度講,Binder 機制是優於內存共享的。網絡
另外一方面就是安全性。Android 做爲一個開放性的平臺,市場上有各種海量的應用供用戶選擇安裝,所以安全性對於 Android 平臺而言極其重要。做爲用戶固然不但願咱們下載的 APP 偷偷讀取個人通訊錄,上傳個人隱私數據,後臺偷跑流量、消耗手機電量。傳統的 IPC 沒有任何安全措施,徹底依賴上層協議來確保。首先傳統的 IPC 接收方沒法得到對方可靠的進程用戶ID/進程ID(UID/PID),從而沒法鑑別對方身份。Android 爲每一個安裝好的 APP 分配了本身的 UID,故而進程的 UID 是鑑別進程身份的重要標誌。傳統的 IPC 只能由用戶在數據包中填入 UID/PID,但這樣不可靠,容易被惡意程序利用。可靠的身份標識只有由 IPC 機制在內核中添加。其次傳統的 IPC 訪問接入點是開放的,只要知道這些接入點的程序均可以和對端創建鏈接,無論怎樣都沒法阻止惡意程序經過猜想接收方地址得到鏈接。同時 Binder 既支持實名 Binder,又支持匿名 Binder,安全性高。
基於上述緣由,Android 須要創建一套新的 IPC 機制來知足系統對穩定性、傳輸性能和安全性方面的要求,這就是 Binder。
最後用一張表格來總結下 Binder 的優點:
優點 | 描述 |
---|---|
性能 | 只須要一次數據拷貝,性能上僅次於共享內存 |
穩定性 | 基於 C/S 架構,職責明確、架構清晰,所以穩定性好 |
安全性 | 爲每一個 APP 分配 UID,進程的 UID 是鑑別進程身份的重要標誌 |
瞭解 Linux IPC 相關的概念和原理有助於咱們理解 Binder 通訊原理。所以,在介紹 Binder 跨進程通訊原理以前,咱們先聊聊 Linux 系統下傳統的進程間通訊是如何實現。
這裏咱們先從 Linux 中進程間通訊涉及的一些基本概念開始介紹,而後逐步展開,向你們說明傳統的進程間通訊的原理。
Liunx 中跨進程通訊涉及到的基本概念:
簡單的說就是操做系統中,進程與進程間內存是不共享的。兩個進程就像兩個平行的世界,A 進程無法直接訪問 B 進程的數據,這就是進程隔離的通俗解釋。A 進程和 B 進程之間要進行數據交互就得采用特殊的通訊機制:進程間通訊(IPC)。
如今操做系統都是採用的虛擬存儲器,對於 32 位系統而言,它的尋址空間(虛擬存儲空間)就是 2 的 32 次方,也就是 4GB。操做系統的核心是內核,獨立於普通的應用程序,能夠訪問受保護的內存空間,也能夠訪問底層硬件設備的權限。爲了保護用戶進程不能直接操做內核,保證內核的安全,操做系統從邏輯上將虛擬空間劃分爲用戶空間(User Space)和內核空間(Kernel Space)。針對 Linux 操做系統而言,將最高的 1GB 字節供內核使用,稱爲內核空間;較低的 3GB 字節供各進程使用,稱爲用戶空間。
簡單的說就是,內核空間(Kernel)是系統內核運行的空間,用戶空間(User Space)是用戶程序運行的空間。爲了保證安全性,它們之間是隔離的。
雖然從邏輯上進行了用戶空間和內核空間的劃分,但不可避免的用戶空間須要訪問內核資源,好比文件操做、訪問網絡等等。爲了突破隔離限制,就須要藉助系統調用來實現。系統調用是用戶空間訪問內核空間的惟一方式,保證了全部的資源訪問都是在內核的控制下進行的,避免了用戶程序對系統資源的越權訪問,提高了系統安全性和穩定性。
Linux 使用兩級保護機制:0 級供系統內核使用,3 級供用戶程序使用。
當一個任務(進程)執行系統調用而陷入內核代碼中執行時,稱進程處於內核運行態(內核態) 。此時處理器處於特權級最高的(0級)內核代碼中執行。當進程處於內核態時,執行的內核代碼會使用當前進程的內核棧。每一個進程都有本身的內核棧。
當進程在執行用戶本身的代碼的時候,咱們稱其處於用戶運行態(用戶態) 。此時處理器在特權級最低的(3級)用戶代碼中運行。
系統調用主要經過以下兩個函數來實現:
copy_from_user() //將數據從用戶空間拷貝到內核空間
copy_to_user() //將數據從內核空間拷貝到用戶空間
複製代碼
理解了上面的幾個概念,咱們再來看看傳統的 IPC 方式中,進程之間是如何實現通訊的。
一般的作法是消息發送方將要發送的數據存放在內存緩存區中,經過系統調用進入內核態。而後內核程序在內核空間分配內存,開闢一塊內核緩存區,調用 copy_from_user() 函數將數據從用戶空間的內存緩存區拷貝到內核空間的內核緩存區中。一樣的,接收方進程在接收數據時在本身的用戶空間開闢一塊內存緩存區,而後內核程序調用 copy_to_user() 函數將數據從內核緩存區拷貝到接收進程的內存緩存區。這樣數據發送方進程和數據接收方進程就完成了一次數據傳輸,咱們稱完成了一次進程間通訊。
這種傳統的 IPC 通訊方式有兩個問題:
性能低下,一次數據傳遞須要經歷:內存緩存區 --> 內核緩存區 --> 內存緩存區,須要 2 次數據拷貝;
接收數據的緩存區由數據接收進程提供,可是接收進程並不知道須要多大的空間來存放將要傳遞過來的數據,所以只能開闢儘量大的內存空間或者先調用 API 接收消息頭來獲取消息體的大小,這兩種作法不是浪費空間就是浪費時間。
理解了 Linux IPC 相關概念和通訊原理,接下來咱們正式介紹下 Binder IPC 的原理。
正如前面所說,跨進程通訊是須要內核空間作支持的。傳統的 IPC 機制如管道、Socket 都是內核的一部分,所以經過內核支持來實現進程間通訊天然是沒問題的。可是 Binder 並非 Linux 系統內核的一部分,那怎麼辦呢?這就得益於 Linux 的動態內核可加載模塊(Loadable Kernel Module,LKM)的機制;模塊是具備獨立功能的程序,它能夠被單獨編譯,可是不能獨立運行。它在運行時被連接到內核做爲內核的一部分運行。這樣,Android 系統就能夠經過動態添加一個內核模塊運行在內核空間,用戶進程之間經過這個內核模塊做爲橋樑來實現通訊。
在 Android 系統中,這個運行在內核空間,負責各個用戶進程經過 Binder 實現通訊的內核模塊就叫 Binder 驅動(Binder Dirver)。
那麼在 Android 系統中用戶進程之間是如何經過這個內核模塊(Binder 驅動)來實現通訊的呢?難道是和前面說的傳統 IPC 機制同樣,先將數據從發送方進程拷貝到內核緩存區,而後再將數據從內核緩存區拷貝到接收方進程,經過兩次拷貝來實現嗎?顯然不是,不然也不會有開篇所說的 Binder 在性能方面的優點了。
這就不得不通道 Linux 下的另外一個概念:內存映射。
Binder IPC 機制中涉及到的內存映射經過 mmap() 來實現,mmap() 是操做系統中一種內存映射的方法。內存映射簡單的講就是將用戶空間的一塊內存區域映射到內核空間。映射關係創建後,用戶對這塊內存區域的修改能夠直接反應到內核空間;反以內核空間對這段區域的修改也能直接反應到用戶空間。
內存映射能減小數據拷貝次數,實現用戶空間和內核空間的高效互動。兩個空間各自的修改能直接反映在映射的內存區域,從而被對方空間及時感知。也正由於如此,內存映射可以提供對進程間通訊的支持。
Binder IPC 正是基於內存映射(mmap)來實現的,可是 mmap() 一般是用在有物理介質的文件系統上的。
好比進程中的用戶區域是不能直接和物理設備打交道的,若是想要把磁盤上的數據讀取到進程的用戶區域,須要兩次拷貝(磁盤-->內核空間-->用戶空間);一般在這種場景下 mmap() 就能發揮做用,經過在物理介質和用戶空間之間創建映射,減小數據的拷貝次數,用內存讀寫取代I/O讀寫,提升文件讀取效率。
而 Binder 並不存在物理介質,所以 Binder 驅動使用 mmap() 並非爲了在物理介質和用戶空間之間創建映射,而是用來在內核空間建立數據接收的緩存空間。
一次完整的 Binder IPC 通訊過程一般是這樣:
介紹完 Binder IPC 的底層通訊原理,接下來咱們看看實現層面是如何設計的。
一次完整的進程間通訊必然至少包含兩個進程,一般咱們稱通訊的雙方分別爲客戶端進程(Client)和服務端進程(Server),因爲進程隔離機制的存在,通訊雙方必然須要藉助 Binder 來實現。
前面咱們介紹過,Binder 是基於 C/S 架構的。由一系列的組件組成,包括 Client、Server、ServiceManager、Binder 驅動。其中 Client、Server、Service Manager 運行在用戶空間,Binder 驅動運行在內核空間。其中 Service Manager 和 Binder 驅動由系統提供,而 Client、Server 由應用程序來實現。Client、Server 和 ServiceManager 均是經過系統調用 open、mmap 和 ioctl 來訪問設備文件 /dev/binder,從而實現與 Binder 驅動的交互來間接的實現跨進程通訊。
Client、Server、ServiceManager、Binder 驅動這幾個組件在通訊過程當中扮演的角色就如同互聯網中服務器(Server)、客戶端(Client)、DNS域名服務器(ServiceManager)以及路由器(Binder 驅動)以前的關係。
一般咱們訪問一個網頁的步驟是這樣的:首先在瀏覽器輸入一個地址,如 www.google.com 而後按下回車鍵。可是並無辦法經過域名地址直接找到咱們要訪問的服務器,所以須要首先訪問 DNS 域名服務器,域名服務器中保存了 www.google.com 對應的 ip 地址 10.249.23.13,而後經過這個 ip 地址才能放到到 www.google.com 對應的服務器。
咱們已經解釋清楚 Client、Server 藉助 Binder 驅動完成跨進程通訊的實現機制了,可是還有個問題會讓咱們困惑。A 進程想要 B 進程中某個對象(object)是如何實現的呢?畢竟它們分屬不一樣的進程,A 進程 無法直接使用 B 進程中的 object。
前面咱們介紹過跨進程通訊的過程都有 Binder 驅動的參與,所以在數據流經 Binder 驅動的時候驅動會對數據作一層轉換。當 A 進程想要獲取 B 進程中的 object 時,驅動並不會真的把 object 返回給 A,而是返回了一個跟 object 看起來如出一轍的代理對象 objectProxy,這個 objectProxy 具備和 object 一摸同樣的方法,可是這些方法並無 B 進程中 object 對象那些方法的能力,這些方法只須要把把請求參數交給驅動便可。對於 A 進程來講和直接調用 object 中的方法是同樣的。
當 Binder 驅動接收到 A 進程的消息後,發現這是個 objectProxy 就去查詢本身維護的表單,一查發現這是 B 進程 object 的代理對象。因而就會去通知 B 進程調用 object 的方法,並要求 B 進程把返回結果發給本身。當驅動拿到 B 進程的返回結果後就會轉發給 A 進程,一次通訊就完成了。
如今咱們能夠對 Binder 作個更加全面的定義了:
一般咱們在作開發時,實現進程間通訊用的最多的就是 AIDL。當咱們定義好 AIDL 文件,在編譯時編譯器會幫咱們生成代碼實現 IPC 通訊。藉助 AIDL 編譯之後的代碼能幫助咱們進一步理解 Binder IPC 的通訊原理。
可是不管是從可讀性仍是可理解性上來看,編譯器生成的代碼對開發者並不友好。好比一個 BookManager.aidl 文件對應會生成一個 BookManager.java 文件,這個 java 文件包含了一個 BookManager 接口、一個 Stub 靜態的抽象類和一個 Proxy 靜態類。Proxy 是 Stub 的靜態內部類,Stub 又是 BookManager 的靜態內部類,這就形成了可讀性和可理解性的問題。
Android 之因此這樣設計實際上是有道理的,由於當有多個 AIDL 文件的時候把 BookManager、Stub、Proxy 放在同一個文件裏能有效避免 Stub 和 Proxy 重名的問題。
所以便於你們理解,下面咱們來手動編寫代碼來實現跨進程調用。
在正式編碼實現跨進程調用以前,先介紹下實現過程當中用到的一些類。瞭解了這些類的職責,有助於咱們更好的理解和實現跨進程通訊。
一次跨進程通訊必然會涉及到兩個進程,在這個例子中 RemoteService 做爲服務端進程,提供服務;ClientActivity 做爲客戶端進程,使用 RemoteService 提供的服務。以下圖:
那麼服務端進程具有什麼樣的能力?能爲客戶端提供什麼樣的服務呢?還記得咱們前面介紹過的 IInterface 嗎,它表明的就是服務端進程具體什麼樣的能力。所以咱們須要定義一個 BookManager 接口,BookManager 繼承自 IIterface,代表服務端具有什麼樣的能力。
/**
* 這個類用來定義服務端 RemoteService 具有什麼樣的能力
*/
public interface BookManager extends IInterface {
void addBook(Book book) throws RemoteException;
}
複製代碼
只定義服務端具有什麼要的能力是不夠的,既然是跨進程調用,那麼接下來咱們得實現一個跨進程調用對象 Stub。Stub 繼承 Binder, 說明它是一個 Binder 本地對象;實現 IInterface 接口,代表具備 Server 承諾給 Client 的能力;Stub 是一個抽象類,具體的 IInterface 的相關實現須要調用方本身實現。
public abstract class Stub extends Binder implements BookManager {
public static BookManager asInterface(IBinder binder) {
if (binder == null)
return null;
IInterface iin = binder.queryLocalInterface(DESCRIPTOR);
if (iin != null && iin instanceof BookManager)
return (BookManager) iin;
return new Proxy(binder);
}
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
switch (code) {
case INTERFACE_TRANSACTION:
reply.writeString(DESCRIPTOR);
return true;
case TRANSAVTION_addBook:
data.enforceInterface(DESCRIPTOR);
Book arg0 = null;
if (data.readInt() != 0) {
arg0 = Book.CREATOR.createFromParcel(data);
}
this.addBook(arg0);
reply.writeNoException();
return true;
}
return super.onTransact(code, data, reply, flags);
}
}
複製代碼
Stub 類中咱們重點介紹下 asInterface
和 onTransact
。
先說說 asInterface
,當 Client 端在建立和服務端的鏈接,調用 bindService 時須要建立一個 ServiceConnection 對象做爲入參。在 ServiceConnection 的回調方法 onServiceConnected 中 會經過這個 asInterface(IBinder binder) 拿到 BookManager 對象,這個 IBinder 類型的入參 binder 是驅動傳給咱們的,正如你在代碼中看到的同樣,方法中會去調用 binder.queryLocalInterface() 去查找 Binder 本地對象,若是找到了就說明 Client 和 Server 在同一進程,那麼這個 binder 自己就是 Binder 本地對象,能夠直接使用。不然說明是 binder 是個遠程對象,也就是 BinderProxy。所以須要咱們建立一個代理對象 Proxy,經過這個代理對象來是實現遠程訪問。
接下來咱們就要實現這個代理類 Proxy 了,既然是代理類天然須要實現 BookManager 接口。
public class Proxy implements BookManager {
...
public Proxy(IBinder remote) {
this.remote = remote;
}
@Override
public void addBook(Book book) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel replay = Parcel.obtain();
try {
data.writeInterfaceToken(DESCRIPTOR);
if (book != null) {
data.writeInt(1);
book.writeToParcel(data, 0);
} else {
data.writeInt(0);
}
remote.transact(Stub.TRANSAVTION_addBook, data, replay, 0);
replay.readException();
} finally {
replay.recycle();
data.recycle();
}
}
}
複製代碼
咱們看看 addBook() 的實現;在 Stub 類中,addBook(Book book) 是一個抽象方法,Client 端須要繼承並實現它。
在 Proxy 中的 addBook() 方法中首先經過 Parcel 將數據序列化,而後調用 remote.transact()。正如前文所述 Proxy 是在 Stub 的 asInterface 中建立,能走到建立 Proxy 這一步就說明 Proxy 構造函數的入參是 BinderProxy,即這裏的 remote 是個 BinderProxy 對象。最終經過一系列的函數調用,Client 進程經過系統調用陷入內核態,Client 進程中執行 addBook() 的線程掛起等待返回;驅動完成一系列的操做以後喚醒 Server 進程,調用 Server 進程本地對象的 onTransact()。最終又走到了 Stub 中的 onTransact() 中,onTransact() 根據函數編號調用相關函數(在 Stub 類中爲 BookManager 接口中的每一個函數中定義了一個編號,只不過上面的源碼中咱們簡化掉了;在跨進程調用的時候,不會傳遞函數而是傳遞編號來指明要調用哪一個函數);咱們這個例子裏面,調用了 Binder 本地對象的 addBook() 並將結果返回給驅動,驅動喚醒 Client 進程裏剛剛掛起的線程並將結果返回。
這樣一次跨進程調用就完成了。
最後建議你們在不借助 AIDL 的狀況下手寫實現 Client 和 Server 進程的通訊,加深對 Binder 通訊過程的理解。
受我的能力水平限制,文章中不免會有錯誤。若是你們發現文章不足之處,歡迎與我溝通交流。
公衆號:程序員喵大人(專一於Android各種學習筆記、面試題以及IT類資訊的分享。)