打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

作者:張磊BARON

鏈接:https://juejin.im/post/5acccf845188255c3201100f

我又來分享好文了,1w加閱讀量果然不是吹得!都是乾貨建議收藏!

一. 前言

這篇文章我醞釀了很久,參考了很多資料,讀了很多源碼,卻依舊不敢下筆。生怕自己理解上還有偏差,對大家造成誤解,貽笑大方。又怕自己理解不夠透徹,無法用清晰直白的文字準確的表達出 Binder 的設計精髓。直到今天提筆寫作時還依舊戰戰兢兢。

Binder 之複雜遠遠不是一篇文章就能說清楚的,本文想站在一個更高的維度來俯瞰 Binder 的設計,最終幫助大家形成一個完整的概念。對於應用層開發的同學來說,理解到本文這個程度也就差不多了。

希望更加深入理解 Binder 實現機制的,可以閱讀文末的參考資料以及相關源碼。(我都已經給大家整理好咯,到文末去領取吧!)

二. Binder 概述

簡單介紹下什麼是 Binder。Binder 是一種進程間通信機制,基於開源的 OpenBinder 實現;OpenBinder 起初由 Be Inc. 開發,後由 Plam Inc. 接手。從字面上來解釋 Binder 有膠水、粘合劑的意思,顧名思義就是粘和不同的進程,使之實現通信。對於 Binder 更全面的定義,等我們介紹完 Binder 通信原理後再做詳細說明。

2.1 爲什麼必須理解 Binder ?

作爲 Android 工程師的你,是不是常常會有這樣的疑問:

  • 爲什麼 Activity 間傳遞對象需要序列化?
  • Activity 的啓動流程是什麼樣的?
  • 四大組件底層的通信機制是怎樣的?
  • AIDL 內部的實現原理是什麼?
  • 插件化編程技術應該從何學起?等等...

這些問題的背後都與 Binder 有莫大的關係,要弄懂上面這些問題理解 Bidner 通信機制是必須的。

我們知道 Android 應用程序是由 Activity、Service、Broadcast Receiver 和 Content Provide 四大組件中的一個或者多個組成的。有時這些組件運行在同一進程,有時運行在不同的進程。這些進程間的通信就依賴於 Binder IPC 機制。不僅如此,Android 系統對應用層提供的各種服務如:ActivityManagerService、PackageManagerService 等都是基於 Binder IPC 機制來實現的。Binder 機制在 Android 中的位置非常重要,毫不誇張的說理解 Binder 是邁向 Android 高級工程的第一步。

2.2 爲什麼是 Binder ?

Android 系統是基於 Linux 內核的,Linux 已經提供了管道、消息隊列、共享內存和 Socket 等 IPC 機制。那爲什麼 Android 還要提供 Binder 來實現 IPC 呢?主要是基於性能穩定性安全性幾方面的原因。

  • 性能

首先說說性能上的優勢。Socket 作爲一款通用接口,其傳輸效率低,開銷大,主要用在跨網絡的進程間通信和本機上進程間的低速通信。消息隊列和管道採用存儲-轉發方式,即數據先從發送方緩存區拷貝到內核開闢的緩存區中,然後再從內核緩存區拷貝到接收方緩存區,至少有兩次拷貝過程。共享內存雖然無需拷貝,但控制複雜,難以使用。Binder 只需要一次數據拷貝,性能上僅次於共享內存。

注:各種IPC方式數據拷貝次數,此表來源於Android Binder 設計與實現 - 設計篇

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 的優勢:

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

三. Linux 下傳統的進程間通信原理

瞭解 Linux IPC 相關的概念和原理有助於我們理解 Binder 通信原理。因此,在介紹 Binder 跨進程通信原理之前,我們先聊聊 Linux 系統下傳統的進程間通信是如何實現。

3.1 基本概念介紹

這裏我們先從 Linux 中進程間通信涉及的一些基本概念開始介紹,然後逐步展開,向大家說明傳統的進程間通信的原理。

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

上圖展示了 Liunx 中跨進程通信涉及到的一些基本概念:

  • 進程隔離
  • 進程空間劃分:用戶空間(User Space)/內核空間(Kernel Space)
  • 系統調用:用戶態/內核態

進程隔離

簡單的說就是操作系統中,進程與進程間內存是不共享的。兩個進程就像兩個平行的世界,A 進程沒法直接訪問 B 進程的數據,這就是進程隔離的通俗解釋。A 進程和 B 進程之間要進行數據交互就得采用特殊的通信機制:進程間通信(IPC)。

進程空間劃分:用戶空間(User Space)/內核空間(Kernel Space)

現在操作系統都是採用的虛擬存儲器,對於 32 位系統而言,它的尋址空間(虛擬存儲空間)就是 2 的 32 次方,也就是 4GB。操作系統的核心是內核,獨立於普通的應用程序,可以訪問受保護的內存空間,也可以訪問底層硬件設備的權限。

爲了保護用戶進程不能直接操作內核,保證內核的安全,操作系統從邏輯上將虛擬空間劃分爲用戶空間(User Space)和內核空間(Kernel Space)。針對 Linux 操作系統而言,將最高的 1GB 字節供內核使用,稱爲內核空間;較低的 3GB 字節供各進程使用,稱爲用戶空間。

簡單的說就是,內核空間(Kernel)是系統內核運行的空間,用戶空間(User Space)是用戶程序運行的空間。爲了保證安全性,它們之間是隔離的。

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

系統調用:用戶態與內核態

雖然從邏輯上進行了用戶空間和內核空間的劃分,但不可避免的用戶空間需要訪問內核資源,比如文件操作、訪問網絡等等。爲了突破隔離限制,就需要藉助系統調用來實現。系統調用是用戶空間訪問內核空間的唯一方式,保證了所有的資源訪問都是在內核的控制下進行的,避免了用戶程序對系統資源的越權訪問,提升了系統安全性和穩定性。

Linux 使用兩級保護機制:0 級供系統內核使用,3 級供用戶程序使用。

當一個任務(進程)執行系統調用而陷入內核代碼中執行時,稱進程處於內核運行態(內核態)。此時處理器處於特權級最高的(0級)內核代碼中執行。當進程處於內核態時,執行的內核代碼會使用當前進程的內核棧。每個進程都有自己的內核棧。

當進程在執行用戶自己的代碼的時候,我們稱其處於用戶運行態(用戶態)。此時處理器在特權級最低的(3級)用戶代碼中運行。

系統調用主要通過如下兩個函數來實現:

copy_from_user() //將數據從用戶空間拷貝到內核空間
copy_to_user() //將數據從內核空間拷貝到用戶空間
複製代碼

3.2 Linux 下的傳統 IPC 通信原理

理解了上面的幾個概念,我們再來看看傳統的 IPC 方式中,進程之間是如何實現通信的。

通常的做法是消息發送方將要發送的數據存放在內存緩存區中,通過系統調用進入內核態。然後內核程序在內核空間分配內存,開闢一塊內核緩存區,調用 copy_from_user() 函數將數據從用戶空間的內存緩存區拷貝到內核空間的內核緩存區中。同樣的,接收方進程在接收數據時在自己的用戶空間開闢一塊內存緩存區,然後內核程序調用 copy_to_user() 函數將數據從內核緩存區拷貝到接收進程的內存緩存區。這樣數據發送方進程和數據接收方進程就完成了一次數據傳輸,我們稱完成了一次進程間通信。如下圖:

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

這種傳統的 IPC 通信方式有兩個問題:

  1. 性能低下,一次數據傳遞需要經歷:內存緩存區 --> 內核緩存區 --> 內存緩存區,需要 2 次數據拷貝;
  2. 接收數據的緩存區由數據接收進程提供,但是接收進程並不知道需要多大的空間來存放將要傳遞過來的數據,因此只能開闢儘可能大的內存空間或者先調用 API 接收消息頭來獲取消息體的大小,這兩種做法不是浪費空間就是浪費時間。

四. Binder 跨進程通信原理

理解了 Linux IPC 相關概念和通信原理,接下來我們正式介紹下 Binder IPC 的原理。

4.1 動態內核可加載模塊 && 內存映射

正如前面所說,跨進程通信是需要內核空間做支持的。傳統的 IPC 機制如管道、Socket 都是內核的一部分,因此通過內核支持來實現進程間通信自然是沒問題的。但是 Binder 並不是 Linux 系統內核的一部分,那怎麼辦呢?這就得益於 Linux 的動態內核可加載模塊(Loadable Kernel Module,LKM)的機制;模塊是具有獨立功能的程序,它可以被單獨編譯,但是不能獨立運行。它在運行時被鏈接到內核作爲內核的一部分運行。這樣,Android 系統就可以通過動態添加一個內核模塊運行在內核空間,用戶進程之間通過這個內核模塊作爲橋樑來實現通信。

在 Android 系統中,這個運行在內核空間,負責各個用戶進程通過 Binder 實現通信的內核模塊就叫 Binder 驅動(Binder Dirver)。

那麼在 Android 系統中用戶進程之間是如何通過這個內核模塊(Binder 驅動)來實現通信的呢?難道是和前面說的傳統 IPC 機制一樣,先將數據從發送方進程拷貝到內核緩存區,然後再將數據從內核緩存區拷貝到接收方進程,通過兩次拷貝來實現嗎?顯然不是,否則也不會有開篇所說的 Binder 在性能方面的優勢了。

這就不得不通道 Linux 下的另一個概念:內存映射

Binder IPC 機制中涉及到的內存映射通過 mmap() 來實現,mmap() 是操作系統中一種內存映射的方法。內存映射簡單的講就是將用戶空間的一塊內存區域映射到內核空間。映射關係建立後,用戶對這塊內存區域的修改可以直接反應到內核空間;反之內核空間對這段區域的修改也能直接反應到用戶空間。

內存映射能減少數據拷貝次數,實現用戶空間和內核空間的高效互動。兩個空間各自的修改能直接反映在映射的內存區域,從而被對方空間及時感知。也正因爲如此,內存映射能夠提供對進程間通信的支持。

4.2 Binder IPC 實現原理

Binder IPC 正是基於內存映射(mmap)來實現的,但是 mmap() 通常是用在有物理介質的文件系統上的。

比如進程中的用戶區域是不能直接和物理設備打交道的,如果想要把磁盤上的數據讀取到進程的用戶區域,需要兩次拷貝(磁盤-->內核空間-->用戶空間);通常在這種場景下 mmap() 就能發揮作用,通過在物理介質和用戶空間之間建立映射,減少數據的拷貝次數,用內存讀寫取代I/O讀寫,提高文件讀取效率。

而 Binder 並不存在物理介質,因此 Binder 驅動使用 mmap() 並不是爲了在物理介質和用戶空間之間建立映射,而是用來在內核空間創建數據接收的緩存空間。

一次完整的 Binder IPC 通信過程通常是這樣:

  1. 首先 Binder 驅動在內核空間創建一個數據接收緩存區;
  2. 接着在內核空間開闢一塊內核緩存區,建立內核緩存區內核中數據接收緩存區之間的映射關係,以及內核中數據接收緩存區接收進程用戶空間地址的映射關係;
  3. 發送方進程通過系統調用 copy_from_user() 將數據 copy 到內核中的內核緩存區,由於內核緩存區和接收進程的用戶空間存在內存映射,因此也就相當於把數據發送到了接收進程的用戶空間,這樣便完成了一次進程間的通信。

如下圖:

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

五. Binder 通信模型

介紹完 Binder IPC 的底層通信原理,接下來我們看看實現層面是如何設計的。

一次完整的進程間通信必然至少包含兩個進程,通常我們稱通信的雙方分別爲客戶端進程(Client)和服務端進程(Server),由於進程隔離機制的存在,通信雙方必然需要藉助 Binder 來實現。

5.1 Client/Server/ServiceManager/驅動

前面我們介紹過,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 驅動的交互來間接的實現跨進程通信。

打造今年爆款!大牛吊炸天的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 對應的服務器。

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

Android Binder 設計與實現一文中對 Client、Server、ServiceManager、Binder 驅動有很詳細的描述,以下是部分摘錄:

Binder 驅動 Binder 驅動就如同路由器一樣,是整個通信的核心;驅動負責進程之間 Binder 通信的建立,Binder 在進程之間的傳遞,Binder 引用計數管理,數據包在進程之間的傳遞和交互等一系列底層支持。

 ServiceManager 與實名 Binder ServiceManager 和 DNS 類似,作用是將字符形式的 Binder 名字轉化成 Client 中對該 Binder 的引用,使得 Client 能夠通過 Binder 的名字獲得對 Binder 實體的引用。註冊了名字的 Binder 叫實名 Binder,就像網站一樣除了除了有 IP 地址意外還有自己的網址。

Server 創建了 Binder,併爲它起一個字符形式,可讀易記得名字,將這個 Binder 實體連同名字一起以數據包的形式通過 Binder 驅動發送給 ServiceManager ,通知 ServiceManager 註冊一個名爲“張三”的 Binder,它位於某個 Server 中。驅動爲這個穿越進程邊界的 Binder 創建位於內核中的實體節點以及 ServiceManager 對實體的引用,將名字以及新建的引用打包傳給 ServiceManager。
ServiceManger 收到數據後從中取出名字和引用填入查找表。 細心的讀者可能會發現,ServierManager 是一個進程,Server 是另一個進程,Server 向 ServiceManager 中註冊 Binder 必然涉及到進程間通信。當前實現進程間通信又要用到進程間通信,這就好像蛋可以孵出雞的前提卻是要先找只雞下蛋!Binder 的實現比較巧妙,就是預先創造一隻雞來下蛋。

ServiceManager 和其他進程同樣採用 Bidner 通信,ServiceManager 是 Server 端,有自己的 Binder 實體,其他進程都是 Client,需要通過這個 Binder 的引用來實現 Binder 的註冊,查詢和獲取。ServiceManager 提供的 Binder 比較特殊,它沒有名字也不需要註冊。當一個進程使用 BINDER_SET_CONTEXT_MGR 命令將自己註冊成 ServiceManager 時 Binder 驅動會自動爲它創建 Binder 實體(這就是那隻預先造好的那隻雞)。其次這個 Binder 實體的引用在所有 Client 中都固定爲 0 而無需通過其它手段獲得。也就是說,一個 Server 想要向 ServiceManager 註冊自己的 Binder 就必須通過這個 0 號引用和 ServiceManager 的 Binder 通信。

類比互聯網,0 號引用就好比是域名服務器的地址,你必須預先動態或者手工配置好。要注意的是,這裏說的 Client 是相對於 ServiceManager 而言的,一個進程或者應用程序可能是提供服務的 Server,但對於 ServiceManager 來說它仍然是個 Client。 Client 獲得實名 Binder 的引用 Server 向 ServiceManager 中註冊了 Binder 以後, Client 就能通過名字獲得 Binder 的引用了。

Client 也利用保留的 0 號引用向 ServiceManager 請求訪問某個 Binder: 我申請訪問名字叫張三的 Binder 引用。ServiceManager 收到這個請求後從請求數據包中取出 Binder 名稱,在查找表裏找到對應的條目,取出對應的 Binder 引用作爲回覆發送給發起請求的 Client。從面向對象的角度看,Server 中的 Binder 實體現在有兩個引用:一個位於 ServiceManager 中,一個位於發起請求的 Client 中。如果接下來有更多的 Client 請求該 Binder,系統中就會有更多的引用指向該 Binder ,就像 Java 中一個對象有多個引用一樣。

5.2 Binder 通信過程

至此,我們大致能總結出 Binder 通信過程:

  1. 首先,一個進程使用 BINDER_SET_CONTEXT_MGR 命令通過 Binder 驅動將自己註冊成爲 ServiceManager;
  2. Server 通過驅動向 ServiceManager 中註冊 Binder(Server 中的 Binder 實體),表明可以對外提供服務。驅動爲這個 Binder 創建位於內核中的實體節點以及 ServiceManager 對實體的引用,將名字以及新建的引用打包傳給 ServiceManager,ServiceManger 將其填入查找表。
  3. Client 通過名字,在 Binder 驅動的幫助下從 ServiceManager 中獲取到對 Binder 實體的引用,通過這個引用就能實現和 Server 進程的通信。

我們看到整個通信過程都需要 Binder 驅動的接入。下圖能更加直觀的展現整個通信過程(爲了進一步抽象通信過程以及呈現上的方便,下圖我們忽略了 Binder 實體及其引用的概念):

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

5.3 Binder 通信中的代理模式

我們已經解釋清楚 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原理講解,你準備好上車了嗎?

5.4 Binder 的完整定義

現在我們可以對 Binder 做個更加全面的定義了:

  • 從進程間通信的角度看,Binder 是一種進程間通信的機制;
  • 從 Server 進程的角度看,Binder 指的是 Server 中的 Binder 實體對象;
  • 從 Client 進程的角度看,Binder 指的是對 Binder 代理對象,是 Binder 實體對象的一個遠程代理
  • 從傳輸過程的角度看,Binder 是一個可以跨進程傳輸的對象;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 重名的問題。

因此便於大家理解,下面我們來手動編寫代碼來實現跨進程調用。

6.1 各 Java 類職責描述

在正式編碼實現跨進程調用之前,先介紹下實現過程中用到的一些類。瞭解了這些類的職責,有助於我們更好的理解和實現跨進程通信。

  • IBinder : IBinder 是一個接口,代表了一種跨進程通信的能力。只要實現了這個藉口,這個對象就能跨進程傳輸。
  • IInterface : IInterface 代表的就是 Server 進程對象具備什麼樣的能力(能提供哪些方法,其實對應的就是 AIDL 文件中定義的接口)
  • Binder : Java 層的 Binder 類,代表的其實就是 Binder 本地對象。BinderProxy 類是 Binder 類的一個內部類,它代表遠程進程的 Binder 對象的本地代理;這兩個類都繼承自 IBinder, 因而都具有跨進程傳輸的能力;實際上,在跨越進程的時候,Binder 驅動會自動完成這兩個對象的轉換。
  • Stub : AIDL 的時候,編譯工具會給我們生成一個名爲 Stub 的靜態內部類;這個類繼承了 Binder, 說明它是一個 Binder 本地對象,它實現了 IInterface 接口,表明它具有 Server 承諾給 Client 的能力;Stub 是一個抽象類,具體的 IInterface 的相關實現需要開發者自己實現。

6.2 實現過程講解

一次跨進程通信必然會涉及到兩個進程,在這個例子中 RemoteService 作爲服務端進程,提供服務;ClientActivity 作爲客戶端進程,使用 RemoteService 提供的服務。如下圖:

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

那麼服務端進程具備什麼樣的能力?能爲客戶端提供什麼樣的服務呢?還記得我們前面介紹過的 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 端需要繼承並實現它。

  • 如果 Client 和 Server 在同一個進程,那麼直接就是調用這個方法。
  • 如果是遠程調用,Client 想要調用 Server 的方法就需要通過 Binder 代理來完成,也就是上面的 Proxy。

在 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 進程裏剛剛掛起的線程並將結果返回。

這樣一次跨進程調用就完成了。

完整的代碼我放到 GitHub 上了,有興趣的小夥伴可以去看看。源碼地址:github.com/BaronZ88/He…

最後建議大家在不借助 AIDL 的情況下手寫實現 Client 和 Server 進程的通信,加深對 Binder 通信過程的理解。

受個人能力水平限制,文章中難免會有錯誤。如果大家發現文章不足之處,歡迎與我溝通交流。

本文在寫作過程中參考了很多文章、書籍和源碼,其中有很多描述和圖片都借鑑了下面的文章,在這裏感謝大佬們的無私分享!

參考資料如下:

  • Android Binder 設計與實現 - 設計篇
  • Android 進程間通信(IPC)機制 Binder 簡要介紹和學習計劃、《Android 系統源代碼情景分析》
  • Binder 學習指南
  • Binder 系列文章
  • Android 圖文詳解 Binder 跨進程通信原理
  • Android 深入淺出之 Binder 機制
  • 用戶空間與內核空間
  • 認真分析 mmap :是什麼 爲什麼 怎麼用

Binder精選資料

330頁Android進階核心筆記

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

對應核心筆記的資料文檔

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

對應資料的學習視頻

打造今年爆款!大牛吊炸天的Binder原理講解,你準備好上車了嗎?

資料都在這裏咯!大家評論,轉發加關注私信我【Binder】我免費分享給你。然後你再按照我的資料去總結自己的經驗,找適合自己的模式。學習效率一定會提高的!!

直接點擊鏈接也可以領取哦!

Android學習PDF+架構視頻+面試文檔+源碼筆記

如果你有其他需要的話,也可以在 GitHub 上查看,下面的資料也會陸續上傳到Github

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章