寫給 Android 應用工程師的 Binder 原理剖析

張磊BARON發表於2019-03-04

歡迎關注微信公眾號:BaronTalk

一. 前言

這篇文章我醞釀了很久,參考了很多資料,讀了很多原始碼,卻依舊不敢下筆。生怕自己理解上還有偏差,對大家造成誤解,貽笑大方。又怕自己理解不夠透徹,無法用清晰直白的文字準確的表達出 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 的優勢:

優勢 描述
效能 只需要一次資料拷貝,效能上僅次於共享記憶體
穩定性 基於 C/S 架構,職責明確、架構清晰,因此穩定性好
安全性 為每個 APP 分配 UID,程式的 UID 是鑑別程式身份的重要標誌

三. Linux 下傳統的程式間通訊原理

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

3.1 基本概念介紹

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

Linux 背景知識

上圖展示了 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)是使用者程式執行的空間。為了保證安全性,它們之間是隔離的。

圖片來自網路

系統呼叫:使用者態與核心態

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

Linux 使用兩級保護機制:0 級供系統核心使用,3 級供使用者程式使用。

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

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

系統呼叫主要通過如下兩個函式來實現:

copy_from_user() //將資料從使用者空間拷貝到核心空間
copy_to_user() //將資料從核心空間拷貝到使用者空間
複製程式碼

3.2 Linux 下的傳統 IPC 通訊原理

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

通常的做法是訊息傳送方將要傳送的資料存放在記憶體快取區中,通過系統呼叫進入核心態。然後核心程式在核心空間分配記憶體,開闢一塊核心快取區,呼叫 copy_from_user() 函式將資料從使用者空間的記憶體快取區拷貝到核心空間的核心快取區中。同樣的,接收方程式在接收資料時在自己的使用者空間開闢一塊記憶體快取區,然後核心程式呼叫 copy_to_user() 函式將資料從核心快取區拷貝到接收程式的記憶體快取區。這樣資料傳送方程式和資料接收方程式就完成了一次資料傳輸,我們稱完成了一次程式間通訊。如下圖:

傳統 IPC 通訊原理

這種傳統的 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 IPC 原理

五. 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 驅動的互動來間接的實現跨程式通訊。

寫給 Android 應用工程師的 Binder 原理剖析

Client、Server、ServiceManager、Binder 驅動這幾個元件在通訊過程中扮演的角色就如同網際網路中伺服器(Server)、客戶端(Client)、DNS域名伺服器(ServiceManager)以及路由器(Binder 驅動)之前的關係。

通常我們訪問一個網頁的步驟是這樣的:首先在瀏覽器輸入一個地址,如 www.google.com 然後按下Enter鍵。但是並沒有辦法通過域名地址直接找到我們要訪問的伺服器,因此需要首先訪問 DNS 域名伺服器,域名伺服器中儲存了 www.google.com 對應的 ip 地址 10.249.23.13,然後通過這個 ip 地址才能放到到 www.google.com 對應的伺服器。

網際網路通訊模型

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 程式,一次通訊就完成了。

寫給 Android 應用工程師的 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 提供的服務。如下圖:

寫給 Android 應用工程師的 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 類中我們重點介紹下 asInterfaceonTransact

先說說 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 通訊過程的理解。

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

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

參考資料如下:

如果你喜歡我的文章,就關注下我的公眾號 BaronTalk知乎專欄 或者在 GitHub 上添個 Star 吧!

寫給 Android 應用工程師的 Binder 原理剖析

相關文章