注:此篇筆記只記錄重難點,對于基礎和詳細內容請自行學習《Android開發藝術探索》
2.1 IPC簡介
Inter-Process Communication的縮寫。含義為進程間通信或跨進程通信,是指兩個進程之間進行數據交換的過程。
進程和線程的區別
- 按照操作系統的描述,線程是CPU調度的最小單元,同時線程是一種有限的系統資源。
- 進程一般指一個執行單元,在PC和移動設備上指一個程序或者一個應用。一個進程可以包含多個線程,因此進程和線程是包含與被包含的關系。
多進程分為兩種
- 第一種情況是一個應用因為某些原因自身需要采用多線程模式來實現。
- 另一種情況是當前應用需要向其他應用獲取數據
2.2 Android中的多進程模式
2.2.1 開啟多進程
通過給四大組件指定android:process屬性,我們可以開啟多線程模式
- 進程名以":"開頭的進程屬于當前應用的私有進程,其他應用的組件不可以和它跑在同一進程,而進程名不以":"開頭的進程屬于全局進程,其他應用通過ShareUID方式可以和它跑在同一個進程中。
- Android系統會為每個應用分配一個唯一的UID,具有相同UID的應用才能共享數據,兩個應用通過ShareUID跑在同一個進程中是有要求的,需要這兩個應用有相同的ShareUID并且簽名相同才可以。在這種情況下,它們可以互相訪問對方的私有數據,比如data目錄、組件信息等,不管它們是否跑在同一個進程中。當然如果它們跑在同一個進程中,那么除了能共享data目錄、組件信息,還可以共享內存數據,或者說它們看起來就像是一個應用的兩個部分。
2.2.2 多進程模式的運行機制
- Android為每一個應用分配了一個獨立的虛擬機,或者說為每個進程都分配了一個獨立的虛擬機,不同的虛擬機在不同的內存分配上有不同的地址空間,這就導致在不同的虛擬機中訪問同一個類的對象會產生多份副本。
- 所有運行在不同進程中的四大組件,只要它們之間需要通過內存來共享數據,都會共享失敗。
一般來說,使用多進程會造成如下幾個方面的問題:
- 靜態成員和單例模式完全失效
- 線程同步機制完全失效
不管是鎖對象還是鎖全局類都無法保證線程同步,因為不同進程鎖的不是同一個對象
- SharedPreference的可靠性下降
SharedPreferences不支持兩個進程同時去執行寫操作,否則會導致一定幾率的數據丟失,這時因為SharedPreferences底層是通過讀寫XML文件來實現的,并發寫顯然是可能出問題的,甚至并發讀寫都有可能發生問題
- Application會多次創建
運行在同一個進程中的組件是屬于同一個虛擬機和同一個Application的。同理,運行在不同進程中的組件是屬于兩個不同的虛擬機和Application的。
IPC基礎概念介紹
2.3.1 Serializable接口
是Java所提供的一個序列化接口,它是一個空接口,為對象提供標準的序列化和反序列化操作。使用Serializable來實現序列化相當簡單,只需要在類的聲明中指定一個類似下面的標識即可自動實現默認的序列化過程。
private static final long serialVersionUID = 8711368828010083044L
通過Serializable方來實現對象的序列化,如下代碼:
//序列化過程
User user = new User(0, "jake", true);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();
//反序列化過程
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User)in.readObject();
in.close();
原則上序列化后的數據中的serialVersionUID只有和當前類的serialVersionUID相同時才能夠正常的被反序列化。
serialVersionUID的詳細工作機制是這樣的:序列化的時候系統會把當前類的serialVersionUID寫入序列化的文件中(也可能是其他的中介),當反序列化的時候系統會去檢測文件中的serialVersionUID,看它是否和當前類的serialVersionUID一致,如果一致就說明序列化的類的版本和當前類的版本是相同的,這個時候可以成功反序列化,否則就說明當前類和序列化的類相比發生了某些變換。
給serialVersionUID制定為1L或者采用Eclipse根據當前類結構去生成的hash值,這兩者并沒有本質區別。
- 靜態成員變量屬于類不屬于對象,所以不會參與序列化過程
- 其次用transient關鍵字標記的成員變量不參與序列化過程
2.3.2 Parcelable接口
Parcelable也是一個接口,只要實現這個接口,一個類的對象就可以實現序列化并可以通過Intent的Binder傳遞
Parcelable的方法說明:
方法 | 功能 | 標記位 |
---|---|---|
createFromParcel(Parcel in) | 從序列化的對象中創建原始對象 | |
newArray[int size] | 創建指定長度的原始對象數組 | |
User(Parcel in) | 從序列化的對象中創建原始對象 | |
write ToParcel(Parcel out, int flags) | 將當前對象寫入序列化結構中,其中flags標識有兩種值0或1(參見右側標記位)。為1時標識當前對象需要作為返回值返回,不能立即釋放資源,幾乎所有情況都為0 | PARCELABLE_WRITE_RETURN_VALUE |
describeContents | 返回當前對象的內容描述。如果含有文件描述符,返回1(參見右側標記位),否則返回0,幾乎所有的情況都返回0 | CONTENTS_FILE_DESCRIPTOR |
-
具體如下:
public class Book implements Parcelable { public static final Creator<Book> CREATOR = new Creator<Book>() { @Override public Book createFromParcel(Parcel in) { return new Book(in); } @Override public Book[] newArray(int size) { return new Book[size]; } }; public int code; public String name; public Book(int code, String name) { this.code = code; this.name = name; } protected Book(Parcel in) { code = in.readInt(); name = in.readString(); } public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(code); dest.writeString(name); } }
系統已經為我們提供了許多實現了Parcelable接口的類,它們都是可以直接序列化的,比如Intent、Bundle、Bitmap等,同時List和Map也可以序列化,前提是它們里面的每個元素都是可序列化的。
如何選取
- Serializable是Java中的序列化接口,其使用起來簡單但是開銷很大,序列化和反序列化需要大量I/O操作。而Parceleble是Android中的序列化方式,因此更適合在Android平臺上,缺點是麻煩,但是效率高,這是Android推薦的序列化方式,所以我們要首選Parcelable。
- Parcelable主要用在內存序列化上,通過Parcelable將對象序列化到存儲設備中或者將對象序列化之后通過網絡傳輸,但是過程稍顯復雜,因此在這兩種情況下建議大家使用Serializable。
2.3.3 Binder
- 繼承了IBinder接口
- Binder是一種跨進程通信方式
- 是ServiceManager連接各種Manager(ActivityManager,WindowManager等)和相應ManagerService的橋梁
- 從Android應用層來說,Binder是客戶端和服務端進行通信的媒介,當bindService的時候,服務器會返回一個包含了服務器端業務調用的Binder對象,通過這個Binder對象,客戶端就可以獲取服務端提供的服務或者是數據,這里的服務包含了普通服務和基于AIDL的服務
aidl工具根據aidl文件自動生成的java接口解析:
- 首先,它聲明了幾個接口方法,同時還聲明了幾個整型的id用于標識這些方法,id用于標識在transact過程中客戶端所請求的到底是哪個方法;
- 接著,它聲明了一個內部類Stub,這個Stub就是一個Binder類,當客戶端和服務端都位于同一個進程時,方法調用不會走跨進程的transact過程,而當兩者位于不同進程時,方法調用需要走transact過程,這個邏輯由Stub內部的代理類Proxy來完成。
所以,這個接口的核心就是它的內部類Stub和Stub內部的代理類Proxy。 下面分析其中的方法:
- asInterface(android.os.IBinder obj):用于將服務器端的Binder對象轉化成客戶端所需的AIDL接口類型的對象,這種轉換過程是區分進程的,如果客戶端和服務端是在同一進程中,那么這個方法返回的是服務端的Stub對象本身,否則返回的是系統封裝的Stub.Proxy對象。
- asBinder:返回當前Binder對象
- onTransact:這個方法運行在服務端中的Binder線程池中,當客戶端發起跨進程請求時,遠程請求會通過系統底層封裝后交由此方法來處理。
這個方法的原型是public Boolean onTransact(int code, Parcelable data, Parcelable reply, int flags)
服務端通過code可以知道客戶端請求的目標方法,接著從data中取出所需的參數,然后執行目標方法,執行完畢之后,將結果寫入到reply中。如果此方法返回false,說明客戶端的請求失敗,利用這個特性可以做權限驗證(即驗證是否有權限調用該服務)。 - Proxy#[Method]:代理類中的接口方法,這些方法運行在客戶端,當客戶端遠程調用此方法時,它的內部實現是:首先創建該方法所需要的參數,然后把方法的參數信息寫入到_data中,接著調用transact方法來發起RPC請求,同時當前線程掛起;然后服務端的onTransact方法會被調用,直到RPC過程返回后,當前線程繼續執行,并從_reply中取出RPC過程的返回結果,最后返回_reply中的數據。
首先,當客戶端發起遠程請求時,由于當前線程會被掛起直至服務端進程返回數據,所以如果一個遠程方法是很耗時的,那么不能在UI線程發起此遠程請求;
其次,由于服務端的Binder方法運行在Binder的線程池中,所以不管Binder是否耗時都應該采用同步的方式去實現,因為它已經運行在一個線程中了。
Binder兩種重要的方法 linkToDeath 和 unlinkToDeath
Binder運行在服務端,如果由于某種服務端異常終止了的話會導致客戶端的遠程調用失敗、所以Binder提供了兩個配對的方法linkToDeath和unlinkToDeath,通過linkToDeath方法可以給Binder設置一個死亡代理,當Binder死亡的時候客戶端就會收到通知,然后就可以重新發起連接從而恢復連接了。
如何給Binder設置死亡代理
1、聲明一個DeathRecipient對象、DeathRecipient是一個接口,其內部只有一個方法bindDied,實現這個方法就可以在Binder死亡的時候收到通知了。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (mRemoteBookManager == null) return;
mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
mRemoteBookManager = null;
// TODO:這里重新綁定遠程Service
}
};
2、在客戶端綁定遠程服務成功之后,給binder設置死亡代理
mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);
2.4 Android的IPC方式
1、 使用Bundle
Bundle實現了Parcelable接口,Activity、Service和Receiver都支持在Intent中傳遞Bundle數據
2、 使用文件共享
這種方式簡單,適合在對數據同步要求不高的進程之間進行通信,并且要妥善處理并發讀寫的問題,SharedPreferences是一個特例,雖然它也是文件的一種,但是由于系統對它的讀寫有一定的緩存策略,即在內存中會有一份SharedPreferences文件的緩存,因此在多進程模式下、系統對它的讀寫就變的不可靠,當面對高并發讀寫訪問的時候,有很大幾率會丟失,因此,不建議在進程間通信中使用SharedPreferences。
3、 使用Messenger
Messenger是一種輕量級的IPC方案,它的底層實現就是AIDL。Messenger是以串行的方式處理請求的,即服務端只能一個個處理,不存在并發執行的情形。
4、 使用AIDL
大致流程:首先建一個Service和一個AIDL接口,接著創建一個類繼承自AIDL接口中的Stub類中的抽象方法,在Service的onBind方法中返回這個類的對象,然后客戶端就可以綁定服務端Service,建立連接后就可以訪問遠程服務端的方法了。
AIDL支持的數據類型:基本數據類型、String和CharSequence、ArrayList、HashMap、Parcelable以及AIDL;
某些類即使和AIDL文件在同一個包中也要顯式import進來;
AIDL中除了基本數據類,其他類型的參數都要標上方向:in、out或者inout;
AIDL接口中支持方法,不支持聲明靜態變量;
為了方便AIDL的開發,建議把所有和AIDL相關的類和文件全部放入同一個包中,這樣做的好處是,當客戶端是另一個應用的時候,可以直接把整個包復制到客戶端工程中。
-
RemoteCallbackList是系統專門提供的用于刪除跨進程Listener的接口。RemoteCallbackList是一個泛型,支持管理任意的AIDL接口,因為所有的AIDL接口都繼承自IInterface接口。
?
5、使用ContentProvider
ContentProvider主要以表格的形式來組織數據,并且可以包含多個表;
ContentProvider還支持文件數據,比如圖片、視頻等,系統提供的MediaStore就是文件類型的ContentProvider;
ContentProvider對底層的數據存儲方式沒有任何要求,可以是SQLite、文件,甚至是內存中的一個對象都行;
-
要觀察ContentProvider中的數據變化情況,可以通過ContentResolver的registerContentObserver方法來注冊觀察者;
?
6、使用Socket
套接字,分為流式套接字和用戶數據報套接字兩種,分別對應于網絡的傳輸控制層中TCP和UDP協議。
- TCP協議是面向連接的協議,提供穩定的雙向通信功能,TCP連接的建立需要經過"三次握手"才能完成,為了提供穩定的數據傳輸功能,其本身提供了超時重傳功能,因此具有很高的穩定性
- UDP是無連接的,提供不穩定的單向通信功能,當然UDP也可以實現雙向通信功能,在性能上,UDP具有更好的效率,其缺點是不保證數據能夠正確傳輸,尤其是在網絡擁塞的情況下。
Binder連接池
當項目規模很大的時候,創建很多個Service是不對的做法,因為service是系統資源,太多的service會使得應用看起來很重,所以最好是將所有的AIDL放在同一個Service中去管理。整個工作機制是:每個業務模塊創建自己的AIDL接口并實現此接口,這個時候不同業務模塊之間是不能有耦合的,所有實現細節我們要單獨開來,然后向服務端提供自己的唯一標識和其對應的Binder對象;對于服務端來說,只需要一個Service,服務端提供一個queryBinder接口,這個接口能夠根據業務模塊的特征來返回相應的Binder對象給它們,不同的業務模塊拿到所需的Binder對象后就可以進行遠程方法調用了。
Binder連接池的主要作用就是將每個業務模塊的Binder請求統一轉發到遠程Service去執行,從而避免了重復創建Service的過程。-
建議在AIDL開發工作中引入BinderPool機制。當新業務模塊加入新的AIDL,那么在它實現自己的AIDL接口后,只需要修改BinderPoolImpl中的queryBinder方法,給自己添加一個新的binderCode并返回相對應的Binder對象即可,不需要添加新的Service。
?