Android開發藝術-第二章 IPC 機制
2.1 Android IPC 簡單介紹
IPC 意為進程間通信或者跨進程通信,線程是 CPU 調度的最小單元,是一種有限的系統資源。
進程一般指一個執行單元。不論什麽操作系統都須要對應的 IPC 機制。
如 Windows 上能夠通過剪切板 管道 和郵槽來進行;Linux 上能夠通過命名管道 共享內容 信號量等來進行。
在 Android 中最有特色的進程間通信方式就是 Binder 了,同一時候也支持 Socket 實現隨意兩個終端之間的通信。
2.2 Android 中的多進程模式
(1) 通過給四大組件指定 android:process 屬性,能夠開啟多線程模式。默認進程的進程名字是包名。
android:process=":sunquan"
android:process="cn.sunquan.com.xx"
“:”指當前的進程名前面附加上當前的包名,簡寫的方式。其次進程名以“:”開頭的進程屬於當前應用的私有進程,其它應用組件不能夠跟它在同一個進程中,而進程名不以“:”開頭的進程屬於全局進程。其它的應用能夠通過 ShareUID 方式和它跑到同一個進程中。
(2) 系統為每個應用分配一個唯一的 UID,具備同樣 UID 的應用才幹共享數據。兩個應用通過 ShareUID 跑在同一個進程中須要有同樣 ShareUID 的而且簽名同樣。在這樣的情況下它們能夠相互訪問對方的私有數據比方 data 文件夾、 組件信息等。無論它們是否跑在同一個進程中。
當然假設它們跑在同一個進程中,那麽除了能共享 data 文件夾、組件信息,還能夠共享內存數據,或者說它們看起來像一個應用的兩個部門。
(3) Android 為每個應用分配了一個獨立的虛擬機,或者說為每個進程都分配了一個獨立的虛擬機。不同的虛擬機在內存分配上有不同的地址空間,這就導致在不同虛擬機上訪問同一個類的對象會產生多份副本。所以執行在不同進程中的四大組件。僅僅要它們之間須要通過內存來共享數據,都會共享失敗,這就是多進程所帶來的主要影響。
(4) 多進程通常會造成例如以下幾方面的問題:
1. 靜態成員和單例模式全然失效:不在同一塊內存
2. 線程同步機制全然失效:無論鎖對象還是鎖全局類都無法保證線程同步,由於不同進程鎖的不是一個對象。
3. SharePreference 的可靠性下降:SharePreference 底層是通過讀/寫 XML 文件來實現,並發寫顯然可能出問題。SharePreference 不支持兩個進程同一時候執行寫操作,否則會有一定幾率的丟失。
4. Application 會多次被創建:當一個組件跑在一個新的進程中。系統會創建新的進程同一時候分配獨立的虛擬機。應用重新啟動一次,會創建新的 Application。
執行在同一個進程中的組件屬於同一個虛擬機和同一個 Application。不同進程的組件的確會擁有獨立的虛擬機、Application 以及內存空間。
同一應用的不同組件,運作在不同的進程中,那跟它們分別屬於兩個應用的部門沒有本質差別。
(5) 盡管不能直接共享內存可是通過跨進程通信還是能夠實現數據交互。實現跨進程的方式:通過 Intent 來傳遞數據;共享文件;SharePreference;基於 Binder 的 Messager 和 AIDL 以及 socket。
2.3 IPC 基礎概念介紹
(1) Serializable 是 Java 所提供的一個序列化接口,為對象提供標準的序列化和反序列化操作。Parceable 接口是 Android 提供的序列化方式。
(2) 實現 Serializable 接口,並聲明一個 serialVersionUID 就可以讓一個對象實現序列化。serialVersionUID 是一串long型的數字,用來輔助序列化和反序列化過程。
原則上序列化後的數字中的 serialVersionUID 僅僅有和當前累的 serialVersionUID 同樣才幹夠正常的被反序列化。serialVersionUID 的具體工作機制:序列化得時候系統會把當前類的 serialVersionUID 寫入序列化得文件裏(也能夠是其它中介),當反序列化得時候系統會檢測文件裏的 serialVersionUID,看它是否和當前類的 serialVersionUID 一致,如一致說明序列化的類的版本號和當前類的版本號同樣。這個時候反序列化能夠成功,否則說明當前類和序列化的類相比發生了某些變換,一般來說我們應該指定 serialVersionUID 的指。
註意:1.靜態成員變量屬於類不屬於對象,不參與序列化過程;2.用transient 關鍵字標記的成員變量不參與序列化過程。
(3) 實現 Parceable 接口,一個類的對象能夠通過實現序列化並能夠通過 Intent 和 Binder 傳遞。Pacel 內部包裝了可序列化的數據,能夠在 Binder 中自由傳輸,能夠直接序列化得有 Intent、Bundle、Bitmap、List、Map等。前提是它們裏面的每個元素都是可序列化的。
(4) Serializable 和 Parceable 的差別:Serializable 是 Java 中的序列化接口,其使用起來簡單可是由於大量 I/O 操作而開銷大。
Parceable 是 Android 中的序列化方式。更適用在 Android 平臺上,缺點是使用起來麻煩,可是效率高。Parceable 主要用在內存序列上,而序列化到存儲設備上或者序列化後通過網絡傳輸則建立適用 Serializable。
(5) Binder 是 Android 中的一個類。它實現了 Binder 接口。從 IPC 角度來說,Binder 是 Android 中一種跨進程通信方式。Binder 還能夠理解為一種虛擬的物理設備,設備驅動是 /dev/binder。該通信方式在 Linux 中沒有;從 Android Framework 角度來說。Binder 是ServiceManager 連接各種 Manager 和對應 ManagerService 的橋梁;從 Android 應用層來說,Binder 是client和服務端進行通信的媒介。當 bindService 的時候。服務端會返回一個包含了服務端業務調用的 Binder 對象。通過這個 Binder 對象,client就能夠獲取服務端提供的服務或數據。這裏的服務包含普通服務和基於 AIDL 的服務。
Android 開發中。Binder 主要用在 Service 中。包含 AIDL 和 Messager。當中普通 Service 中的 Binder 不涉及進程間的通信。較為簡單。
而 Messager 的底層事實上也是 AIDL。
(6) aidl工具依據 aide文件自己主動生成 Java 接口的解析:聲明了幾個接口的方法,同一時候聲明幾個整型 id 來標識這幾個接口,id 用來標識在 transact 過程中client請求的是哪個方法。接著會聲明一個 內部類 Stub 。這個 Stub 就是一個 Binder 類。 當client和服務器位於同一個進程中。則不會走 跨進程的 transact 過程,假設不在同一個進程,方法調用須要走 transact 過程,這個邏輯有 Stub 內部代理 Proxy 來完畢。
其核心實現就是它的內部類 Stub 和 Stub 內部代理 Proxy。
其幾個方法分析例如以下:
1. asInterface (android.os.Binder obj):用於將服務端的 Binder 對象轉換成client所需的 AIDL 接口類型的對象。
假設client和服務端位於同一進程。那麽此方法返回服務端的 Stub 對象本身,否則返回系統封裝後的 Stub.proxy 對象。
2. asBinder:用於放回當前 Binder 對象
3. onTransact :執行在服務端中的 Binder 線程中,當client發起跨進程請求中,遠程請求會通過系統底層封裝後由此方法來處理。
該方法的原型為 public Boolean onTranact (int code, android.os.Parcel data, android.os.Parcel reply, int flags)。服務端通過 code 能夠確定client所請求的目標方法是什麽,接著從 data 中取出目標方法所需的參數(假設目標方法有參數的話)。然後執行目標方法。當目標方法執行完畢後。就向 reply 中寫入返回值(假設目標方法有返回值的話)。
假設此方法返回 false。那麽client的請求會失敗,能夠通過這個特性做權限驗證。
4. Proxy#[method] :執行在client,當client調用此方法時,首先創建該方法所需的輸入型 Parcel 對象 _data、輸出型 Parcel 對象 _repley 和返回值對象。把該方法的參數信息寫入 _data 中(假設有參數),然後調用 transact 方法發起 RPC(遠程過程調用)請求,同一時候當前線程掛起。然後服務端的 onTransact 方法會被調用,直到 RPC 過程返回後。當前線程繼續執行,並從 _reply 中取出 RPC 過程的返回結果。最後返回 _reply 中的數據。
註意:1.當client發起請求時,由於當前線程會被掛機直到服務端進程返回數據。假設遠程方法耗時,那麽不能在 UI 線程中發起此遠程請求。2.服務端的 Binder 方法執行在 Binder 的線程池中。無論 Binder 方法是否耗時都應採用同步的方式去實現,由於執行在一個線程中。
5. AIDL 文件的本質就是系統提供一種高速實現 Binder 的工具。我們能夠自己手動寫。也能夠通過 AIDL 文件讓系統自己主動生成。
6. Binder 有兩個非常重要的方法:linkToDeath 和 unlinkToDeath。Binder 執行在服務端,服務端進程由於某些原因異常終止了,服務端的 Binder 連接斷裂,導致client遠程調用失敗。通過 linkToDeath 能夠給 Binder 設置一個死亡代理,當 Binder 死亡時,會收到通知,這時能夠又一次發起連接請求從而恢復連接。
設置 Binder 死亡代理例如以下:
首先聲明一個 DeathRecipient 對象,DeathRecipient 是一個接口,內部僅僅有一個方法 binderDied,當 Binder 死亡時候。系統回調此方法,我們能夠在移除之前綁定的 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
}
};
其次在client綁定遠程服務成功後。給 Binder 設置死亡代理:
mservice= IMessageBoxManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient, 0)
當中 linkToDeath 的第二個參數是個標記位。直接設置0就可以。此外通過 Binder 的方法 isBinderAlive 也能夠推斷 Binder 是否死亡。
2.4 Android 中的 IPC 方式
(1) 使用 Bunble:Bunble 實現了 Parcelable 接口,能夠在不同的進程間傳輸。
Bunble 不支持的類型無法通過它在進程間傳輸。
(2) 使用文件共享:由於 Android 系統基於 Linux,並發讀/寫文件能夠沒有限制的進行,兩個線程對同一個文件進行寫操作都是執行,但可能出問題。
文件共享方式適合對數據同步要求不高的進程間進行通信。並要妥善處理並發讀/寫的問題。
SharedPreference 是一個特例,屬於文件的一種。但系統對其的讀/寫有一定的緩存策略,即在內存中會有一份 SharedPreference 文件的緩存,因此在多進程下,系統對它的讀/寫變得不可靠,面對高並發的讀/寫訪問。SharedPreference 有非常大幾率丟失數據。所以不建議在進程間通信中使用 SharedPreference。
(3) 使用 Messenger:Messenger 是一種輕量級的 IPC 方案,其底層實現是 AIDL,以串行方式處理client發來的消息,其服務端一次處理一個請求。不存在並發執行的情形(對於有大量並發請求,Messenger 就不合適適用)。
(4) 使用 AIDL :首先服務端創建一個 Service 用來監聽client的連接請求。創建一個 AIDL 文件,將暴露給client的接口在這個 AIDL 文件裏聲明。最後 Service 中實現這個 AIDL接口。client綁定服務端 Service,建立連接就可訪問遠程服務端的方法。
1. AIDL 支持的數據類型:基本數據類型(int、long、chat、boolean、double 等);String 和 CharSequence;List(僅僅支持 ArrayList,裏面的子元素都必須能夠被 AIDL 支持)。Map(僅僅支持 HashMap。裏面每個元素都必須被 AIDL 支持,包含 key 和 value);Parcelable(所以實現了 Parcelable 接口的對象);AIDL(全部的 AIDL 接口也能夠在 AIDL 文件裏使用)。
2. 自己定義的 Parcelable 對象和 AIDL 文件就算和當前 AIDL 文件位於同一包內也要顯式 import。
3. 假設 AIDL 文件裏用到自己定義 Parcelable 對象,必須新建一個和它同名的 AIDL 文件,並在當中聲明它為 Parcelable 類型。
4. AIDL 中除了基本數據類型,其它類型的參數必須標上方向:in、out 或者 inout。in 表示輸入類型參數,out 表示輸出型參數。
5. AIDL 接口中僅僅支持方法,不支持聲明靜態常量。差別於傳統的接口。
6. 為方便 AIDL 的開發。建議把全部和 AIDL 相關的類和文件全部放入同一個包中。當client是還有一個應用時。能夠直接把整個包拷貝到clientproject中。
7. RemoteCallbackList 是系統專門提供用於刪除進程 listener 的接口,RemoteCallbackList 是一個泛型。支持管理隨意的 AIDL 接口,全部的 AIDL 接口都繼承自 Interface 接口,在它的內部有一個 Map 結構專門用來保存全部的 AIDL 回調。其 Map 的 key 是 Binder 類型,value 是 Callback 類型。當client進程終止後,它能夠自己主動移除client所註冊的 listener。
另外 RemoteCallbackList 內部自己主動實現了線程同步的功能,所以使用它進行註冊和解註冊時。不須要額外的線程同步工作。使用 RemoteCallbackList 須要註意是:它不是一個 List。遍歷 RemoteCallbackList 須要依照下面方式進行。當中 beginBroadcast 和 finishBroadcast 必須配對使用,哪怕僅僅是獲取 RemoteCallbackList 中的元素個數:
int N =mListenerList.beginBroadcast();
for(int i = 0; i < N; i++){
//
}
mListenerList.finishBroadcast();
- client的 onServiceConnected 和 onServiceDisconnected 方法都執行在 UI 線程中,所以不能夠在它們裏面直接調用服務端的耗時方法。
服務端的方法本身就執行在服務端的 Binder 線程池中,所以服務端方法本身就能夠執行大量耗時操作,這個時候切記不要在服務端方法中開線程去進行異步任務。除非明白幹什麽。
- Binder 可能意外死亡。須要又一次連接服務。有兩種方法:給 Binder 設置 DeathRecipient 監聽,死亡時會收到 binderDied 方法回調,然後能夠重連;一種是在 onServiceDisconnected 中重連。其二者差別:onServiceDisconnected 在client的 UI 線程中被回調。binderDied 在client的 Binder 線程池中被回調,不能訪問 UI。
- AIDL 使用經常使用的權限驗證方法: 一是在 onBind 中進行驗證,驗證不通過返回 null,client直接無法綁定服務。驗證方式如 使用 permission 驗證,在 AndroidMenifest 中聲明所需權限(自己定義 permission)。此方式同樣適用於 Messenger 中。二是在服務端 onTransact 方法中進行權限驗證。假設驗證失敗返回 false,這樣服務端就不會終止執行 AIDL 中的方法從而達到保護服務端的效果。驗證方式也能夠採用permission。還能夠採用 Uid 和 Pid 來做驗證。通過 getCallingUid 和 getCallingPid 拿到client所屬應用的 Uid 和 Pid。
這個兩個參數能夠用來做一些驗證工作,比方驗證包名。
(5) 使用 ContentProvider:ContentProvider 是 Android 中提供的專門用於不同應用間進行數據共享方式。和 Messenger 一樣,其底層實現是 Binder,當事實上現過程比 AIDL 簡單很多。主要以表格的形式來組織數據,能夠包含多個表。還支持文件數據,比方圖片和視頻等。文件數據和表格數據的結構不同,因此處理此類數據能夠在 ContentProvider 中返回文件的句柄給外界從而讓文件來訪問 ContentProvider,Android 系統提供的 MediaStore 功能就是文件類型的 ContentProvider。
ContentProvider 的底層數據看起來像一個 SQLite 數據庫,可是 ContentProvider 對底層的數據存儲方式沒有不論什麽要求。能夠使用 SQLite 數據庫,也能夠使用普通文件,甚至能夠採用內存中的一個對象進行數據的存儲。要觀察一個 ContentProvider 中的數據改變情況,能夠通過 ContentResolver 的 registerContentObserver 方法來註冊觀察者。能夠通過 unregisterContentObserver 方法來解除觀察者。
(6) 使用 Socket : Socket 被稱為套接字,分為流式套接字和用戶數據報套接字兩種。分別對應於網絡的傳輸控制層中的 TCP 和 UDP 協議。
2.5 Binder 連接池
當項目到一定程度,無限制的添加 Service 是不正確的,Service 是四大組件之中的一個。也是一種系統資源。我們須要將全部的 AIDL 放在同一個 Service 中去管理。工作機制:每個業務模塊創建自己的 AIDL 接口並實現此接口,不同的業務模塊之間是不能有耦合的。全部實現細節都單獨開來,然後向服務端提供自己的唯一標識和其相對應的 Binder 對象。對於服務端來說僅僅須要一個 Service 就能夠了,服務端提供一個 queryBinder 接口,這個接口能夠依據業務模塊的特征來返回對應的 Binder 對象給它們。不同的業務模塊拿到所需的 Binder 對象後就能夠進行遠程方法調用,由此可見,Binder 連接池的主要作用就是將每個業務模塊的 Binder 請求同一轉發到遠程 Service 中去執行,從而避免反復創建 Service。建議在開發中使用 BinderPool。
具體源代碼查看:
https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_2/src/com/ryg/chapter_2/binderpool/BinderPool.java
2.6 選用合適的 IPC 方式
Android開發藝術-第二章 IPC 機制