標籤:

談談IPC(1)

最近在看藝術開發探索,思來想去還是想把這個部分的東西進行一個記錄,雖然沒有理解為什麼要將這個內容放置在第二章,但是本著存在即合理的本質,它的存在一定有它自己的道理。所以思考再三還是進行一個記錄對日後的自己進行一個啟發。 可能有一些偏差性的錯誤,有的話勞煩指出,不勝感激。

可能當我們看見IPC這個單詞的時候都百思不得其解,指它具體是什麼,幹什麼用的,是怎麼實現的。那麼我們開始談談的話,也從這些地方開始吧。 自然,因為自己的技術原因,不會過分深究太過於底層的實現,僅僅參考《Android開發藝術探索》和部分博文來對於這一部分進行說明。

初識

是什麼

在這裡可以很直白的告訴你IPC,就是跨進程通信的意思。 更具體的一點,就是指兩個進程交換數據的過程。 那這個時候問題就來了,進程是什麼?為什麼要跨進程進行通信? 那麼,提及進程,可能有人就會聯想到線程,但是其實這兩個並不是同一個東西。

  • 進程: 你可以將它理解為是每一個應用/程序佔用的內存空間,正式地來說叫做一個執行單元,就可以將它類比成一個生產零件的車間。
  • 線程: 你可以將它理解成是每一個對應的內存空間之內,每次要進行一些操作的時候使用的一個東西,正式地解釋是CPU的最小執行單元。 其實這個就可以類比成車間內不同的車間工人,他們每個人身上可能都背負著不同的任務,但是有時候又有幾個人協同完成一個工作內容的情況。 當然這個比喻也可以應用到線程的其他部分裡面去,在這裡就不進行展開了。

這樣興許就能夠分辨清楚兩者之間的差異,線程是包含在每一個不同的進程中的,而每一個進程能夠通過自身的一些需求來對不同的線程進行調度。 放在Android之內,最明顯的例子就是每一個不同的App應用就對應著不同的進程,而每一個App內的UI線程,以及後台進行計算或者是其他操作的線程都是包括在一個進程內。 那麼就可以說通過進程,將不同內容的應用進行切割、分離,讓用戶能夠更加清晰地了解到這兩個東西不是同一個App,而線程就是為了展現不同進程之中的內容,而服務於進程。

為什麼

那麼這個時候就有人想問了,為什麼我們要使用IPC呢? 那這樣吧,首先講講在什麼常見的情況之下使用IPC,你就知道我們為什麼要使用IPC了,啊,不過這些情景不一定在Android環境下的,只是提出為了更好理解,為什麼我們創造並使用了它。

  • Windows:剪貼板
  • Linux:數據傳遞

其實想想也是情有可原,不同的應用在不同的進程,但是數據通信在某些時候也是必要的,所以說IPC也就這麼產生了。

但是對於Android開發者來說,IPC大部分用於以下情況

  • 分擔主進程過重的載入任務

例如:大量圖片載入(易OOM),讀取一大段數據存入數據,載入Flash,頻繁繪製一個頁面,視頻播放等...

  • 將進程crash對於用戶的影響降到最低

在前些日子看見的文章裡面,發現QQ音樂的開發也是才用了多進程的方式,這樣播放音樂的進程不會因為其他進程在交互過程中出現了錯誤產生crash,進而導致進程的關閉,影響用戶體驗。

  • 一個App需要源自於其他App的對應數據

在這個情況下,我們一般使用ContentProvider,但是其實它底層的實現還是跟跨進程通信相關,只不過這個東西被Android封裝了,我們使用的過程之中感知不到一個跨進程通信的過程而已。關於它的具體描述在之後會提及。

這樣的解釋之下興許能夠了解它是在什麼範圍內進行使用了。

怎麼做

現在,興許大家對它有了那麼一點模糊的概念了,可能就想上手嘗試一下這個東西用起來具體是個什麼樣子。 那從兩個部分開始講述:如何在自己的App中開啟另外一個線程,以及如何進行跨進程通信:

  • 在開啟另外一個進程 首先我們需要知道的是,在Android裡面只能夠通過在AndroidMenifest文件裡面,通過對四大組件聲明android:process=以表示對應的四大組件在使用的時候是在另外一個新的進程之中進行的。 啊,是的,你沒有看錯,假如說多進程運行這個App,只能夠通過這個方式來執行。

  • 如何在Android之中進行跨進程通信 那麼對於跨進程通信來說,就是利用一下不同的方式來完成跨進程通信了,其中可能會有優勢,有劣勢。

    • Bundle
    • Messenger
    • 文件共享
    • ContentProvider
    • Socket
    • AIDL

如何開啟一個進程

在之前我們就提到了,在Android裡面開啟另外一個新的進程只能夠通過在AndroidMenifest文件裡面,通過對四大組件聲明android:process=xxx來表示該組件在使用的時候是在對應的另外一個進程裡面。

android:process

這個屬性可謂是打開了多進程的大門,但是這個時候你可能又有問題了,這個對應的屬性值又代表什麼呢?其實就是這個進程對應的進程名。 譬如說

android:process=":remote"
android:process="com.cynthia.demo.remote"

在這裡面就是分別聲明了兩個進程,一個是com.cynthia.demo:remote,一個是com.cynthia.demo.remote,這個時候你可能又會疑惑了,為什麼一種這種表達方式可以簡寫呢? 主要是:這個符號含義就是要在當前的進程名前面附加上當前的包名,所以有這樣的效果。當然,這個符號不僅僅也只有這一個作用,它同時也表示它本身是一個私有進程。 假如說沒有這個符號的話,它就屬於全局進程。當然,名字不能偷懶簡寫了。

那麼第二個問題來了,私有進程和全局進程又是什麼? - 私有進程 僅僅只能夠在當前應用之中進行工作,其他應用假如說想跟這個部分共享同一個進程,顯然是不能的。 - 全局進程 其他的應用在進行一些特殊的操作之後可以跟這個部分共享同一個進程。

在這裡,共享進程前提是他們的shareUID和簽名相同,假若兩者同時運行在同一個進程中,這兩個應用之間就可以相互訪問對方的私有數據(例如data目錄,組件信息,共享內存數據)。在兩者沒有同時運行在同一個進程中的時候,也可以共享data目錄和組件信息。 那麼,shareUID又是什麼呢,它是Android對每個App會分配的唯一的一個id 這個id的存在就是為了該應用的文件設置許可權只對該用戶好應用自身可見。保證了文件的安全性。

但是這樣雖然是說開啟了多進程的大門,但是多進程真的只是這個樣子嗎?

啊,顯然並不是這個樣子。

使用多進程會造成的問題

在開始講述這部分內容之前,我們得知道Android對於每個不同的應用(也可以說是進程)分配一個獨立的虛擬機進行對應的操作。自然,不同的虛擬機在內存的分配上面有不同的地址空間。 那再這樣的概述之下,興許聰明的你已經猜到問題了。

不同的虛擬機在訪問同一個類信息的時候會產生多個副本

這樣就會導致,不同的進程之中數據不同步的情況。相對應的,你可以將其理解成線程之中沒有加同步鎖然後進行數據改寫的情況。這樣顯然會導致我們的業務要求會在跳轉到不同的進程的時候拿的只是一個數據的副本,原進程也無法感知到新進程數據的修改,導致實際的差距與理論差距甚遠。 那麼你會說,單例模式/靜態成員了解一下? 其實在這樣的環境之下,同樣也是失效的。 按照之前的解釋來說,不同的進程會分配不同的地址空間,一開始運行的時候是將原進程的數據拷貝到自己對應的進程對應的內存,從根本上來說這個東西都不已經屬於之前的那一個內存的,什麼都是全新的。

那麼在這樣的情況之下就會有以下的問題發生 - 靜態成員和單例模式完全失效 - 線程同步機制失效 - Application會多次創建 - SharedPreferences的可靠性下降

前三個其實原理跟先前解釋得原因相近,第三條無非是因為每次開啟一個進程相當於開啟一個新的應用程序,伴隨著新的「應用程序」的開啟,Application也會再次創建。

那麼針對第四個問題的話,來源是關於SharePreferences本身,因為這個在官方文檔裡面是這麼說的:

Note: This class does not support use across multiple processes.

本身SharePreferences就不能夠適用於跨進程通信。因為它自己實現數據的存儲和讀取是通過寫入和讀取XML文件夾來實現的。同時,在操作它的時候應用的內存會對其對應的鍵值對進行緩存,這樣在進程之中對其進行並發的讀寫的操作的時候導致其內容有一定的概率丟失。或者是說讀取的數據並不是最新的數據,可能是上一代,或者上上代的。

但是又有人說,不是有一個MODE_MULTI_PROCESS的Flag嗎?啊,對於這個官方的解釋是這樣的。

@deprecated MODE_MULTI_PROCESS does not work reliably in some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.

大概翻譯一下的話,意思就是這個Flag在Android的某些版本並不可靠,並且未來也不會在對其提供支持,如果要跨進程對數據進行傳輸的話還是推薦ContentProvider

所以說SharePreferences在跨進程數據傳遞可靠性還是會降低的。在實際的開發之中如果要多進程進行數據讀取或者存儲需要考慮其他的方式。

如何在Android之中進行跨進程通信

在剛才,我們知道了多進程的打開方式,但是同時我們也知道了這其中也有很多的問題。自然,每個進程之間也是需要進行通信的,那我們怎麼進行通信呢? 在前面,我們已經大概列出來了幾個方式,在現在,就開始慢慢進行一定的講解。

Bundle

我們現在知道的是,四大組件中的三大組件(Activity、Service、Receiver)都是支持在Intent之中傳遞Bundle數據的。那我們為什麼說Bundle可以支持跨進程傳輸的呢? 因為Bundle是聲明了Parcelable介面的一個類,可以支持序列化和反序列化。

那麼在這裡,我們就先講一下ParcelableSerializable這兩個介面,以便於對於之後有更好的理解。

Serializable

它自己本身只是Java自己提供的一個反序列化的介面,當我們點入源碼的時候才發現它其實是一個空介面,相當於只是一個說可以進行序列化的標誌。 而序列化就標誌著這個東西可以跨進程進行傳輸了。 在其中,有些時候需要自己指定一個serialVersionUID(或者讓Java自己給你生成一個)來進行表示。那麼這個值的存在意義是什麼呢?

這個值是一個標識值,是在每次進行序列化的時候系統會寫入文件的一個值,在進行反序列化的時候會將當前的這個值與寫入文件的值進行比對,假若說兩者相等,才能夠進行反序列化的操作。不然會報錯。 但是就是自己指定的值跟Java自己默認生成的值又有什麼差異呢? 默認生成的值會跟隨這個類裡面的成員變數和方法的變化而更新這個值。 但是說假如自己指定的話,就會一定程度上避免了在序列化之後更改了成員變數/方法,反序列化失敗這樣的事情發生。

當然,假如說類結構已經發生了毀滅性的改變的話(例如修改類名,修改成員變數的類型等),即便是ID驗證通過了,反序列化仍舊是失敗的。

自然,我們還需要注意的是,靜態成員變數和transient關鍵字修飾的變數不參與序列化過程。

所以說我們自己實現這個介面的時候最好聲明serialVersionUID 防止在之後對類進行了非毀滅性的結構改變的時候還能夠正常的恢復與寫入數據。

Parcelable

Parcelable介面是Android這邊給進行序列化提供的一個介面,這個跟Serializable介面相比,實現就會複雜很多,示例大概如下:

public class User implements Parcelable {
private int id;
private String name;

// 省略了對外的getter和setter方法

//從Parcel容器之中讀取數據,反序列化過程
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];
}
};

private User(Parcel in) {
id = in.readInt();
name = in.readString();
}

//返回當前對象的內容描述,如果有文件描述符,返回1
//但是基本上都是返回0

@Override
public int describeContents() {
return 0;
}

/*
序列化過程,將當前的信息寫入Parcel之中
第二個為標值,只有0與1的情況
當flags為1時,表明當前對象需要作為返回值返回,不能能立刻釋放資源,
但是flags基本上就只有0的情況
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
}
}

在聲明了這個介面的類,都要複寫上面展示的這幾個方法,以便完成對應的序列化以及反序列化的過程。在整個過程之中,Parcel這個類包裝了可序列化的數據,而每個需要實現序列化的類都是通過Parcel的一系列方法完成的,例如說readwrite 我們還需要注意的一種情況是,假若說在聲明了Parcelable的介面裡面還含有聲明了可序列化的介面的其他自定義類,在序列化以及反序列化的過程之中都有一些差異。 在這裡,就拿UserCar來做例子

public class Car implements Parcelable {
private int id;
private String name;
private User user;

//省略了其他需要聲明的方法,只留下了不同的部分。

private Car(Parcel in) {
id = in.readInt();
name = in.readString();
user = in.readParcelable(User.class.getClassLoader());
}

@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
dest.writeParcelable(user, flags);
}
}

在其中就可以發現一個可序列化對象在反序列化過程的時候需要傳遞一個User.class.getClassLoader(),這個是表明了當前的類載入器,否則在進行載入的時候會報錯。

但是這樣又有一個問題了,既然SerializableParcelable這兩個介面都是為了序列化,那麼這兩個又有什麼區別呢?

  • Serializable 這個是Java自帶的序列化介面,但是效率不高。 緣由是聲明介面很簡單,但是裡面進行了大量的I/O操作,導致了序列化和反序列化效率的降低,但是該介面針對數據持久化操作是穩定的。例如說保存到本地以及進行網路傳輸。
  • Parcelable 這個是為了優化Serializable在內存傳輸過程中效率過慢,Android進行開發的一個介面,但是這個也只是適合於內存之中的序列化操作。 因為android的不同版本也可能導致Parcelable的不同,導致在序列化過程之中有一定不如人意的情況出現,所以不推薦利用該介面進行數據持久化

好的,現在講完了這兩個介面之後,興許就能夠理解為什麼Bundle能夠傳遞數據了,因為它自己本身就聲明了Parcelable介面,然後藉助Intent就能夠在內存裡面進行序列化和反序列化。

當然,Bundle之中傳遞的對象也需要能夠進行序列化的,不能夠利用Bundle進行傳輸的東西在該類源碼之中已經列出,各位有興趣的可以看看源碼。


推薦閱讀:

TAG:Android開發 |