進階之路 | 奇妙的 IPC 之旅

前言

本文已經收錄到個人Github我的博客,歡迎大佬們光臨寒舍:html

個人GIthub博客java

學習清單:

  • IPC的基礎概念
  • 多進程和多線程的概念
  • Android中的序列化機制和Binder
  • Android中的IPC方式
  • Binder鏈接池的概念及運用
  • 各類IPC的優缺點

一.爲何要學習IPC

IPCInter-Process Communication的縮寫,含義是進程間通訊,是指兩個進程之間進行數據交換的過程。linux

有些讀者可能疑惑: "那什麼是進程呢?什麼是線程呢?多進程和多線程有什麼區別呢?"android

  • 進程:是資源分配的最小單位,通常指一個執行單元,在PC和移動設備上指一個程序應用
  • 線程:CPU調度的最小單位,線程是一種有限的系統資源。

二者關係:一個進程可包含多個線程,即一個應用程序上能夠同時執行多個任務。git

  • 主線程(UI線程):UI操做
  • 有限個子線程:耗時操做

注意:不可在主線程作大量耗時操做,會致使ANR(應用無響應)。解決辦法:將耗時任務放在線程中。github

IPC不是Android所特有的,Android中最有特點的IPC方式是Binder。而平常開發中涉及到的知識:AIDL,插件化,組件化等等,都離不開Binder。因而可知,IPC是挺重要的。數據庫

二.核心知識點概括

2.1 Android中的多進程模式

Q1:開啓多線程的方式緩存

  • (經常使用)在AndroidMenifest中給四大組件指定屬性android:process

precess的命名規則:安全

  • 默認進程:沒有指定該屬性則運行在默認進程,其進程名就是包名
  • 以「:」爲命名開頭的進程:「:」的含義是在進程名前面加上包名,屬於當前應用私有進程
  • 完整命名的進程:屬於全局進程,其餘應用能夠經過ShareUID方式和他跑在用一個進程中(須要ShareUID和簽名相同)。
  • (不經常使用)經過JNI在native層fork一個新的進程。

Q2:多進程模式的運行機制服務器

Andoird爲每一個進程分配了一個獨立的虛擬機,不一樣虛擬機在內存分配上有不一樣的地址空間,這也致使了不一樣虛擬機中訪問同一個對象會產生多份副本

帶來四個方面的問題:

  • 靜態變量和單例模式失效-->緣由:不一樣虛擬機中訪問同一個對象會產生多份副本
  • 線程同步機制失效-->緣由:內存不一樣,線程沒法同步。
  • SharedPreference的可靠性降低-->緣由:底層是經過讀寫XML文件實現的,發生併發問題。
  • Application屢次建立-->緣由:Android系統會爲新的進程分配獨立虛擬機,至關於應用從新啓動了一次。

2.2 IPC基礎概念

這裏主要介紹三方面內容:

  • Serializable
  • Parcelable
  • Binder

只有熟悉這三方面的內容,才能更好理解IPC的各類方式

2.2.1 什麼是序列化

  • 含義:序列化表示將一個對象轉換成可存儲或可傳輸的狀態。序列化後的對象能夠在網絡上進行傳輸,也能夠存儲到本地。
  • 使用場景:須要經過IntentBinder等傳輸類對象就必須完成對象的序列化過程。
  • 兩種方式:實現Serializable/Parcelable接口。

2.2.2 Serializable接口

Java提供的序列化接口,使用方式比較簡單:

  • 實體類實現Serializable
  • 手動設置/系統自動生成serialVersionUID
//Serializable Demo
public class Person implements Serializable{
    private static final long serialVersionUID = 7382351359868556980L;
    private String name;
    private int age;
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}
複製代碼

這裏特別注意一下serialVersionUID

  • 含義:是Serializable接口中用來輔助序列化和反序列化過程。
  • 注意:原則上序列化後的數據中的serialVersionUID要和當前類的serialVersionUID 相同才能正常的序列化。當類發生很是規性變化(修改了類名/修改了成員變量的類型)的時候,序列化失敗。

2.2.3 Parcelable接口

是Android中的序列化接口,使用的時候,類中須要實現下面幾點:

  • 實現Parcelable接口
  • 內容描述
  • 序列化方法
  • 反序列化方法
public class User implements Parcelable {
    

    public int userId;
    public String userName;
    public boolean isMale;

    public Book book;

    public User() {
    }

    public User(int userId, String userName, boolean isMale) {
        this.userId = userId;
        this.userName = userName;
        this.isMale = isMale;
    }

    //返回內容描述
    public int describeContents() {
        return 0;
    }
    
	//序列化
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(userId);
        out.writeString(userName);
        out.writeInt(isMale ? 1 : 0);
        out.writeParcelable(book, 0);
    }

    //反序列化
    public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
        //從序列化的對象中建立原始對象
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

        public User[] newArray(int size) {
            return new User[size];
        }
    };

    //從序列化的對象中建立原始對象
    private User(Parcel in) {
        userId = in.readInt();
        userName = in.readString();
        isMale = in.readInt() == 1;
        book = in.readParcelable(Thread.currentThread().getContextClassLoader());
    }

    @Override
    public String toString() {
        return String.format("User:{userId:%s, userName:%s, isMale:%s}, with child:{%s}",
                userId, userName, isMale, book);
    }

}

複製代碼

2.2.4 Serializable和Parcelable接口的比較

Serializable接口 Parcelable接口
平臺 Java Andorid
序列化原理 將一個對象轉換成可存儲或者可傳輸的狀態 將對象進行分解,且分解後的每一部分都是傳遞可支持的數據類型
優缺點 優勢:使用簡單 缺點:開銷大(由於須要進行大量的IO操做) 優勢:高效 缺點:使用麻煩
使用場景 將對象序列化到存儲設備或者經過網絡傳輸 主要用在內存序列化上

2.2.5 Binder

Q1:Binder是什麼

  • 從API角度:是一個類,實現IBinder接口。
  • 從IPC角度:是Android中的一種跨進程通訊方式。
  • 從Framework角度:是ServiceManager鏈接各類Manager和相應ManagerService的橋樑。
  • 從應用層:是客戶端和服務端進行通訊的媒介。客戶端經過它可獲取服務端提供的服務或者數據。

Q2:Android是基於Linux內核基礎上設計的,卻沒有把管道/消息隊列/共享內存/信號量/Socket等一些IPC通訊手段做爲Android的主要IPC方式,而是新增了Binder機制,其優勢有:

A1:傳輸效率高、可操做性強

傳輸效率主要影響因素是內存拷貝的次數,拷貝次數越少,傳輸速率越高。幾種數據傳輸方式比較

方式 拷貝次數 操做難度
Binder 1 簡易
消息隊列 2 簡易
Socket 2 簡易
管道 2 簡易
共享內存 0 複雜

從Android進程架構角度分析:對於消息隊列、Socket和管道來講,數據先從發送方的緩存區拷貝到內核開闢的緩存區中,再從內核緩存區拷貝到接收方的緩存區,一共兩次拷貝,如圖:

消息隊列、Socket和管道的數據拷貝

對Binder來講:數據從發送方的緩存區拷貝到內核的緩存區,而接收方的緩存區與內核的緩存區是映射到同一塊物理地址的,節省了一次數據拷貝的過程

A2:實現C/S架構方便

Linux的衆IPC方式除了Socket之外都不是基於C/S架構,而Socket主要用於網絡間的通訊且傳輸效率較低。Binder基於C/S 架構 ,Server端與Client端相對獨立,穩定性較好。

A3:安全性高

傳統Linux IPC的接收方沒法得到對方進程可靠的UID/PID,從而沒法鑑別對方身份;而Binder機制爲每一個進程分配了UID/PID且在Binder通訊時會根據UID/PID進行有效性檢測。

Q3:Binder框架定義了四個角色呢?

A1:Server&Client

服務器&客戶端。在Binder驅動和Service Manager提供的基礎設施上,進行Client-Server之間的通訊。

A2:ServiceManager:

服務管理者,將Binder名字轉換爲Client中對該Binder的引用,使得Client能夠經過Binder名字得到Server中Binder實體的引用。

ServiceManager工做機制

A3:Binder驅動

  • 與硬件設備沒有關係,其工做方式與設備驅動程序是同樣的,工做於內核態。
  • 提供open()mmap()poll()、**ioctl()**等標準文件操做。
  • 以字符驅動設備中的misc設備註冊在設備目錄/dev下,用戶經過/dev/binder訪問該它。
  • 負責進程之間binder通訊的創建,傳遞,計數管理以及數據的傳遞交互等底層支持。
  • 驅動和應用程序之間定義了一套接口協議,主要功能由**ioctl()**接口實現,因爲ioctl()靈活、方便且可以一次調用實現先寫後讀以知足同步交互,所以沒必要分別調用write()和read()接口。
  • 其代碼位於linux目錄的drivers/misc/binder.c中。

ioctl(input/output control)是一個專用於設備輸入輸出操做的系統調用,該調用傳入一個跟設備有關的請求碼,系統調用的功能徹底取決於請求碼

Q4:Binder 工做原理是什麼

  • 服務器端:在服務端建立好了一個Binder對象後,內部就會開啓一個線程用於接收Binder驅動發送的消息,收到消息後會執行onTranscat(),並按照參數執行不一樣的服務端代碼。
  • Binder驅動:在服務端成功建立Binder對象後,Binder驅動也會建立一個mRemote對象(也是Binder類),客戶端可藉助它調用transcat()便可向服務端發送消息。
  • 客戶端:客戶端要想訪問Binder的遠程服務,就必須獲取遠程服務的Binder對象在Binder驅動層對應的mRemote引用。當獲取到mRemote對象的引用後,就能夠調用相應Binder對象的暴露給客戶端的方法。

Binder工做機制

當發出遠程請求後客戶端會掛起,直到返回數據纔會喚醒Client

Q5:當服務端進程異常終止的話,形成Binder死亡的話,怎麼辦?

在客戶端綁定遠程服務成功後,給Binder設置死亡代理,當Binder死亡的時候,咱們會收到通知,從而從新發起鏈接請求。

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
        @Override
        public void binderDied(){
        if(mBookManager == null){
        return;
        }
        mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
        mBookManager = null;
        // TODO:這裏從新綁定遠程Service
        }
        }
複製代碼
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
複製代碼

2.3 Android 中的IPC方式

Android中的IPC方式有不少種,可是都與Binder有或多或少的關係

Android中各類IPC方式的關係

2.3.1 Bundle

  • 原理:Bundle底層實現了Parcelable接口,它可方便的在不一樣的進程中傳輸。
  • 注意:Bundle不支持的數據類型沒法在進程中被傳遞。

小課堂測試:在A進程進行計算後的結果不是Bundle所支持的數據類型,該如何傳給B進程?

Answer: 將在A進程進行的計算過程轉移到B進程中的一個Service裏去作,這樣可成功避免進程間的通訊問題。

  • Intent和Bundle的區別與聯繫:Intent底層實際上是經過Bundle進行傳遞數據的;使用起來,Intent比較簡單,Bundle比較複雜;Intent旨在數據傳遞,bundle旨在存取數據

2.3.2 文件共享

  • 概念:兩個進程經過讀/寫同一個文件來交換數據。好比A進程把數據寫入文件,B進程經過讀取這個文件來獲取數據。
  • 適用場景:對數據同步要求不高的進程之間進行通訊,而且要妥善處理併發讀/寫的問題。
  • 特殊狀況:SharedPreferences也是文件存儲的一種,但不建議採用。由於系統對SharedPreferences的讀/寫有必定的緩存策略,即在內存中有一份該文件的緩存,所以在多進程模式下,其讀/寫會變得不可靠,甚至丟失數據。

2.3.3 AIDL

2.3.3.1 概念

AIDL(Android Interface Definition Language,Android接口定義語言):若是在一個進程中要調用另外一個進程中對象的方法,可以使用AIDL生成可序列化的參數,AIDL會生成一個服務端對象的代理類,經過它客戶端實現間接調用服務端對象的方法。

2.3.3.2 支持的數據類型
  • 基本數據類型
  • String和CharSequence

想了解String和CharSequence區別的讀者,能夠看下這篇文章:String和CharSequence的區別

  • ArrayList、HashMap且裏面的每一個元素都能被AIDL支持
  • 實現Parcelable接口的對象
  • 全部AIDL接口自己

注意:除了基本數據類型,其它類型的參數必須標上方向:in、out或inout,用於表示在跨進程通訊中數據的流向。

2.3.3.3 兩種AIDL文件
  • 用於定義parcelable對象,以供其餘AIDL文件使用AIDL中非默認支持的數據類型的。
  • 用於定義方法接口,以供系統使用來完成跨進程通訊的。

注意:

  • 自定義的Parcelable對象必須把java文件和自定義的AIDL文件顯式的import進來,不管是否在同一包內。
  • AIDL文件用到自定義Parcelable的對象,必須新建一個和它同名的AIDL文件,並在其中聲明它爲Parcelable類型。
2.3.3.4 本質,關鍵類和方法

a:本質是系統提供了一套可快速實現Binder的工具。

b:關鍵類和方法是什麼?

  • AIDL接口:繼承IInterface
  • Stub類Binder的實現類,服務端經過這個類來提供服務。
  • Proxy類:服務器的本地代理,客戶端經過這個類調用服務器的方法。
  • asInterface():客戶端調用,將服務端的返回的Binder對象,轉換成客戶端所須要的AIDL接口類型對象。

返回對象:

  • 若客戶端和服務端位於同一進程,則直接返回Stub對象自己;
  • 不然,返回的是系統封裝後的Stub.proxy對象。
  • asBinder():返回代理Proxy的Binder對象。
  • onTransact():運行服務端的Binder線程池中,當客戶端發起跨進程請求時,遠程請求會經過系統底層封裝後交由此方法來處理。
  • transact():運行在客戶端,當客戶端發起遠程請求的同時將當前線程掛起。以後調用服務端的onTransact()直到遠程請求返回,當前線程才繼續執行。

AIDL工做機制

2.3.3.5 實現方法

若是感興趣的讀者想要了解具體的AIDL實現IPC的流程,筆者分享一篇文章:android跨進程通訊(IPC):使用AIDL

A.服務端:

  • 建立一個aidl文件
  • 建立一個Service,實現AIDL的接口函數並暴露AIDL接口。

B.客戶端:

  • 經過bindService綁定服務端的Service;
  • 綁定成功後,將服務端返回的Binder對象轉化成AIDL接口所屬的類型,進而調用相應的AIDL中的方法。

總結:服務端裏的某個Service給和它綁定的特定客戶端進程提供Binder對象,客戶端經過AIDL接口的靜態方法asInterface() 將Binder對象轉化成AIDL接口的代理對象,經過這個代理對象就能夠發起遠程調用請求。

2.3.3.6 可能產生ANR的情形

A.客戶端:

  • 調用服務端的方法是運行在服務端的Binder線程池中,若主線程所調用的方法裏執行了較耗時的任務,同時會致使客戶端線程長時間阻塞,易致使客戶端ANR。
  • 在**onServiceConnected()onServiceDisconnected()**裏直接調用服務端的耗時方法,易致使客戶端ANR。

B.服務端:

  • 服務端的方法自己就運行在服務端的Binder線程中,可在其中執行耗時操做,而無需再開啓子線程
  • 回調客戶端Listener的方法是運行在客戶端的Binder線程中,若所調用的方法裏執行了較耗時的任務,易致使服務端ANR。

解決客戶端頻繁調用服務器方法致使性能極大損耗的辦法:實現觀察者模式

即當客戶端關注的數據發生變化時,再讓服務端通知客戶端去作相應的業務處理。

2.3.3.7 解註冊失敗的問題
  • 緣由: Binder進行對象傳輸實際是經過序列化和反序列化進行,即Binder會把客戶端傳遞過來的對象從新轉化並生成一個新的對象,雖然在註冊和解註冊的過程當中使用的是同一個客戶端傳遞的對象,但通過Binder傳到服務端後會生成兩個不一樣的對象。另外,屢次跨進程傳輸的同一個客戶端對象會在服務端生成不一樣的對象,但它們在底層的Binder對象是相同的。
  • 解決辦法:當客戶端解註冊的時候,遍歷服務端全部的Listener,找到和解註冊Listener具備相同的Binder對象的服務端Listener,刪掉便可。

須要用到RemoteCallBackList:Android系統專門提供的用於刪除跨進程listener的接口。其內部自動實現了線程同步的功能。

2.3.4 Messager

Q1.什麼是Messager?

A1:Messager是輕量級的IPC方案,經過它可在不一樣進程中傳遞Message對象。

Messenger.send(Message);
複製代碼

Q2:特色是什麼

  • 底層實現是AIDL,即對AIDL進行了封裝,更便於進行進程間通訊。
  • 其服務端以串行的方式來處理客戶端的請求,不存在併發執行的情形,故無需考慮線程同步的問題。
  • 可在不一樣進程中傳遞Message對象,Messager可支持的數據類型即Messenge可支持的數據類型。

Messenge可支持的數據類型:

  • arg一、arg二、what字段:int型數據
  • obj字段:Object對象,支持系統提供的Parcelable對象
  • setData:Bundle對象
  • 有兩個構造函數,分別接收Handler對象和Binder對象。

Q3:實現的方法

讀者若是對Messenger的具體使用感興趣的話,能夠看下這篇文章:IPC-Messenger使用實例

A1:服務端:

  • 建立一個Service用於提供服務;
  • 其中建立一個Handler用於接收客戶端進程發來的數據
  • 利用Handler建立一個Messenger對象;
  • 在Service的onBind()中返回Messenger對應的Binder對象。

A2:客戶端:

  • 經過bindService綁定服務端的Service;

  • 經過綁定後返回的IBinder對象建立一個Messenger,進而可向服務器端進程發送Message數據。(至此只完成單向通訊)

  • 在客戶端建立一個Handler並由此建立一個Messenger,並經過Message的replyTo字段傳遞給服務器端進程。服務端經過讀取Message獲得Messenger對象,進而向客戶端進程傳遞數據。(完成雙向通訊)

    Messenger通訊

Q4:缺點:

  • 主要做用是傳遞 Message,難以實現遠程方法調用。
  • 以串行的方式處理客戶端發來的消息的,不適合高併發的場景。

解決方式:使用AIDL的方式處理IPC以應對高併發的場景

2.3.5 ContentProvider

ContentProvider是Android提供的專門用來進行不一樣應用間數據共享的方式,底層一樣是經過Binder實現的。

  • 除了**onCreat()**運行在UI線程中,其餘的query()、update()、insert()、delete()和getType()都運行在Binder線程池中。
  • CRUD四大操做存在多線程併發訪問,要注意在方法內部要作好線程同步。
  • 一個SQLiteDatabase內部對數據庫的操做有同步處理,但多個SQLiteDatabase之間沒法同步。

2.3.6 Socket

Socket不只能夠跨進程,還能夠跨設備通訊

Q1:使用類型是什麼?

  • 流套接字:基於TCP協議,採用流的方式提供可靠的字節流服務。
  • 數據流套接字:基於UDP協議,採用數據報文提供數據打包發送的服務。

Q2:實現方法是什麼?

A1:服務端:

  • 建立一個Service,在線程中創建TCP服務、監聽相應的端口等待客戶端鏈接請求;
  • 與客戶端鏈接時,會生成新的Socket對象,利用它可與客戶端進行數據傳輸;
  • 與客戶端斷開鏈接時,關閉相應的Socket並結束線程。

A2:客戶端:

  • 開啓一個線程、經過Socket發出鏈接請求;
  • 鏈接成功後,讀取服務端消息;
  • 斷開鏈接,關閉Socket。

2.3.7 優缺點比較

名稱 優勢 缺點 適用場景
Bundle 簡單易用 只能傳輸Bundle支持的數據類型 四大組件間的進程間通訊
文件共享 簡單易用 不適合高併發場景,沒法作到進程間的即時通訊 無併發訪問,交換簡單數據且實時性不高
AIDL 支持一對多併發和實時通訊 使用稍複雜,須要處理線程同步 一對多且有RPC需求
Messenger 支持一對多串行通訊 不能很好處理高併發,不支持RPC,只能傳輸Bundle支持的數據類型 低併發的一對多
ContentProvider 支持一對多併發數據共享 可理解爲受約束的AIDL 一對多進程間數據共享
Socket 支持一對多併發數據共享 實現細節繁瑣 網絡數據交換

2.4 Binder鏈接池

有多個業務模塊都須要AIDL來進行IPC,此時須要爲每一個模塊建立特定的aidl文件,那麼相應的Service就會不少。必然會出現系統資源耗費嚴重、應用過分重量級的問題。所以須要Binder鏈接池,經過將每一個業務模塊的Binder請求統一轉發到一個遠程Service中去執行的方式,從而避免重複建立Service。

Q1:工做原理是什麼

每一個業務模塊建立本身的AIDL接口並實現此接口,而後向服務端提供本身的惟一標識和其對應的Binder對象。服務端只須要一個Service,服務器提供一個queryBinder接口,它會根據業務模塊的特徵來返回相應的Binder對像,不一樣的業務模塊拿到所需的Binder對象後就可進行遠程方法的調用了。

Binder鏈接池工做原理

Q2:實現方式是什麼

讀者若是對具體的實現方式感興趣的話,能夠看一下這篇文章:Android IPC機制(四):細說Binder鏈接池

  • 爲每一個業務模塊建立AIDL接口並具體實現;
  • 爲Binder鏈接池建立AIDL接口IBinderPool.aidl並具體實現;
  • 遠程服務BinderPoolService的實現,在onBind()返回實例化的IBinderPool實現類對象;
  • Binder鏈接池的具體實現,來綁定遠程服務。
  • 客戶端的調用。

三.碎碎念

恭喜你,已經完成了此次奇妙的IPC之旅了,若是你感到對概念仍是有點模糊不清的話,不要緊,很正常,不用太糾結於細節,你能夠繼續進行下面的旅程了,將來的你,再看這篇文章,也許會有更深的體會,到時候就會有茅舍頓開的感受了。將來的你,必定會更優秀!!!

路漫漫其修遠兮,吾將上下而求索。《離騷》--屈原


若是文章對您有一點幫助的話,但願您能點一下贊,您的點贊,是我前進的動力

本文參考連接:

相關文章
相關標籤/搜索