Android 高階面試-2:IPC 相關

WngShhng發表於2019-02-18

內容

IPC 就是指跨程式通訊。IPC 相關的內容,涉及的主要有:

  1. 常見的 IPC 通訊方式;
  2. Binder 相關;
  3. 兩種序列化方式及其對比;

問題

IPC

  • Android 上的 IPC 跨程式通訊時如何工作的
  • 簡述 IPC?
  • 程式間通訊的機制
  • AIDL 機制
  • Bundle 機制

IPC 就是指程式之間的通訊機制,在 Android 系統中啟動 Activity/Service 等都涉及跨程式呼叫的過程。

Android 中有多種方式可以實現 IPC,

Bundle,用於在四大元件之間傳遞資訊,優點是使用簡單,缺點是隻能使用它支援的資料型別。Bundle 繼承自 BaseBundle,它通過內部維護的 ArrayMap<String, Object> 來儲存資料。當我們使用 put()get() 系列的方法的時候都會直接與其進行互動。ArrayMap<String, Object> 與 HashMap 類似,也是用作鍵值對的對映,但是它的實現方式與 SpareArray 類似,是基於兩個陣列來實現的對映。目的也是為了提升 Map 的效率。它在查詢某個雜湊值的時候使用的是二分查詢。

共享檔案,即兩個程式通過讀/寫同一個檔案來進行交換資料。由於 Android 系統是基於 Linux的,使得其併發讀/寫檔案可以沒有任何限制地進行,甚至兩個執行緒同時對同一個檔案進行寫操作都是被充許的。如果併發讀/寫,我們讀取出來的資料可能不是最新的。檔案共享方式適合在對資料同步要求不高的情況的程式之間進行通訊,並且要妥善處理併發讀/寫的問題。

另外,SharedPreferences 也是屬於檔案的一種,但是系統對於它的讀/寫有一定的快取策略,即在記憶體中有一份 SP 檔案的快取,因此在多程式模式下,系統對它的讀/寫變得不可靠,面對高併發的讀/寫訪問有很大機率會丟失資料。不建議在程式間通訊中使用 SP.

Messenger 是一種輕量級的 IPC 方案,它的底層實現是 AIDL,可以在不同程式中傳遞 Message. 它一次只處理一個請求,在服務端不需要考慮執行緒同步的問題,服務端不存在併發執行的情形。在遠端的服務中,宣告一個 Messenger,使用一個 Handler 用來處理收到的訊息,然後再 onBind() 方法中返回 Messenger 的 binder. 當客戶端與 Service 繫結的時候就可以使用返回的 Binder 建立 Messenger 並向該 Service 傳送服務。

    // 遠端服務的程式碼
    private Messenger messenger = new Messenger(new MessengerHandler(this));

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        ToastUtils.makeToast("MessengerService bound!");
        return messenger.getBinder();
    }

    // 客戶端 bind 服務的時候用到的 ServiceConnection
    private ServiceConnection msgConn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 這樣就拿到了遠端的 Messenger,向它傳送訊息即可
            boundServiceMessenger = new Messenger(service);
        }
        // ... ...
    }

    // 客戶端傳送訊息的程式碼
    Message message = Message.obtain(null, /*what=*/ MessengerService.MSG_SAY_SOMETHING);
    message.replyTo = receiveMessenger; // 客戶端用來接收服務端訊息的 Messenger
    Bundle bundle = new Bundle(); // 構建訊息
    bundle.putString(MessengerService.MSG_EXTRA_COMMAND, "11111");
    message.setData(bundle);
    boundServiceMessenger.send(message); // 傳送訊息給服務端
複製程式碼

AIDL:Messenger 是以序列的方式處理客戶端發來的訊息,如果大量訊息同時傳送到服務端,服務端只能一個一個處理,所以大量併發請求就不適合用 Messenger ,而且 Messenger 只適合傳遞訊息,不能跨程式呼叫服務端的方法。AIDL 可以解決併發和跨程式呼叫方法的問題。

AIDL 即 Android 介面定義語言。使用的時候只需要建立一個字尾名為 .aidl 的檔案,然後在編譯期間,編譯器會使用 aidl.exe 自動生成 Java 類檔案。

遠端的服務只需要實現 Stub 類,客戶端需要在 bindService() 的時候傳入一個 ServiceConnection,並在連線的回撥方法中將 Binder 轉換成為本地的服務。然後就可以在本地呼叫遠端服務中的方法了。

    // 遠端服務的程式碼
    private Binder binder = new INoteManager.Stub() {
        @Override
        public Note getNote(long id) {
            // ... ...
        }
    };
    // 繫結服務
    public IBinder onBind(Intent intent) {
        return binder;
    }

    // 客戶端程式碼
    private INoteManager noteManager;
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 獲取遠端的服務,轉型,然後就可以在本地使用了
            noteManager = INoteManager.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) { }
    };

    // 服務端訪問許可權控制:使用 Permission 驗證,在 manifest 中宣告
    <permission android:name="com.jc.ipc.ACCESS_BOOK_SERVICE"
        android:protectionLevel="normal"/>
    <uses-permission android:name="com.jc.ipc.ACCESS_BOOK_SERVICE"/>
    // 服務端 onBinder 方法中
    public IBinder onBind(Intent intent) {
        //Permission 許可權驗證
        int check = checkCallingOrSelfPermission("com.jc.ipc.ACCESS_BOOK_SERVICE");
        if (check == PackageManager.PERMISSION_DENIED) return null;
        return mBinder;
    }
複製程式碼

AIDL 支援的資料型別包括,1).基本資料型別;2).string 和 CharSequence;3).List 中只支援 ArrayList,並且其元素必須能夠被 AIDL 支援;4).Map 中只支援 HashMap,並且其元素必須能夠被 AIDL 支援;5).所有實現了 Parcelable 介面的物件;6).AIDL:所有 AIDL 介面本身也可以在AIDL檔案中使用。

注意!這裡使用了自定義的 Parcelable 物件:Note 類,但是 AIDL 不認識這個類,所以我們要建立一個與 Note 類同名的 AIDL 檔案:Note.aidl. 並且類必須與 aidl 檔案的包結構一致。

ContentProvider,主要用來對提供資料庫方面的共享。缺點是主要提供資料來源的 CURN 操作。

Socket,Socket 主要用在網路方面的資料交換。在 Android 系統中,啟動的 Zygote 程式的時候會啟動一個 ServerSocket. 當我們需要建立應用程式的時候會通過 Socket 與之進行通訊,這也是 Socket 的應用。

管道,另外在使用 Looper 啟動 MQ 的時候會在 Native 層啟動一個 Looper. Native 層的與 Java 層的 Looper 進行通訊的時候使用的是 epoll,也就是管道通訊機制。

Android 中的程式間通訊的機制

  • 為何需要進行 IPC?多程式通訊可能會出現什麼問題?

在 Android 系統中一個應用預設只有一個程式,每個程式都有自己獨立的資源和記憶體空間,其它程式不能任意訪問當前程式的記憶體和資源,系統給每個程式分配的記憶體會有限制。如果一個程式佔用記憶體超過了這個記憶體限制,就會報 OOM 的問題,很多涉及到大圖片的頻繁操作或者需要讀取一大段資料在記憶體中使用時,很容易報 OOM 的問題,為了解決應用記憶體的問題,Android 引入了多程式的概念,它允許在同一個應用內,為了分擔主程式的壓力,將佔用記憶體的某些頁面單獨開一個程式,比如 Flash、視訊播放頁面,頻繁繪製的頁面等。

實現的方式很簡單就是在 Manifest 中註冊 Activity 等的時候,使用 process 屬性指定一個程式即可。process 分私有程式和全域性程式,以 : 號開頭的屬於私有程式,其他應用元件不可以和他跑在同一個程式中;不以 : 號開頭的屬於全域性程式,其他應用可以通過 ShareUID 的方式和他跑在同一個程式中。此外,還有一種特殊方法,通過 JNI 在 native 層去 fork 一個新的程式。

但是多程式模式出現以下問題:

  1. 靜態成員和單例模式完全失效,因為沒有儲存在同一個空間上;
  2. 執行緒同步機制完全失效,因為執行緒處於不同的程式;
  3. SharedPreferences 的可靠性下降,因為系統對於它的讀/寫有一定的快取策略,即在記憶體中有一份 SP 檔案的快取;
  4. Application 多次建立。

解決這些問題可以依靠 Android 中的程式通訊機制,即 IPC,接上面的問題。

  • Binder 相關?

為什麼要設計 Binder,Binder 模型,高效的原因

Binder 是 Android 設計的一套程式間的通訊機制。Linux 本身具有很多種跨程式通訊方式,比如管道(Pipe)、訊號(Signal)和跟蹤(Trace)、插口(Socket)、訊息佇列(Message)、共享記憶體(Share Memory)和訊號量(Semaphore)。之所以設計出 Binder 是因為,這幾種通訊機制在效率、穩定性和安全性上面無法滿足 Android 系統的要求。

效率上 :Socket 作為一款通用介面,其傳輸效率低,開銷大,主要用在跨網路的程式間通訊和本機上程式間的低速通訊。訊息佇列和管道採用儲存-轉發方式,即資料先從傳送方快取區拷貝到核心開闢的快取區中,然後再從核心快取區拷貝到接收方快取區,至少有兩次拷貝過程。共享記憶體雖然無需拷貝,但控制複雜,難以使用。Binder 只需要一次資料拷貝,效能上僅次於共享記憶體。

穩定性:Binder 基於 C|S 架構,客戶端(Client)有什麼需求就丟給服務端(Server)去完成,架構清晰、職責明確又相互獨立,自然穩定性更好。共享記憶體雖然無需拷貝,但是控制負責,難以使用。從穩定性的角度講,Binder 機制是優於記憶體共享的。

安全性:Binder 通過在核心層為客戶端新增身份標誌 UID|PID,來作為身份校驗的標誌,保障了通訊的安全性。 傳統 IPC 訪問接入點是開放的,無法建立私有通道。比如,命名管道的名稱,SystemV 的鍵值,Socket 的 ip 地址或檔名都是開放的,只要知道這些接入點的程式都可以和對端建立連線,不管怎樣都無法阻止惡意程式通過猜測接收方地址獲得連線。

在 Binder 模型中共有 4 個主要角色,它們分別是:Client、Server、Binder 驅動和 ServiceManager. Binder 的整體結構是基於 C|S 結構的,以我們啟動 Activity 的過程為例,每個應用都會與 AMS 進行互動,當它們拿到了 AMS 的 Binder 之後就像是拿到了網路介面一樣可以進行訪問。如果我們將 Binder 和網路的訪問過程進行類比,那麼 Server 就是伺服器,Client 是客戶終端,ServiceManager 是域名伺服器(DNS),驅動是路由器。

  1. Client、Server 和 Service Manager 實現在使用者空間中,Binder 驅動程式實現在核心空間中;
  2. Binder 驅動程式和 ServiceManager 在 Android 平臺中已經實現,開發者只需要在使用者空間實現自己的 Client 和 Server;
  3. Binder 驅動程式提供裝置檔案 /dev/binder 與使用者空間互動,Client、Server 和 ServiceManager 通過 open 和 ioctl 檔案操作函式與 Binder 驅動程式進行通訊;
  4. Client 和 Server 之間的程式間通訊通過 Binder 驅動程式間接實現;
  5. ServiceManager 是一個守護程式,用來管理 Server,並向 Client 提供查詢 Server 介面的能力。

系統啟動的 init 程式通過解析 init.rc 檔案建立 ServiceManager. 此時會,先開啟 Binder 驅動,註冊 ServiceManager 成為上下文,最後啟動 Binder 迴圈。當使用到某個服務的時候,比如 AMS 時,會先根據它的字串名稱到緩衝當中去取,拿不到的話就從遠端獲取。這裡的 ServiceManager 也是一種服務。

  1. 客戶端首先獲取伺服器端的代理物件。所謂的代理物件實際上就是在客戶端建立一個服務端的“引用”,該代理物件具有服務端的功能,使其在客戶端訪問服務端的方法就像訪問本地方法一樣。
  2. 客戶端通過呼叫伺服器代理物件的方式向伺服器端傳送請求。
  3. 代理物件將使用者請求通過 Binder 驅動傳送到伺服器程式。
  4. 伺服器程式處理使用者請求,並通過 Binder 驅動返回處理結果給客戶端的伺服器代理物件。

Binder 高效的原因,當兩個程式之間需要通訊的時候,Binder 驅動會在兩個程式之間建立兩個對映關係:核心快取區和核心中資料接收快取區之間的對映關係,以及核心中資料接收快取區和接收程式使用者空間地址的對映關係。這樣,當把資料從 1 個使用者空間拷貝到核心緩衝區的時候,就相當於拷貝到了另一個使用者空間中。這樣只需要做一次拷貝,省去了核心中暫存這個步驟,提升了一倍的效能。實現記憶體對映靠的就是上面的 mmap() 函式。

(瞭解 Binder 相關的知識可以參考我的文章:《Android 系統原始碼-2:Binder 通訊機制》)

序列化

  • 序列化的作用,以及 Android 兩種序列化的區別
  • 序列化,Android 為什麼引入 Parcelable
  • 有沒有嘗試簡化 Parcelable 的使用

Android 中主要有兩種序列化的方式。

第一種是 Serializable. 它是 Java 提供的序列化方式,讓類實現 Serializable 介面就可以序列化地使用了。這種序列化方式的缺點是,它序列化的效率比較低,更加適用於網路和磁碟中資訊的序列化,不太適用於 Android 這種記憶體有限的應用場景。優點是使用方便,只需要實現一個介面就行了。

這種序列化的類可以使用 ObjectOutputStream/ObjectInputStream 進行讀寫。這種序列化的物件可以提供一個名為 serialVersionUID 的欄位,用來標誌類的版本號,比如當類的解構發生變化的時候將無法進行反序列化。

此外,

  1. 靜態成員變數不屬於物件,不會參與序列化過程
  2. 用 transient 關鍵字標記的成員變數不會參與序列化過程。

第二種方式是 Parcelable. 它是 Android 提供的新的序列化方式,主要用來進行記憶體中的序列化,無法進行網路和磁碟的序列化。它的缺點是使用起來比較繁瑣,需要實現兩個方法,和一個靜態的內部類。

Serializable 會使用反射,序列化和反序列化過程需要大量 I/O 操作,在序列化的時候會產生大量的臨時變數,從而引起頻繁的GC。Parcelable 自已實現封送和解封(marshalled & unmarshalled)操作不需要用反射,資料也存放在 Native 記憶體中,效率要快很多。

我自己嘗試過一些簡化 Parcelable 使用的方案,通常有兩種解決方案:第一種方式是使用 IDE 的外掛來輔助生成 Parcelable 相關的程式碼(外掛地址);第二種方案是使用反射,根據欄位的型別呼叫 wirte()read() 方法(效能比較低);第三種方案是基於註解處理,在編譯期間生成代理類,然後在需要覆寫的方法中呼叫生成的代理類的方法即可。

程式與執行緒

  • 程式與執行緒之間有什麼區別與聯絡?

一個程式就是一個執行單元,在 PC 和移動裝置上指一個程式或應用。在 Android 中,一個應用預設只有一個程式,每個程式都有自己獨立的資源和記憶體空間,其它程式不能任意訪問當前程式的記憶體和資源,系統給每個程式分配的記憶體會有限制。實現的方式很簡單就是在 Manifest 中註冊 Activity 等的時候,使用 process 屬性指定一個程式即可。process 分私有程式和全域性程式,以 : 號開頭的屬於私有程式,其他應用元件不可以和他跑在同一個程式中;不以 : 號開頭的屬於全域性程式,其他應用可以通過 ShareUID 的方式和他跑在同一個程式中

Android 系統啟動的時候會先啟動 Zygote 程式,當我們需要建立應用程式程式的時候的會通過 Socket 與之通訊,Zygote 通過 fork 自身來建立我們的應用程式的程式。

不應只是簡單地講述兩者之間的區別,同時涉及系統程式的建立,應用程式的建立,以及如何在程式中使用多程式等。

執行緒是 CPU 排程的最小單元,一個程式可包含多個執行緒。Java 執行緒的實現是基於一對一的執行緒模型,即通過語言級別層面程式去間接呼叫系統的核心執行緒。核心執行緒由作業系統核心支援,由作業系統核心來完成執行緒切換,核心通過操作排程器進而對執行緒執行排程,並將執行緒的任務對映到各個處理器上。由於我們編寫的多執行緒程式屬於語言層面的,程式一般不會直接去呼叫核心執行緒,取而代之的是一種輕量級的程式(Light Weight Process),也是通常意義上的執行緒。由於每個輕量級程式都會對映到一個核心執行緒,因此我們可以通過輕量級程式呼叫核心執行緒,進而由作業系統核心將任務對映到各個處理器。這種輕量級程式與核心執行緒間1對1的關係就稱為一對一的執行緒模型。

一對一的執行緒模型

(瞭解 Android 系統啟動過程和虛擬機器記憶體模型 JMM,請參考我的文章:Android 系統原始碼-1:Android 系統啟動流程原始碼分析JVM掃盲-3:虛擬機器記憶體模型與高效併發


Android 高階面試系列文章,關注作者及時獲取更多面試資料

本系列以及其他系列的文章均維護在 Github 上面:Github / Android-notes,歡迎 Star & Fork. 如果你喜歡這篇文章,願意支援作者的工作,請為這篇文章點個贊?!

相關文章