Android 高級面試-2:IPC 相關

內容

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

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

問題

IPC

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

IPC 就是指進程之間的通訊機制,在 Android 系統中啓動 Activity/Service 等都涉及跨進程調用的過程。android

Android 中有多種方式能夠實現 IPC,git

Bundle,用於在四大組件之間傳遞信息,優勢是使用簡單,缺點是隻能使用它支持的數據類型。Bundle 繼承自 BaseBundle,它經過內部維護的 ArrayMap<String, Object> 來存儲數據。當咱們使用 put()get() 系列的方法的時候都會直接與其進行交互。ArrayMap<String, Object> 與 HashMap 相似,也是用做鍵值對的映射,可是它的實現方式與 SpareArray 相似,是基於兩個數組來實現的映射。目的也是爲了提高 Map 的效率。它在查找某個哈希值的時候使用的是二分查找。github

共享文件,即兩個進程經過讀/寫同一個文件來進行交換數據。因爲 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. 若是你喜歡這篇文章,願意支持做者的工做,請爲這篇文章點個贊👍!

相關文章
相關標籤/搜索