本文來自網(wǎng)絡(luò),也可能略有改動,如有任何不妥可以聯(lián)系刪除,原文地址 http://www.itdecent.cn/p/429a1ff3560c
一. 前言
這篇文章我醞釀了很久,參考了很多資料,讀了很多源碼,卻依舊不敢下筆。生怕自己理解上還有偏差,對大家造成誤解,貽笑大方。又怕自己理解不夠透徹,無法用清晰直白的文字準確的表達出 Binder 的設(shè)計精髓。直到今天提筆寫作時還依舊戰(zhàn)戰(zhàn)兢兢。
Binder 之復(fù)雜遠遠不是一篇文章就能說清楚的,本文想站在一個更高的維度來俯瞰 Binder 的設(shè)計,最終幫助大家形成一個完整的概念。對于應(yīng)用層開發(fā)的同學(xué)來說,理解到本文這個程度也就差不多了。希望更加深入理解 Binder 實現(xiàn)機制的,可以閱讀文末的參考資料以及相關(guān)源碼。
二. Binder 概述
簡單介紹下什么是 Binder。Binder 是一種進程間通信機制,基于開源的 OpenBinder 實現(xiàn);OpenBinder 起初由 Be Inc. 開發(fā),后由 Plam Inc. 接手。從字面上來解釋 Binder 有膠水、粘合劑的意思,顧名思義就是粘和不同的進程,使之實現(xiàn)通信。對于 Binder 更全面的定義,等我們介紹完 Binder 通信原理后再做詳細說明。
2.1 為什么必須理解 Binder ?
作為 Android 工程師的你,是不是常常會有這樣的疑問:
- 為什么 Activity 間傳遞對象需要序列化?
- Activity 的啟動流程是什么樣的?
- 四大組件底層的通信機制是怎樣的?
- AIDL 內(nèi)部的實現(xiàn)原理是什么?
- 插件化編程技術(shù)應(yīng)該從何學(xué)起?等等...
這些問題的背后都與 Binder 有莫大的關(guān)系,要弄懂上面這些問題理解 Bidner 通信機制是必須的。
我們知道 Android 應(yīng)用程序是由 Activity、Service、Broadcast Receiver 和 Content Provide 四大組件中的一個或者多個組成的。有時這些組件運行在同一進程,有時運行在不同的進程。這些進程間的通信就依賴于 Binder IPC 機制。不僅如此,Android 系統(tǒng)對應(yīng)用層提供的各種服務(wù)如:ActivityManagerService、PackageManagerService 等都是基于 Binder IPC 機制來實現(xiàn)的。Binder 機制在 Android 中的位置非常重要,毫不夸張的說理解 Binder 是邁向 Android 高級工程的第一步。
2.2 為什么是 Binder ?
Android 系統(tǒng)是基于 Linux 內(nèi)核的,Linux 已經(jīng)提供了管道、消息隊列、共享內(nèi)存和 Socket 等 IPC 機制。那為什么 Android 還要提供 Binder 來實現(xiàn) IPC 呢?主要是基于性能、穩(wěn)定性和安全性幾方面的原因。
性能
首先說說性能上的優(yōu)勢。Socket 作為一款通用接口,其傳輸效率低,開銷大,主要用在跨網(wǎng)絡(luò)的進程間通信和本機上進程間的低速通信。消息隊列和管道采用存儲 - 轉(zhuǎn)發(fā)方式,即數(shù)據(jù)先從發(fā)送方緩存區(qū)拷貝到內(nèi)核開辟的緩存區(qū)中,然后再從內(nèi)核緩存區(qū)拷貝到接收方緩存區(qū),至少有兩次拷貝過程。共享內(nèi)存雖然無需拷貝,但控制復(fù)雜,難以使用。Binder 只需要一次數(shù)據(jù)拷貝,性能上僅次于共享內(nèi)存。
注:各種 IPC 方式數(shù)據(jù)拷貝次數(shù),此表來源于 Android Binder 設(shè)計與實現(xiàn) - 設(shè)計篇
| IPC 方式 | 數(shù)據(jù)拷貝次數(shù) |
|---|---|
| 共享內(nèi)存 | 0 |
| Binder | 1 |
| Socket / 管道 / 消息隊列 | 2 |
穩(wěn)定性
再說說穩(wěn)定性,Binder 基于 C/S 架構(gòu),客戶端(Client)有什么需求就丟給服務(wù)端(Server)去完成,架構(gòu)清晰、職責(zé)明確又相互獨立,自然穩(wěn)定性更好。共享內(nèi)存雖然無需拷貝,但是控制負責(zé),難以使用。從穩(wěn)定性的角度講,Binder 機制是優(yōu)于內(nèi)存共享的。
安全性
另一方面就是安全性。Android 作為一個開放性的平臺,市場上有各類海量的應(yīng)用供用戶選擇安裝,因此安全性對于 Android 平臺而言極其重要。作為用戶當然不希望我們下載的 APP 偷偷讀取我的通信錄,上傳我的隱私數(shù)據(jù),后臺偷跑流量、消耗手機電量。傳統(tǒng)的 IPC 沒有任何安全措施,完全依賴上層協(xié)議來確保。首先傳統(tǒng)的 IPC 接收方無法獲得對方可靠的進程用戶 ID / 進程 ID(UID/PID),從而無法鑒別對方身份。Android 為每個安裝好的 APP 分配了自己的 UID,故而進程的 UID 是鑒別進程身份的重要標志。傳統(tǒng)的 IPC 只能由用戶在數(shù)據(jù)包中填入 UID/PID,但這樣不可靠,容易被惡意程序利用??煽康纳矸輼俗R只有由 IPC 機制在內(nèi)核中添加。其次傳統(tǒng)的 IPC 訪問接入點是開放的,只要知道這些接入點的程序都可以和對端建立連接,不管怎樣都無法阻止惡意程序通過猜測接收方地址獲得連接。同時 Binder 既支持實名 Binder,又支持匿名 Binder,安全性高。
基于上述原因,Android 需要建立一套新的 IPC 機制來滿足系統(tǒng)對穩(wěn)定性、傳輸性能和安全性方面的要求,這就是 Binder。
最后用一張表格來總結(jié)下 Binder 的優(yōu)勢:
| 優(yōu)勢 | 描述 |
|---|---|
| 性能 | 只需要一次數(shù)據(jù)拷貝,性能上僅次于共享內(nèi)存 |
| 穩(wěn)定性 | 基于 C/S 架構(gòu),職責(zé)明確、架構(gòu)清晰,因此穩(wěn)定性好 |
| 安全性 | 為每個 APP 分配 UID,進程的 UID 是鑒別進程身份的重要標志 |
三. Linux 下傳統(tǒng)的進程間通信原理
了解 Linux IPC 相關(guān)的概念和原理有助于我們理解 Binder 通信原理。因此,在介紹 Binder 跨進程通信原理之前,我們先聊聊 Linux 系統(tǒng)下傳統(tǒng)的進程間通信是如何實現(xiàn)。
3.1 基本概念介紹
這里我們先從 Linux 中進程間通信涉及的一些基本概念開始介紹,然后逐步展開,向大家說明傳統(tǒng)的進程間通信的原理。

Linux 背景知識
上圖展示了 Liunx 中跨進程通信涉及到的一些基本概念:
- 進程隔離
- 進程空間劃分:用戶空間 (User Space)/ 內(nèi)核空間 (Kernel Space)
- 系統(tǒng)調(diào)用:用戶態(tài) / 內(nèi)核態(tài)
進程隔離
簡單的說就是操作系統(tǒng)中,進程與進程間內(nèi)存是不共享的。兩個進程就像兩個平行的世界,A 進程沒法直接訪問 B 進程的數(shù)據(jù),這就是進程隔離的通俗解釋。A 進程和 B 進程之間要進行數(shù)據(jù)交互就得采用特殊的通信機制:進程間通信(IPC)。
進程空間劃分:用戶空間 (User Space)/ 內(nèi)核空間 (Kernel Space)
現(xiàn)在操作系統(tǒng)都是采用的虛擬存儲器,對于 32 位系統(tǒng)而言,它的尋址空間(虛擬存儲空間)就是 2 的 32 次方,也就是 4GB。操作系統(tǒng)的核心是內(nèi)核,獨立于普通的應(yīng)用程序,可以訪問受保護的內(nèi)存空間,也可以訪問底層硬件設(shè)備的權(quán)限。為了保護用戶進程不能直接操作內(nèi)核,保證內(nèi)核的安全,操作系統(tǒng)從邏輯上將虛擬空間劃分為用戶空間(User Space)和內(nèi)核空間(Kernel Space)。針對 Linux 操作系統(tǒng)而言,將最高的 1GB 字節(jié)供內(nèi)核使用,稱為內(nèi)核空間;較低的 3GB 字節(jié)供各進程使用,稱為用戶空間。
簡單的說就是,內(nèi)核空間(Kernel)是系統(tǒng)內(nèi)核運行的空間,用戶空間(User Space)是用戶程序運行的空間。為了保證安全性,它們之間是隔離的。

圖片來自網(wǎng)絡(luò)
系統(tǒng)調(diào)用:用戶態(tài)與內(nèi)核態(tài)
雖然從邏輯上進行了用戶空間和內(nèi)核空間的劃分,但不可避免的用戶空間需要訪問內(nèi)核資源,比如文件操作、訪問網(wǎng)絡(luò)等等。為了突破隔離限制,就需要借助系統(tǒng)調(diào)用來實現(xiàn)。系統(tǒng)調(diào)用是用戶空間訪問內(nèi)核空間的唯一方式,保證了所有的資源訪問都是在內(nèi)核的控制下進行的,避免了用戶程序?qū)ο到y(tǒng)資源的越權(quán)訪問,提升了系統(tǒng)安全性和穩(wěn)定性。
Linux 使用兩級保護機制:0 級供系統(tǒng)內(nèi)核使用,3 級供用戶程序使用。
當一個任務(wù)(進程)執(zhí)行系統(tǒng)調(diào)用而陷入內(nèi)核代碼中執(zhí)行時,稱進程處于內(nèi)核運行態(tài)(內(nèi)核態(tài))。此時處理器處于特權(quán)級最高的(0 級)內(nèi)核代碼中執(zhí)行。當進程處于內(nèi)核態(tài)時,執(zhí)行的內(nèi)核代碼會使用當前進程的內(nèi)核棧。每個進程都有自己的內(nèi)核棧。
當進程在執(zhí)行用戶自己的代碼的時候,我們稱其處于用戶運行態(tài)(用戶態(tài))。此時處理器在特權(quán)級最低的(3 級)用戶代碼中運行。
系統(tǒng)調(diào)用主要通過如下兩個函數(shù)來實現(xiàn):
copy_from_user()
copy_to_user()
3.2 Linux 下的傳統(tǒng) IPC 通信原理
理解了上面的幾個概念,我們再來看看傳統(tǒng)的 IPC 方式中,進程之間是如何實現(xiàn)通信的。
通常的做法是消息發(fā)送方將要發(fā)送的數(shù)據(jù)存放在內(nèi)存緩存區(qū)中,通過系統(tǒng)調(diào)用進入內(nèi)核態(tài)。然后內(nèi)核程序在內(nèi)核空間分配內(nèi)存,開辟一塊內(nèi)核緩存區(qū),調(diào)用 copy_from_user() 函數(shù)將數(shù)據(jù)從用戶空間的內(nèi)存緩存區(qū)拷貝到內(nèi)核空間的內(nèi)核緩存區(qū)中。同樣的,接收方進程在接收數(shù)據(jù)時在自己的用戶空間開辟一塊內(nèi)存緩存區(qū),然后內(nèi)核程序調(diào)用 copy_to_user() 函數(shù)將數(shù)據(jù)從內(nèi)核緩存區(qū)拷貝到接收進程的內(nèi)存緩存區(qū)。這樣數(shù)據(jù)發(fā)送方進程和數(shù)據(jù)接收方進程就完成了一次數(shù)據(jù)傳輸,我們稱完成了一次進程間通信。如下圖:

傳統(tǒng) IPC 通信原理
這種傳統(tǒng)的 IPC 通信方式有兩個問題:
- 性能低下,一次數(shù)據(jù)傳遞需要經(jīng)歷:內(nèi)存緩存區(qū) --> 內(nèi)核緩存區(qū) --> 內(nèi)存緩存區(qū),需要 2 次數(shù)據(jù)拷貝;
- 接收數(shù)據(jù)的緩存區(qū)由數(shù)據(jù)接收進程提供,但是接收進程并不知道需要多大的空間來存放將要傳遞過來的數(shù)據(jù),因此只能開辟盡可能大的內(nèi)存空間或者先調(diào)用 API 接收消息頭來獲取消息體的大小,這兩種做法不是浪費空間就是浪費時間。
四. Binder 跨進程通信原理
理解了 Linux IPC 相關(guān)概念和通信原理,接下來我們正式介紹下 Binder IPC 的原理。
4.1 動態(tài)內(nèi)核可加載模塊 && 內(nèi)存映射
正如前面所說,跨進程通信是需要內(nèi)核空間做支持的。傳統(tǒng)的 IPC 機制如管道、Socket 都是內(nèi)核的一部分,因此通過內(nèi)核支持來實現(xiàn)進程間通信自然是沒問題的。但是 Binder 并不是 Linux 系統(tǒng)內(nèi)核的一部分,那怎么辦呢?這就得益于 Linux 的動態(tài)內(nèi)核可加載模塊(Loadable Kernel Module,LKM)的機制;模塊是具有獨立功能的程序,它可以被單獨編譯,但是不能獨立運行。它在運行時被鏈接到內(nèi)核作為內(nèi)核的一部分運行。這樣,Android 系統(tǒng)就可以通過動態(tài)添加一個內(nèi)核模塊運行在內(nèi)核空間,用戶進程之間通過這個內(nèi)核模塊作為橋梁來實現(xiàn)通信。
在 Android 系統(tǒng)中,這個運行在內(nèi)核空間,負責(zé)各個用戶進程通過 Binder 實現(xiàn)通信的內(nèi)核模塊就叫 Binder 驅(qū)動(Binder Dirver)。
那么在 Android 系統(tǒng)中用戶進程之間是如何通過這個內(nèi)核模塊(Binder 驅(qū)動)來實現(xiàn)通信的呢?難道是和前面說的傳統(tǒng) IPC 機制一樣,先將數(shù)據(jù)從發(fā)送方進程拷貝到內(nèi)核緩存區(qū),然后再將數(shù)據(jù)從內(nèi)核緩存區(qū)拷貝到接收方進程,通過兩次拷貝來實現(xiàn)嗎?顯然不是,否則也不會有開篇所說的 Binder 在性能方面的優(yōu)勢了。
這就不得不通道 Linux 下的另一個概念:內(nèi)存映射。
Binder IPC 機制中涉及到的內(nèi)存映射通過 mmap() 來實現(xiàn),mmap() 是操作系統(tǒng)中一種內(nèi)存映射的方法。內(nèi)存映射簡單的講就是將用戶空間的一塊內(nèi)存區(qū)域映射到內(nèi)核空間。映射關(guān)系建立后,用戶對這塊內(nèi)存區(qū)域的修改可以直接反應(yīng)到內(nèi)核空間;反之內(nèi)核空間對這段區(qū)域的修改也能直接反應(yīng)到用戶空間。
內(nèi)存映射能減少數(shù)據(jù)拷貝次數(shù),實現(xiàn)用戶空間和內(nèi)核空間的高效互動。兩個空間各自的修改能直接反映在映射的內(nèi)存區(qū)域,從而被對方空間及時感知。也正因為如此,內(nèi)存映射能夠提供對進程間通信的支持。
4.2 Binder IPC 實現(xiàn)原理
Binder IPC 正是基于內(nèi)存映射(mmap)來實現(xiàn)的,但是 mmap() 通常是用在有物理介質(zhì)的文件系統(tǒng)上的。
比如進程中的用戶區(qū)域是不能直接和物理設(shè)備打交道的,如果想要把磁盤上的數(shù)據(jù)讀取到進程的用戶區(qū)域,需要兩次拷貝(磁盤 --> 內(nèi)核空間 --> 用戶空間);通常在這種場景下 mmap() 就能發(fā)揮作用,通過在物理介質(zhì)和用戶空間之間建立映射,減少數(shù)據(jù)的拷貝次數(shù),用內(nèi)存讀寫取代 I/O 讀寫,提高文件讀取效率。
而 Binder 并不存在物理介質(zhì),因此 Binder 驅(qū)動使用 mmap() 并不是為了在物理介質(zhì)和用戶空間之間建立映射,而是用來在內(nèi)核空間創(chuàng)建數(shù)據(jù)接收的緩存空間。
一次完整的 Binder IPC 通信過程通常是這樣:
- 首先 Binder 驅(qū)動在內(nèi)核空間創(chuàng)建一個數(shù)據(jù)接收緩存區(qū);
- 接著在內(nèi)核空間開辟一塊內(nèi)核緩存區(qū),建立內(nèi)核緩存區(qū)和內(nèi)核中數(shù)據(jù)接收緩存區(qū)之間的映射關(guān)系,以及內(nèi)核中數(shù)據(jù)接收緩存區(qū)和接收進程用戶空間地址的映射關(guān)系;
- 發(fā)送方進程通過系統(tǒng)調(diào)用 copy_from_user() 將數(shù)據(jù) copy 到內(nèi)核中的內(nèi)核緩存區(qū),由于內(nèi)核緩存區(qū)和接收進程的用戶空間存在內(nèi)存映射,因此也就相當于把數(shù)據(jù)發(fā)送到了接收進程的用戶空間,這樣便完成了一次進程間的通信。
如下圖:

Binder IPC 原理
五. Binder 通信模型
介紹完 Binder IPC 的底層通信原理,接下來我們看看實現(xiàn)層面是如何設(shè)計的。
一次完整的進程間通信必然至少包含兩個進程,通常我們稱通信的雙方分別為客戶端進程(Client)和服務(wù)端進程(Server),由于進程隔離機制的存在,通信雙方必然需要借助 Binder 來實現(xiàn)。
5.1 Client/Server/ServiceManager / 驅(qū)動
前面我們介紹過,Binder 是基于 C/S 架構(gòu)的。由一系列的組件組成,包括 Client、Server、ServiceManager、Binder 驅(qū)動。其中 Client、Server、Service Manager 運行在用戶空間,Binder 驅(qū)動運行在內(nèi)核空間。其中 Service Manager 和 Binder 驅(qū)動由系統(tǒng)提供,而 Client、Server 由應(yīng)用程序來實現(xiàn)。Client、Server 和 ServiceManager 均是通過系統(tǒng)調(diào)用 open、mmap 和 ioctl 來訪問設(shè)備文件 /dev/binder,從而實現(xiàn)與 Binder 驅(qū)動的交互來間接的實現(xiàn)跨進程通信。

Client、Server、ServiceManager、Binder 驅(qū)動這幾個組件在通信過程中扮演的角色就如同互聯(lián)網(wǎng)中服務(wù)器(Server)、客戶端(Client)、DNS 域名服務(wù)器(ServiceManager)以及路由器(Binder 驅(qū)動)之前的關(guān)系。
通常我們訪問一個網(wǎng)頁的步驟是這樣的:首先在瀏覽器輸入一個地址,如 www.google.com 然后按下回車鍵。但是并沒有辦法通過域名地址直接找到我們要訪問的服務(wù)器,因此需要首先訪問 DNS 域名服務(wù)器,域名服務(wù)器中保存了 www.google.com 對應(yīng)的 ip 地址 10.249.23.13,然后通過這個 ip 地址才能放到到 www.google.com 對應(yīng)的服務(wù)器。

互聯(lián)網(wǎng)通信模型
Android Binder 設(shè)計與實現(xiàn)一文中對 Client、Server、ServiceManager、Binder 驅(qū)動有很詳細的描述,以下是部分摘錄:
Binder 驅(qū)動
Binder 驅(qū)動就如同路由器一樣,是整個通信的核心;驅(qū)動負責(zé)進程之間 Binder 通信的建立,Binder 在進程之間的傳遞,Binder 引用計數(shù)管理,數(shù)據(jù)包在進程之間的傳遞和交互等一系列底層支持。ServiceManager 與實名 Binder
ServiceManager 和 DNS 類似,作用是將字符形式的 Binder 名字轉(zhuǎn)化成 Client 中對該 Binder 的引用,使得 Client 能夠通過 Binder 的名字獲得對 Binder 實體的引用。注冊了名字的 Binder 叫實名 Binder,就像網(wǎng)站一樣除了除了有 IP 地址意外還有自己的網(wǎng)址。Server 創(chuàng)建了 Binder,并為它起一個字符形式,可讀易記得名字,將這個 Binder 實體連同名字一起以數(shù)據(jù)包的形式通過 Binder 驅(qū)動發(fā)送給 ServiceManager ,通知 ServiceManager 注冊一個名為 “張三” 的 Binder,它位于某個 Server 中。驅(qū)動為這個穿越進程邊界的 Binder 創(chuàng)建位于內(nèi)核中的實體節(jié)點以及 ServiceManager 對實體的引用,將名字以及新建的引用打包傳給 ServiceManager。ServiceManger 收到數(shù)據(jù)后從中取出名字和引用填入查找表。細心的讀者可能會發(fā)現(xiàn),ServierManager 是一個進程,Server 是另一個進程,Server 向 ServiceManager 中注冊 Binder 必然涉及到進程間通信。當前實現(xiàn)進程間通信又要用到進程間通信,這就好像蛋可以孵出雞的前提卻是要先找只雞下蛋!Binder 的實現(xiàn)比較巧妙,就是預(yù)先創(chuàng)造一只雞來下蛋。ServiceManager 和其他進程同樣采用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 實體,其他進程都是 Client,需要通過這個 Binder 的引用來實現(xiàn) Binder 的注冊,查詢和獲取。ServiceManager 提供的 Binder 比較特殊,它沒有名字也不需要注冊。當一個進程使用 BINDER_SET_CONTEXT_MGR 命令將自己注冊成 ServiceManager 時 Binder 驅(qū)動會自動為它創(chuàng)建 Binder 實體(這就是那只預(yù)先造好的那只雞)。其次這個 Binder 實體的引用在所有 Client 中都固定為 0 而無需通過其它手段獲得。也就是說,一個 Server 想要向 ServiceManager 注冊自己的 Binder 就必須通過這個 0 號引用和 ServiceManager 的 Binder 通信。類比互聯(lián)網(wǎng),0 號引用就好比是域名服務(wù)器的地址,你必須預(yù)先動態(tài)或者手工配置好。要注意的是,這里說的 Client 是相對于 ServiceManager 而言的,一個進程或者應(yīng)用程序可能是提供服務(wù)的 Server,但對于 ServiceManager 來說它仍然是個 Client。
Client 獲得實名 Binder 的引用
Server 向 ServiceManager 中注冊了 Binder 以后, Client 就能通過名字獲得 Binder 的引用了。Client 也利用保留的 0 號引用向 ServiceManager 請求訪問某個 Binder: 我申請訪問名字叫張三的 Binder 引用。ServiceManager 收到這個請求后從請求數(shù)據(jù)包中取出 Binder 名稱,在查找表里找到對應(yīng)的條目,取出對應(yīng)的 Binder 引用作為回復(fù)發(fā)送給發(fā)起請求的 Client。從面向?qū)ο蟮慕嵌瓤?,Server 中的 Binder 實體現(xiàn)在有兩個引用:一個位于 ServiceManager 中,一個位于發(fā)起請求的 Client 中。如果接下來有更多的 Client 請求該 Binder,系統(tǒng)中就會有更多的引用指向該 Binder ,就像 Java 中一個對象有多個引用一樣。
5.2 Binder 通信過程
至此,我們大致能總結(jié)出 Binder 通信過程:
- 首先,一個進程使用 BINDER_SET_CONTEXT_MGR 命令通過 Binder 驅(qū)動將自己注冊成為 ServiceManager;
- Server 通過驅(qū)動向 ServiceManager 中注冊 Binder(Server 中的 Binder 實體),表明可以對外提供服務(wù)。驅(qū)動為這個 Binder 創(chuàng)建位于內(nèi)核中的實體節(jié)點以及 ServiceManager 對實體的引用,將名字以及新建的引用打包傳給 ServiceManager,ServiceManger 將其填入查找表。
- Client 通過名字,在 Binder 驅(qū)動的幫助下從 ServiceManager 中獲取到對 Binder 實體的引用,通過這個引用就能實現(xiàn)和 Server 進程的通信。
我們看到整個通信過程都需要 Binder 驅(qū)動的接入。下圖能更加直觀的展現(xiàn)整個通信過程 (為了進一步抽象通信過程以及呈現(xiàn)上的方便,下圖我們忽略了 Binder 實體及其引用的概念):

Binder 通信模型
5.3 Binder 通信中的代理模式
我們已經(jīng)解釋清楚 Client、Server 借助 Binder 驅(qū)動完成跨進程通信的實現(xiàn)機制了,但是還有個問題會讓我們困惑。A 進程想要 B 進程中某個對象(object)是如何實現(xiàn)的呢?畢竟它們分屬不同的進程,A 進程 沒法直接使用 B 進程中的 object。
前面我們介紹過跨進程通信的過程都有 Binder 驅(qū)動的參與,因此在數(shù)據(jù)流經(jīng) Binder 驅(qū)動的時候驅(qū)動會對數(shù)據(jù)做一層轉(zhuǎn)換。當 A 進程想要獲取 B 進程中的 object 時,驅(qū)動并不會真的把 object 返回給 A,而是返回了一個跟 object 看起來一模一樣的代理對象 objectProxy,這個 objectProxy 具有和 object 一摸一樣的方法,但是這些方法并沒有 B 進程中 object 對象那些方法的能力,這些方法只需要把把請求參數(shù)交給驅(qū)動即可。對于 A 進程來說和直接調(diào)用 object 中的方法是一樣的。
當 Binder 驅(qū)動接收到 A 進程的消息后,發(fā)現(xiàn)這是個 objectProxy 就去查詢自己維護的表單,一查發(fā)現(xiàn)這是 B 進程 object 的代理對象。于是就會去通知 B 進程調(diào)用 object 的方法,并要求 B 進程把返回結(jié)果發(fā)給自己。當驅(qū)動拿到 B 進程的返回結(jié)果后就會轉(zhuǎn)發(fā)給 A 進程,一次通信就完成了。

5.4 Binder 的完整定義
現(xiàn)在我們可以對 Binder 做個更加全面的定義了:
- 從進程間通信的角度看,Binder 是一種進程間通信的機制;
- 從 Server 進程的角度看,Binder 指的是 Server 中的 Binder 實體對象;
- 從 Client 進程的角度看,Binder 指的是對 Binder 代理對象,是 Binder 實體對象的一個遠程代理
- 從傳輸過程的角度看,Binder 是一個可以跨進程傳輸?shù)膶ο?;Binder 驅(qū)動會對這個跨越進程邊界的對象對一點點特殊處理,自動完成代理對象和本地對象之間的轉(zhuǎn)換。
六. 手動編碼實現(xiàn)跨進程調(diào)用
通常我們在做開發(fā)時,實現(xiàn)進程間通信用的最多的就是 AIDL。當我們定義好 AIDL 文件,在編譯時編譯器會幫我們生成代碼實現(xiàn) IPC 通信。借助 AIDL 編譯以后的代碼能幫助我們進一步理解 Binder IPC 的通信原理。
但是無論是從可讀性還是可理解性上來看,編譯器生成的代碼對開發(fā)者并不友好。比如一個 BookManager.aidl 文件對應(yīng)會生成一個 BookManager.java 文件,這個 java 文件包含了一個 BookManager 接口、一個 Stub 靜態(tài)的抽象類和一個 Proxy 靜態(tài)類。Proxy 是 Stub 的靜態(tài)內(nèi)部類,Stub 又是 BookManager 的靜態(tài)內(nèi)部類,這就造成了可讀性和可理解性的問題。
Android 之所以這樣設(shè)計其實是有道理的,因為當有多個 AIDL 文件的時候把 BookManager、Stub、Proxy 放在同一個文件里能有效避免 Stub 和 Proxy 重名的問題。
因此便于大家理解,下面我們來手動編寫代碼來實現(xiàn)跨進程調(diào)用。
6.1 各 Java 類職責(zé)描述
在正式編碼實現(xiàn)跨進程調(diào)用之前,先介紹下實現(xiàn)過程中用到的一些類。了解了這些類的職責(zé),有助于我們更好的理解和實現(xiàn)跨進程通信。
IBinder : IBinder 是一個接口,代表了一種跨進程通信的能力。只要實現(xiàn)了這個借口,這個對象就能跨進程傳輸。
IInterface : IInterface 代表的就是 Server 進程對象具備什么樣的能力(能提供哪些方法,其實對應(yīng)的就是 AIDL 文件中定義的接口)
Binder : Java 層的 Binder 類,代表的其實就是 Binder 本地對象。BinderProxy 類是 Binder 類的一個內(nèi)部類,它代表遠程進程的 Binder 對象的本地代理;這兩個類都繼承自 IBinder, 因而都具有跨進程傳輸?shù)哪芰?;實際上,在跨越進程的時候,Binder 驅(qū)動會自動完成這兩個對象的轉(zhuǎn)換。
Stub : AIDL 的時候,編譯工具會給我們生成一個名為 Stub 的靜態(tài)內(nèi)部類;這個類繼承了 Binder, 說明它是一個 Binder 本地對象,它實現(xiàn)了 IInterface 接口,表明它具有 Server 承諾給 Client 的能力;Stub 是一個抽象類,具體的 IInterface 的相關(guān)實現(xiàn)需要開發(fā)者自己實現(xiàn)。
6.2 實現(xiàn)過程講解
一次跨進程通信必然會涉及到兩個進程,在這個例子中 RemoteService 作為服務(wù)端進程,提供服務(wù);ClientActivity 作為客戶端進程,使用 RemoteService 提供的服務(wù)。如下圖:

那么服務(wù)端進程具備什么樣的能力?能為客戶端提供什么樣的服務(wù)呢?還記得我們前面介紹過的 IInterface 嗎,它代表的就是服務(wù)端進程具體什么樣的能力。因此我們需要定義一個 BookManager 接口,BookManager 繼承自 IIterface,表明服務(wù)端具備什么樣的能力。
public interface BookManager extends IInterface {
void addBook(Book book) throws RemoteException;
}
只定義服務(wù)端具備什么要的能力是不夠的,既然是跨進程調(diào)用,那么接下來我們得實現(xiàn)一個跨進程調(diào)用對象 Stub。Stub 繼承 Binder, 說明它是一個 Binder 本地對象;實現(xiàn) IInterface 接口,表明具有 Server 承諾給 Client 的能力;Stub 是一個抽象類,具體的 IInterface 的相關(guān)實現(xiàn)需要調(diào)用方自己實現(xiàn)。
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 端在創(chuàng)建和服務(wù)端的連接,調(diào)用 bindService 時需要創(chuàng)建一個 ServiceConnection 對象作為入?yún)?。?ServiceConnection 的回調(diào)方法 onServiceConnected 中 會通過這個 asInterface(IBinder binder) 拿到 BookManager 對象,這個 IBinder 類型的入?yún)?binder 是驅(qū)動傳給我們的,正如你在代碼中看到的一樣,方法中會去調(diào)用 binder.queryLocalInterface() 去查找 Binder 本地對象,如果找到了就說明 Client 和 Server 在同一進程,那么這個 binder 本身就是 Binder 本地對象,可以直接使用。否則說明是 binder 是個遠程對象,也就是 BinderProxy。因此需要我們創(chuàng)建一個代理對象 Proxy,通過這個代理對象來是實現(xiàn)遠程訪問。
接下來我們就要實現(xiàn)這個代理類 Proxy 了,既然是代理類自然需要實現(xiàn) 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() 的實現(xiàn);在 Stub 類中,addBook(Book book) 是一個抽象方法,Client 端需要繼承并實現(xiàn)它。
- 如果 Client 和 Server 在同一個進程,那么直接就是調(diào)用這個方法。
- 如果是遠程調(diào)用,Client 想要調(diào)用 Server 的方法就需要通過 Binder 代理來完成,也就是上面的 Proxy。
在 Proxy 中的 addBook() 方法中首先通過 Parcel 將數(shù)據(jù)序列化,然后調(diào)用 remote.transact()。正如前文所述 Proxy 是在 Stub 的 asInterface 中創(chuàng)建,能走到創(chuàng)建 Proxy 這一步就說明 Proxy 構(gòu)造函數(shù)的入?yún)⑹?BinderProxy,即這里的 remote 是個 BinderProxy 對象。最終通過一系列的函數(shù)調(diào)用,Client 進程通過系統(tǒng)調(diào)用陷入內(nèi)核態(tài),Client 進程中執(zhí)行 addBook() 的線程掛起等待返回;驅(qū)動完成一系列的操作之后喚醒 Server 進程,調(diào)用 Server 進程本地對象的 onTransact()。最終又走到了 Stub 中的 onTransact() 中,onTransact() 根據(jù)函數(shù)編號調(diào)用相關(guān)函數(shù)(在 Stub 類中為 BookManager 接口中的每個函數(shù)中定義了一個編號,只不過上面的源碼中我們簡化掉了;在跨進程調(diào)用的時候,不會傳遞函數(shù)而是傳遞編號來指明要調(diào)用哪個函數(shù));我們這個例子里面,調(diào)用了 Binder 本地對象的 addBook() 并將結(jié)果返回給驅(qū)動,驅(qū)動喚醒 Client 進程里剛剛掛起的線程并將結(jié)果返回。
這樣一次跨進程調(diào)用就完成了。
完整的代碼我放到 GitHub 上了,有興趣的小伙伴可以去看看。源碼地址:https://github.com/BaronZ88/HelloBinder
最后建議大家在不借助 AIDL 的情況下手寫實現(xiàn) Client 和 Server 進程的通信,加深對 Binder 通信過程的理解。
受個人能力水平限制,文章中難免會有錯誤。如果大家發(fā)現(xiàn)文章不足之處,歡迎與我溝通交流。
本文在寫作過程中參考了很多文章、書籍和源碼,其中有很多描述和圖片都借鑒了下面的文章,在這里感謝大佬們的無私分享!
參考資料如下: