【Android】一篇足矣:全面方位詳解跨進程通信(IPC機制)

前言:IPC(inter-Process-Communication)進程間通信,用于兩個進程之間進行數據交互的過程,任何操作系統都有IPC機制,但不同的操作系統有著不同的通信方式,Android系統是一種基于Linux內核的移動操作系統,Linux的跨進程通信主要通過管道、共享內存、內存映射等實現的,但Android有自己的進程間通信機制,最具代表性的就是Binder。

本文篇幅雖長,但可以復習到的知識卻有很多,通過閱讀你能了解什么是IPC機制(Linux&Android)、多進程模式的用法、跨進城通訊的方式、序列化反序列化、Binder的作用、Bundle的使用、ContentProvider、Messenger、Socket等知識點。

尊重原著,參考資料如下:任志強《Android開發藝術探索》、Serializable和ParcelableServiceManagerAIDL的使用詳解AIDL的工作原理ContentProvider、Binder原理大量參考寫給 Android 應用工程師的 Binder 原理剖析,寫的很好請去支持!

碼字不易,耗時三天學習和理解最后歸納出了這篇文章,轉載請標明出處。

簡單介紹一下IPC機制

  • 含義:進程間通信、跨進程通信,進程是資源分配的最小單位,操作系統的一個執行單元,即一個程序或者應用。一個進程至少包含一個線程,線程是CPU調度的最小單位,是一種有限的系統資源。
  • 方式:Binder、AIDL、Socket、文件共享、ContentProvide等。
  • 場景:多進程、一種是一個應用由于特殊原因需要多進程,特殊原因比如模塊需要運行在單獨的進程中,或者應用為了獲取更大的內存空間(系統給單個應用分配的空間有限,不同手機設備大小不一,但相差不大)。另一種是當前應用需要向其他應用獲取數據。

Android中的多進程模式

指的是指定應用內部開啟多進程模式,只有一中方法,給四大組件在AndroidMenifest中指定android:process屬性。如下圖方式:
項目實例圖

上圖可以看到process屬性有兩種寫法,這兩種寫法是有區別的,以”:“開頭的相當于當前應用的私有進程,其他應用的組件不可以和他跑在同一進程中,而另一種則屬于全局進程,其他應用通過ShareUID方式和它跑在同一進程,那UID是什么呢?UID是Android系統為每一個應用分配的用戶身份證明來區分不同的應用。

  • 具有相同UID的應用能共享數據(可以互相訪問對方的私有數據data目錄、組件信息等)
  • 跑在同一進程需要相同的UID+相同的簽名(還可以訪問共享內存數據)
    相同UID的應用實現資源共享: 首先需要在兩個應用的AndroidManifest.xml中都定義相同的sharedUserId,如:android:sharedUserId="com.test"。
    uid共享res資源實例
    假設我們有這樣一個需求,A和B是兩個應用,現在要求在A中獲取B的一張名字為send_bg的圖片資源,那么先將A和B的注冊文件的AndroidManifest.xml節點添加sharedUserId,并且賦值相同,然后在A中可以用如下方式實現:
    Context thdContext = null;
    try {
        thdContext = createPackageContext(
                "com.example.testdatabase",
                Context.CONTEXT_IGNORE_SECURITY);
        Resources res = thdContext.getResources();  
        int menuIconId = res.getIdentifier("send_bg", "drawable",  
                "com.example.testdatabase");  
        Drawable drawable = res.getDrawable(menuIconId);  
        mButton.setBackgroundDrawable(drawable);
    } catch (NameNotFoundException e) {
        e.printStackTrace();
    }

uid共享database實例
假設我們有這樣一個需求,A和B是兩個應用,現在要求在A中要獲取B的數據庫,那么先將A和B的注冊文件的AndroidManifest.xml節點添加sharedUserId,并且賦值相同,然后在A中可以用如下方式實現:

    Context thdContext = null;
    try {
        thdContext = createPackageContext(
                "com.example.testdatabase",
                Context.CONTEXT_IGNORE_SECURITY);
        String dbPath = thdContext.getDatabasePath("BookStore.db")
                .getAbsolutePath();
        SQLiteDatabase db = SQLiteDatabase.openDatabase(dbPath,
                null, SQLiteDatabase.OPEN_READWRITE);
        Cursor cursor = db.query("Book", null, null, null, null,
                null, null);
        if (cursor.moveToFirst()) {
            do {
                String name = cursor.getString(cursor
                        .getColumnIndex("name"));
                Log.d(TAG, "name: " + name);
            } while (cursor.moveToNext());
        }
    } catch (NameNotFoundException e) {
        e.printStackTrace();
    }

同應用開啟多進程后,就運行在不同的虛擬機里了,不同的虛擬機在內存分配上有不同的地址空間,造成的問題主要是:

  • 靜態成員和單例模式失效
  • 線程同步機制失效
  • SharePreferences可靠性下降,底層是通過讀寫XML文件來實現的,不支持兩個進程同時讀寫操作,會丟失數據。
  • Application會多次創建,因為系統要為新建的進程分配獨立的虛擬機,這個過程就是啟動一個新應用的過程,相當于系統又把這個應用重新啟動了一遍導致重建新的Application。運行在同一進程中的組件屬于同一個虛擬機和Application,反之則反。

利用跨進程通信解決上述問題

簡單說就是:通過使數據持久化存儲在設備上來傳輸數據。
需要通過Serializable接口/Parcelable接口來完成對象的序列化,再通過Intent和Binder傳輸數據或者通過Serializable將對象持久化到存儲設備上或者網絡傳輸給其他應用。

對比 Parcelable Serializable
實現方式 實現Parcelable接口 實現Serializable接口
屬于 android 專用,基于DVM Java自帶,基于JVM
內存消耗 優秀 一般,相對開銷大
讀寫數據 內存中直接進行讀寫 通過使用IO流的形式將數據讀寫入在硬盤上
持久化 不可以 可以
速度 優秀 一般
Android中使用場景 跨進城數據的傳輸 數據持久化存儲設備、網絡傳輸數據

Serializable接口,基于JVM上的持久化數據

Java提供的一個序列化接口,它是一個空接口,為對象提供標準的序列化和反序列化。Serializable使用IO讀寫存儲在硬盤上。序列化過程使用了反射技術,并且期間產生臨時對象。Serializable在序列化的時候會產生大量的臨時變量,從而引起頻繁的GC。優點代碼少。
使用方法:在需要序列化的類里面實現Serializable接口并聲明一個標識serialVersionUID 如

public class User implements Serializable {
    //可以不寫,系統根據結構計算當前類的hash值自動分配,增加或刪除結構從新計算會hash值會不一致,導致反序列化失敗程序crash。
    //手動指定,最大程度反序列化成功,建議用插件自動生成。
    private static final long serialVersionUID =463442362345654634L;
    ... ...
}

剩下的工作系統會自動完成,進行序列化和反序列化的方法:采用ObjectOutputStream和ObjectInputStream。

//序列化過程
User user = new User("chou",27,man);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.tex"));
out.writeObject(user);
out.close;

//反序列化過程
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.tex"));
User newUser = (User)in.readObject();
in.close;

Parcelable接口,Android專用,基于DVM上的持久化數據,Parcelable 的讀取順序必須一致,不然有的機型會出錯閃退(遇到過這種bug)

Parcelable是直接在內存中讀寫,我們知道內存的讀寫速度肯定優于硬盤讀寫速度,所以Parcelable序列化方式性能上要優于Serializable方式很多。但是代碼寫起來相比Serializable方式麻煩一些。
代碼示例:User(Parcel source) 參數的順序一定需要與 writeToParcel(Parcel dest, int flags)參數順序

public class User implements Parcelable {
      private String name;
      private int age;
      private String sex;
  
      public User(String name, int age, String sex ) {
          this.name = name;
          this.age = age;
          this.sex = sex;
      }

      //返回當前對象的內容描述
      @Override
      public int describeContents() {
          return 0;
      }

      //序列化操作,將對象寫入序列化結構中
      @Override
      public void writeToParcel(Parcel dest, int flags) {
          dest.writeString(name);
          dest.writeInt(age);
          dest.writeString(sex);
      }

      //反序列化操作
      public static final Creator<User> CREATOR = new Creator<User>() {
          //從序列化后的對象中創建原始對象
          @Override
          public User createFromParcel(Parcel in) {
              return new User(in);
          }
          //創建指定長度的原始對象數組
          @Override
          public User[] newArray(int size) {
              return new User[size];
          }
      };
      //從序列化后的對象中創建原始對象
      public User(Parcel source) {
          this.name = source.readString();
          this.age = source.readInt();
          this.sex = source.readString();
      }
  }

這里先說一下Parcel,Parcel內部包裝了可序列化的數據,可以在Binder中自由傳輸。從上述代碼中可以看出,在序列化過程中需要實現的功能有序列化、反序列化和內容描述。序列化功能由writeToParcel方法來完成,最終是通過Parcel中的一系列write方法來完成的;反序列化功能由CREATOR來完成,其內部標明了如何創建序列化對象和數組,并通過Parcel的一系列read方法來完成反序列化過程;內容描述功能由describeContents方法來完成,幾乎在所有情況下這個方法都應該返回0,僅當當前對象中存在文件描述符時,此方法返回1。需要注意的是,在User(Parcel in)方法中,由于book是另一個可序列化對象,所以它的反序列化過程需要傳遞當前線程的上下文類加載器,否則會報無法找到類的錯誤。詳細的方法說明請參看下面表格

方法 功能 標記位
createFromParcel(Parcel in) 從序列化后的對象中創建原始對象
newArray(int size) 創建指定長度的原始對象數組
User(Parcel in) 從序列化后的對象中創建原始對象
writeToParce(Parcel out, int flags) 將當前對象寫入序列化結構中,其中flags標識有兩種值: 0或者1 (參見右側標記位)。為1時標識當前對象需要作為返回值返回,不能立即釋放資源,幾乎所有情況都為0 PARCELABLE_WRITE_RETURN_VALUE
describeContents 返回當前對象的內容描述。如果含有文件描述符,返回1(參見右側標記位),否則返回0, 幾乎所有情況都返回0 ONTENTS_FILE_DESCRIPTOR
  • 將這個數據放到 intent 或者 Bundle 中完成傳遞
 Intent intent = new Intent(MainActivity.this, ServerService.class);

  intent.setExtrasClassLoader(User.class.getClassLoader());

  Bundle bundle = new Bundle();
  bundle.putParcelable("User", new User("chou",27,"sex"));
  intent.putExtras(bundle); 

  bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
  • 在需要接收目標組件中完成接收操作
  Bundle bundle = intent.getExtras();
  User user = bundle.getParcelable("User");

Binder重頭戲

binder是Android中的一種跨進程通信方式,是Android的一個類,它實現了IBinder接口。

  1. 從IPC角度講:也可以把它當成一種虛擬的物理設備,它的驅動是/dev/binder,該通訊方式在Linux中沒有,所以是Android特有的跨進程通信方式。
  2. 從Android Framework層面上講:Binder是連接ServiceManager(用來管理系統的service)連接各種Manager(ActivityManager、WindowManager等等)和相應ManagerService的橋梁。
  3. 從應用層講:Binder是客戶端和服務端進行通信的媒介,當bindService時,服務端會返回一個包含服務端業務調用的Binder對象,客戶端通過這個對象來獲取服務端提供的服務或數據。
Android是基于Linux系統上的,我們先了解一下Linux中的IPC通信原理

在Linux中,進程之間是隔離的,內存也是不共享的,想要進程之間通信必須先了解一下進程空間,進程空間分為用戶空間和內核空間,用戶空間是用戶程序運行的空間,內核空間則是內核運行的空間,為了防止用戶空間隨便干擾,用戶空間是獨立的,內核空間是共享的,但為了安全性考慮,內核空間跟用戶空間也是隔離的,它們之間的僅可以通過系統調用來通信。至此我們知道IPC的大致方案是A進程的數據通過系統調用把數據傳遞到內核空間,內核空間再利用系統調用把數據傳遞到B空間,其中會有兩次數據的拷貝如下圖:


Linux下的IPC

缺點顯而易見,數據傳遞通過內存緩存—>內核緩存—>內存緩存2次拷貝性能低,其次是傳遞數據后,接收方不知道用多大內存存放,所以盡可能大的開辟內存空間導致內存空間浪費。

再來看一下Android中的IPC通信-Binder

字面意思,粘合劑、膠水的意思,顧名思義就是粘合不同的進程,使之實現通信。
你肯定有疑問,既然基于Linux系統,為什么Android不沿用Linux的IPC而要使用Binder呢?因為綜合考慮其性能、穩定性和安全性。

  • 性能 :數據拷貝次數,共享內存0次、Binder 1次、Socket/管道/消息隊列2次。
  • 穩定性:Binder基于C/S架構,客戶端有需求丟給服務端完成,架構清晰,職責明確獨立,共享內存需要控制負責、難以使用,Binder機制更優。
  • 安全性:Android開放性平臺,不免會有很多惡意APP、流氓軟件等,傳統IPC沒有任何安全防護,無法獲取對方進程ID,Android為每個進程分配UID,傳統的IPC機制只能在數據包中添加,可靠的身份標識只能由IPC機制在內核添加才安全,其次傳統的IPC訪問接入點是開放的,惡意軟件通過猜測接入點可以獲得連接,所以不安全。Binder支持實名和匿名Binder,安全性更高。

先看看了解Binder可以給我們帶來什么幫助?

  • 為什么Activity間傳遞對象需要序列化?
  • 幫助我們理解Activity的啟動流程
  • 四大組件底層的通訊機制是怎樣的?
  • 理解AIDL內部實現機制
  • 幫助理解插件化編程等等~
    Android應用程序是由四大組件中的一個或多個組成,有時這些組件運行在同一進程,有時運行在不同進程,這些進程間通信需要依賴Binder IPC機制,不光于此,Android系統對應用層提供的各種服務如AMS、PMS等都是基于Binder IPC機制來實現的。
了解了這些,我們再來看Binder是怎樣不利用傳統的IPC來實現通信的

IPC基于內核空間,但Binder不是Linux系統內核的一部分,但Linux有動態內核可加載模塊(LKM)的機制,模塊是具有獨立功能的程序,可以被單獨編譯但不能獨立運行,運行時被了鏈接到內核座位內核的一部分運行,至此Android系統就可以通過動態添加一個內核模塊運行在內核空間,用戶進程之間通過這個內核模塊作為橋梁來實現通信,我們把這個內核模塊叫做Binder驅動(Binder Driver)。它是怎樣實現進程間通信的呢?通過內存映射!,Binder IPC 機制中涉及到的內存映射通過 mmap() 來實現,mmap() 是操作系統中一種內存映射的方法。內存映射簡單的講就是將用戶空間的一塊內存區域映射到內核空間。映射關系建立后,用戶對這塊內存區域的修改可以直接反應到內核空間;反之內核空間對這段區域的修改也能直接反應到用戶空間。內存映射能減少數據拷貝次數,實現用戶空間和內核空間的高效互動。兩個空間各自的修改能直接反映在映射的內存區域,從而被對方空間及時感知。也正因為如此,內存映射能夠提供對進程間通信的支持。

Binder IPC 原理

Binder IPC 正是基于內存映射(mmap)來實現的,但是 mmap() 通常是用在有物理介質的文件系統上的。舉個栗子:進程中的用戶區域不能直接訪問物理設備,如果想訪問磁盤數據,需要通過磁盤—>內核空間—>用戶空間這兩次拷貝,我們通過mmap()在兩者之間建立映射,減少數據的拷貝,用內存讀取來取代I/O讀寫提高效率。而 Binder 并不存在物理介質,因此 Binder 驅動使用 mmap() 并不是為了在物理介質和用戶空間之間建立映射,而是用來在內核空間創建數據接收的緩存空間。
過程如下圖文:
1.首先 Binder 驅動在內核空間創建一個數據接收緩存區;
2.接著在內核空間開辟一塊內核緩存區,建立內核緩存區和內核中數據接收緩存區之間的映射關系,以及內核中數據接收緩存區和接收進程用戶空間地址的映射關系;
3.發送方進程通過系統調用 copy_from_user() 將數據 copy 到內核中的內核緩存區,由于內核緩存區和接收進程的用戶空間存在內存映射,因此也就相當于把數據發送到了接收進程的用戶空間,這樣便完成了一次進程間的通信。

Android下的IPC

Client/Server/ServiceManager/驅動

Client、Server、Service Manager 運行在用戶空間,Binder 驅動運行在內核空間。其中 Service Manager 和 Binder 驅動由系統提供,而 Client、Server 由應用程序來實現。Client、Server 和 ServiceManager 均是通過系統調用 open、mmap 和 ioctl 來訪問設備文件 /dev/binder,從而實現與 Binder 驅動的交互來間接的實現跨進程通信。Client、Server、ServiceManager、Binder 驅動這幾個組件在通信過程中扮演的角色就如同互聯網中服務器(Server)、客戶端(Client)、DNS域名服務器(ServiceManager)以及路由器(Binder 驅動)之前的關系。

  • binder 驅動 -> 路由器
  • ServiceManager -> DNS
  • Binder Client -> 客戶端
  • Binder Server -> 服務器
    如下圖比較:



    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 通信中的代理模式

我們已經解釋清楚 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 進程,一次通信就完成了。

各 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 的相關實現需要開發者自己實現。

實現過程

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

那么服務端進程具備什么樣的能力?能為客戶端提供什么樣的服務呢?還記得我們前面介紹過的 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 進程里剛剛掛起的線程并將結果返回。

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

多種多樣的跨進程方式

上面介紹了Binder、序列化,我們再來了解一下其它的方式,比如通過Intent中附加的extra來傳遞信息、通過文件共享的方式共享數據、ContentProvider這種天生就支持跨進城訪問的方式、通過網絡通訊Socket來完成通訊等。

1.使用Bundle:
四大組件都支持在Intent中傳遞Bundle數據,Intent傳遞數據的特點上面說過需要序列化,Bundle實現了Pracelable接口,所以當我們在一個進程中啟動另一個進程的Activity、Service等,就可以利用Bundle附加數據來傳遞信息,Bindle傳遞自定義類型需要序列化,但局限性是Bundle傳遞的數據有限制。
2.使用文件共享:
把一個對象序列化存儲寫入到SD卡上的一個文件里,另一個進程再去讀取這個文件獲得通信。
3.使用Messenger:
Messenger顧名思義,信使的意思,通過它我們可以在不同的進程中傳遞Message對象,Message中存放著需要傳遞的對象。它是一種輕量級的IPC方案,底層實現是AIDL,它對AIDL做了封裝,使我們使用起來更簡單,由于它一次處理一個請求,所以我們不需要考慮線程同步問題。
使用方法:
1.服務端進程:創建Service來處理客戶端的連接請求,同時創建一個Handler并通過它來創建一個Messenger對象,然后在Service的onBind方法中返回這個messenger對象底層的Binder。
2.客戶端進程:首先需要綁定服務端的Service,綁定成功后在服務端返回的IBinder對象中創建一個Messenger,通過它向服務端發Message類型的消息。如果服務端回應客戶端,和服務端,我們還需要創建一個Handler并創建一個新的Messenger并把這個Messenger對象通過Message的replyTo參數傳遞給服務端,服務端通過replyTo參數就可以回應客戶端。
代碼示例:

  • 構建一個運行在獨立進程中的服務端Service:
public class MessengerService extends Service {
    private static final String TAG = "MessagerService";

    /**
     * 處理來自客戶端的消息,并用于構建Messenger
     */
    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message message) {
            switch (message.what) {
                case MESSAGE_FROM_CLIENT:
                    Log.e(TAG, "receive message from client:" + message.getData().getString("msg"));
                    break;
                default:
                    super.handleMessage(message);
                    break;
            }
        }
    }

    /**
     * 構建Messenger對象
     */
    private final Messenger mMessenger = new Messenger(new MessengerHandler());

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        //將Messenger對象的Binder返回給客戶端
        return mMessenger.getBinder();
    }
}
  • 注冊Service,在不同進程
<service
    android:name="com.xxq2dream.service.MessengerService"
    android:process=":remote" />
  • 然后客戶端是通過綁定服務端返回的binder來創建Messenger對象,并通過這個Messenger對象來向服務端發送消息
public class MessengerActivity extends AppCompatActivity {
    private static final String TAG = "MessengerActivity";

    private Messenger mService;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            Log.e(TAG, "ServiceConnection-->" + System.currentTimeMillis());
            //通過服務端返回的Binder創建Messenger
            mService = new Messenger(iBinder);
            //創建消息,通過Bundle傳遞數據
            Message message = Message.obtain(null, MESSAGE_FROM_CLIENT);
            Bundle bundle = new Bundle();
            bundle.putString("msg", "hello service,this is client");
            message.setData(bundle);
            try {
                //向服務端發送消息
                mService.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
            Log.e(TAG, "onServiceDisconnected-->binder died");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_messenger);
        //綁定服務
        Intent intent = new Intent(this, MessengerService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        //解綁服務
        unbindService(mConnection);
        super.onDestroy();
    }
}
  • 服務端如果要回復消息給客戶端,那就要用到Message的replyTo參數了
  • 服務端改造:
private static class MessengerHandler extends Handler {
    @Override
    public void handleMessage(Message message) {
        switch (message.what) {
            case Constant.MESSAGE_FROM_CLIENT:
                Log.e(TAG, "receive message from client:" + message.getData().getString("msg"));
                //獲取客戶端傳遞過來的Messenger,通過這個Messenger回傳消息給客戶端
                Messenger client = message.replyTo;
                //當然,回傳消息還是要通過message
                Message msg = Message.obtain(null, Constant.MESSAGE_FROM_SERVICE);
                Bundle bundle = new Bundle();
                bundle.putString("msg", "hello client, I have received your message!");
                msg.setData(bundle);
                try {
                    client.send(msg);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
            default:
                super.handleMessage(message);
                break;
        }
    }
}
  • 客戶端改造:
/**
 * 用于構建客戶端的Messenger對象,并處理服務端的消息
 */
private static class MessengerHandler extends Handler {
    @Override
    public void handleMessage(Message message) {
        switch (message.what) {
            case Constant.MESSAGE_FROM_SERVICE:
                Log.e(TAG, "receive message from service:" + message.getData().getString("msg"));
                break;
            default:
                super.handleMessage(message);
                break;
        }
    }
}

/**
 * 客戶端Messenger對象
 */
private Messenger mClientMessenger = new Messenger(new MessengerHandler());

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        Log.e(TAG, "ServiceConnection-->" + System.currentTimeMillis());
        mService = new Messenger(iBinder);
        Message message = Message.obtain(null, MESSAGE_FROM_CLIENT);
        Bundle bundle = new Bundle();
        bundle.putString("msg", "hello service,this is client");
        message.setData(bundle);
        //將客戶端的Messenger對象傳遞給服務端
        message.replyTo = mClientMessenger;
        try {
            mService.send(message);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        Log.e(TAG, "onServiceDisconnected-->binder died");
    }
};

Messenger工作原理圖示

4.使用AIDL:
Messenger是串行處理消息的,服務端需要一個個來處理,不適合大量的消息同時發送給服務端。其次Messenger作用是傳遞消息,有時候我們還需要調用服務端方法,這種情景AIDL更適合。步驟如下:
1.服務端:創建Service用來監聽客戶端連接請求
2.客戶端:綁定服務端的Service,將服務端返回的Binder對象轉換成AIDL接口所屬的類型后就可以調用AIDL中的方法了。
3.AIDL接口創建,聲明接口、方法、同步項目,具體使用如下圖:

創建,輸入aidl文件名:IMyAidlInterface.aidl

// IMyAidlInterface.aidl
interface IMyAidlInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     *
     * 此方法作用是告訴我們aidl中可以使用的基本類型,可以刪除無視。
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

在AIDL中定義提供給其他app使用的方法

interface IMyAidlInterface {
   String getName();
}

sycn project同步后,編譯工具會自動幫我們生成一個Stub類,創建一個service并在里面創建一個繼承剛才接口的Stub類內部類,實現接口方法,并在onBind方法中返回內部類的實例:

public class MyService extends Service
{

    public MyService()
    {
      ...
    }

    @Override
    public IBinder onBind(Intent intent)
    {
        return new MyBinder();
    }

    class MyBinder extends IMyAidlInterface.Stub
    {

        @Override
        public String getName() throws RemoteException
        {
            return "test";
        }
    }
}

接下來我們將AIDL文件拷貝到客戶端(包名,文件名必須完全一致),然后在Activity中綁定服務。

public class MainActivity extends AppCompatActivity
{


    private IMyAidlInterface iMyAidlInterface;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindService(new Intent("cc.abto.server"), new ServiceConnection()
        {

            @Override
            public void onServiceConnected(ComponentName name, IBinder service)
            {

                iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
            }

            @Override
            public void onServiceDisconnected(ComponentName name)
            {

            }
        }, BIND_AUTO_CREATE);
    }

    public void onClick(View view)
    {
        try
        {
            Toast.makeText(MainActivity.this, iMyAidlInterface.getName(), Toast.LENGTH_SHORT).show();
        }
        catch (RemoteException e)
        {
            e.printStackTrace();
        }
    }
}

這邊我們通過隱式意圖來綁定service,在onServiceConnected方法中通過IMyAidlInterface.Stub.asInterface(service)獲取iMyAidlInterface對象,然后在onClick中調用iMyAidlInterface.getName()。
使用自定義數據類型是需要把使用的對象序列化實現Parcelable接口,在aidl中導入該類型再使用,注意包名文件名。

5.使用ContentProvider
ContentProvider是Android提供的專門用于應用間進行數據共享的方式,天生適合進程間通信,和Messenger一樣底層同樣實現了Binder(可以理解幾乎所有跨進程都是基于Binder的封裝。來實現的)。

ContentProvider模型

ContentProvider是一個抽象類,如果我們需要開發自己的內容提供者我們就需要繼承這個類并復寫其方法,需要實現的主要方法如下:
public boolean onCreate()
在創建ContentProvider時使用
public Cursor query()
用于查詢指定uri的數據返回一個Cursor
public Uri insert()
用于向指定uri的ContentProvider中添加數據
public int delete()
用于刪除指定uri的數據
public int update()
用戶更新指定uri的數據
public String getType()
用于返回指定的Uri中的數據MIME類型
數據訪問的方法insert,delete和update可能被多個線程同時調用,此時必須是線程安全
其它應用可以通過ContentResolver來訪問ContentProvider提供的數據,而ContentResolver通過uri來定位自己要訪問的數據。
為什么要通過再加一層ContentResolver而不是直接訪問ContentProvider?
原因是:一臺手機中可不是只有一個Provider內容,它可能安裝了很多含有Provider的應用,比如聯系人應用,日歷應用,字典應用等等。有如此多的Provider,如果你開發一款應用要使用其中多個,如果讓你去了解每個ContentProvider的不同實現,豈不是要頭都大了。所以Android為我們提供了ContentResolver來統一管理與不同ContentProvider間的操作。怎樣區別不同的Provider則是通過URI!
ContentResolver工作原理

擴展:URI(Universal Resource Identifier)統一資源定位符
格式:[scheme:][//host:port][path][?query]
URI:http://www.baidu.com:8080/wenku/jiatiao.html?id=123456&name=jack

  • scheme:根據格式我們很容易看出來scheme為http
  • host:www.baidu.com
  • port:就是主機名后面path前面的部分為8080
  • path:在port后面?的前面為wenku/jiatiao.html
  • query:?之后的都是query部分為 id=123456$name=jack
    uri的各個部分在安卓中都是可以通過代碼獲取的,下面我們就以上面這個uri為例來說下獲取各個部分的方法:
  • getScheme() :獲取Uri中的scheme字符串部分,在這里是http
  • getHost():獲取Authority中的Host字符串,即 www.baidu.com
  • getPost():獲取Authority中的Port字符串,即 8080
  • getPath():獲取Uri中path部分,即 wenku/jiatiao.html
  • getQuery():獲取Uri中的query部分,即 id=15&name=du

接下來我們看一下如何使用,創建了兩個工程,進程一自定義了contentprovider,進程二通過ContentResolver來訪問進程一中的contentprovider的數據(對進程一中自定義的contentprovider的數據庫進行增刪改查操作)
先寫個數據庫的工具類用來創建數據庫(進程二就是跨進程操作此數據庫的)

public class DBHelper extends SQLiteOpenHelper {

    // 數據庫名
    private static final String DATABASE_NAME = "finch.db";

    // 表名
    public static final String USER_TABLE_NAME = "user";
    public static final String JOB_TABLE_NAME = "job";

    private static final int DATABASE_VERSION = 1;
    //數據庫版本號

    public DBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {

        // 創建兩個表格:用戶表 和職業表
        db.execSQL("CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " name TEXT)");

        db.execSQL("CREATE TABLE IF NOT EXISTS " + JOB_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " job TEXT)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)   {

    }
}

再自定義一個自己的contentprovider。。DBHelper中有兩張表, UriMatcher是用來根據進程二的ContentResolver調用的uri判斷進程二到底是需要操作哪張表的數據的。

public class MyProvider extends ContentProvider {

    private Context mContext;
    DBHelper mDbHelper = null;
    SQLiteDatabase db = null;
    public static final String AUTOHORITY = "com.example.zhaoziliang";
    // 設置ContentProvider的唯一標識

    public static final int User_Code = 1;
    public static final int Job_Code = 2;

    // UriMatcher類使用:在ContentProvider 中注冊URI
    private static final UriMatcher mMatcher;
    static{
        mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        // 初始化
        mMatcher.addURI(AUTOHORITY,"user", User_Code);
        mMatcher.addURI(AUTOHORITY, "job", Job_Code);
        // 若URI資源路徑 = content://cn.scu.myprovider/user ,則返回注冊碼User_Code
        // 若URI資源路徑 = content://cn.scu.myprovider/job ,則返回注冊碼Job_Code
    }

    // 以下是ContentProvider的6個方法

    /**
     * 初始化ContentProvider
     */
    @Override
    public boolean onCreate() {

        mContext = getContext();
        // 在ContentProvider創建時對數據庫進行初始化
        // 運行在主線程,故不能做耗時操作,此處僅作展示
        mDbHelper = new DBHelper(getContext());
        db = mDbHelper.getWritableDatabase();

        // 初始化兩個表的數據(先清空兩個表,再各加入一個記錄)
        db.execSQL("delete from user");
        db.execSQL("insert into user values(1,'Carson');");
        db.execSQL("insert into user values(2,'Kobe');");

        db.execSQL("delete from job");
        db.execSQL("insert into job values(1,'Android');");
        db.execSQL("insert into job values(2,'iOS');");

        return true;
    }

    /**
     * 添加數據
     */

    @Override
    public Uri insert(Uri uri, ContentValues values) {

        // 根據URI匹配 URI_CODE,從而匹配ContentProvider中相應的表名
        // 該方法在最下面
        String table = getTableName(uri);

        // 向該表添加數據
        db.insert(table, null, values);

        // 當該URI的ContentProvider數據發生變化時,通知外界(即訪問該ContentProvider數據的訪問者)
        mContext.getContentResolver().notifyChange(uri, null);

//        // 通過ContentUris類從URL中獲取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        return uri;
        }

    /**
     * 查詢數據
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // 根據URI匹配 URI_CODE,從而匹配ContentProvider中相應的表名
        // 該方法在最下面
        String table = getTableName(uri);

//        // 通過ContentUris類從URL中獲取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);

        // 查詢數據
        return db.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
    }

    /**
     * 更新數據
     */
    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        // 由于不展示,此處不作展開
        return 0;
    }

    /**
     * 刪除數據
     */
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // 由于不展示,此處不作展開
        return 0;
    }

    @Override
    public String getType(Uri uri) {

        // 由于不展示,此處不作展開
        return null;
    }

    /**
     * 根據URI匹配 URI_CODE,從而匹配ContentProvider中相應的表名
     */
    private String getTableName(Uri uri){
        String tableName = null;
        switch (mMatcher.match(uri)) {
            case User_Code:
                tableName = DBHelper.USER_TABLE_NAME;
                break;
            case Job_Code:
                tableName = DBHelper.JOB_TABLE_NAME;
                break;
        }
        return tableName;
        }
    }

進程二:
ContentResolver通過對應匹配的uri去調用對應的進程一的contentprovider的不同的表進行增刪改查操作


public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        /**
         * 對user表進行操作
         */

        // 設置URI
        Uri uri_user = Uri.parse("content://com.example.zhaoziliang/user");

        // 插入表中數據
        ContentValues values = new ContentValues();
        values.put("_id", 4);
        values.put("name", "zzl");


        // 獲取ContentResolver
        ContentResolver resolver =  getContentResolver();
        // 通過ContentResolver 根據URI 向ContentProvider中插入數據
        resolver.insert(uri_user,values);

        // 通過ContentResolver 向ContentProvider中查詢數據
        Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
        while (cursor.moveToNext()){
            System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
            // 將表中數據全部輸出
        }
        cursor.close();
        // 關閉游標

        /**
         * 對job表進行操作
         */
        // 和上述類似,只是URI需要更改,從而匹配不同的URI CODE,從而找到不同的數據資源
        Uri uri_job = Uri.parse("content://com.example.zhaoziliang/job");

        // 插入表中數據
        ContentValues values2 = new ContentValues();
        values2.put("_id", 4);
        values2.put("job", "LOL Player");

        // 獲取ContentResolver
        ContentResolver resolver2 =  getContentResolver();
        // 通過ContentResolver 根據URI 向ContentProvider中插入數據
        resolver2.insert(uri_job,values2);

        // 通過ContentResolver 向ContentProvider中查詢數據
        Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
        while (cursor2.moveToNext()){
            System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
            // 將表中數據全部輸出
        }
        cursor2.close();
        // 關閉游標
    }
}

6.使用Socket(套接字)
socket實現進程間通信,分為:

  • TCP協議(傳輸控制/流式套接字):面向連接的協議,提供穩定的雙向通信功能,三次握手四次揮手,這個次數是保證安全又高效。
  • UDP協議(用戶數據報套接字):面向無連接,不穩定,不安全,不保證數據一定能傳輸到,但效率高。
    具體就是我們平常使用的網絡請求,有興趣的話自己寫個簡單的服務器聊天室。。。。。。
Binder連接池

隨著項目越來越大,很多業務模塊都需要使用AIDL來通信,我們還需要了解一下Binder連接池的原理,但我真的寫吐了,以后再寫吧。。。。

最后總結一下他們的使用場景

比較使用場景

結尾 :終于結束了,Don’t Say So Much。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。