2.1 Android IPC 簡介
IPC 是 Inter-Process Communication 的縮寫绢馍,含義為進程間通信或者跨進程通信蛮穿,是指兩個進程之間進行數(shù)據(jù)交換的過程。IPC 不是 Android 中所獨有的涂臣,任何一個操作系統(tǒng)都有響應的 IPC 機制。
2.2 Android 中的多進程模式
2.2.1 開啟多進程模式
在Android中使用多進程只有一種方法,即給四大組件在 AndroidManifest 中指定 android:process 屬性流炕。
<activity android:name=".MainActivity">
<intent-filter>
....
</intent-filter>
</activity>
<activity android:name=".SecondActivity"
android:process=":remote">
</activity>
<activity android:name=".ThirdActivity"
android:process="kjn.com.chapter5.remote">
</activity>
Android Studio 使用 adb 命令(使用 adb 命令需要先配置 adb 環(huán)境)查看進程
C:\Users\Administrator.KTS-20160106YSM\Desktop\Chapter5>adb shell ps
...
u0_a94 10485 1256 1407444 52272 SyS_epoll_ 00000000 S kjn.com.chapter5
u0_a94 10508 1256 1405248 50748 SyS_epoll_ 00000000 S kjn.com.chapter5:remote
u0_a94 10525 1256 1405496 52776 SyS_epoll_ 00000000 S kjn.com.chapter5.remote
上述代碼有兩種命名方式:
- :remote: 冒號前面自動加上包名澎现,屬于當前應用的私有進程,其他組件不可以和它跑在同一個進程中每辟。
- 包名.remote :完整的命名方式剑辫,屬于全局進程,其他應用通過 ShareUID 方式可以和它跑在同一個進程中影兽。
我們知道 Android 系統(tǒng)會為每一個應用分配一個唯一的 UID揭斧,具有相同 UID 的應用才能共享數(shù)據(jù),在這種情況下他們可以互相訪問對方的似有數(shù)據(jù)峻堰,比如 data 目錄讹开、組件信息等。這里要說明的是捐名,兩個應用通過 UID 跑在同一個進程中是要求有相同是 ShareUID 和簽名才行旦万,跑在同一個進程中,他們還可以共享內(nèi)存數(shù)據(jù)镶蹋,或者說它們看起來就像是一個應用的兩個部分成艘。
2.2.2 多進程模式的運行機制
新建一個 UserManager 類,這類里只有一個靜態(tài)成員變量
public class UserManager {
public static int sUserId = 1;
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
UserManager.sUserId = 2;
Log.d("MainActivity", "UserManage.sUserId=" + UserManager.sUserId);
}
在 MianActivity 中改變 sUserId 為 2贺归,然后跳轉(zhuǎn)到 SecondActivity 中淆两,并打印 sUserId 的值。
kjn.com.chapter2 D/MainActivity: UserManage.sUserId=2
kjn.com.chapter2:remote D/SecondActivity: UserManage.sUserId=1
上述問題出現(xiàn)的原因是 SecondActivity 運行在一個單獨的進程中拂酣,Android 會為每一個進程應用分配一個獨立的虛擬機秋冰,不同的虛擬機在內(nèi)存分配上有不同的地址空間,這導致不同的虛擬機在訪問同一個類的對象會產(chǎn)生多份副本婶熬,上面 UserManage 就是這種情況剑勾,在一個進程中修改 sUserId 只會影響當前進程。
所有運行不在不同進程中的四大組件赵颅,只要它們之間通過內(nèi)存來共享數(shù)據(jù)虽另,都會共享失敗,這也是多進程帶來的主要影響饺谬。一般來說捂刺,多進程會造成如下幾個方面的問題:
(1)靜態(tài)成員和單例模式完全失效。
就是上面這個案例商蕴。
-
(2)線程同步機制完全失效叠萍。
- 因為不在一塊內(nèi)存上,那么不管是鎖對象還是鎖全局類都無法包裝線程同步绪商,因為不同進程鎖的不是同一個對象苛谷。
(3)SharedPreferences 的可靠性下降。
因為 SharedPreferences 不支持兩個進程同時去執(zhí)行操作格郁,否則會導致一定幾率的數(shù)據(jù)丟失腹殿,這是因為 SharedPreferences 底層是通過讀寫 XML 文件來實現(xiàn)的独悴,并發(fā)寫顯然是可能出問題的,甚至并發(fā)讀寫都有可能出問題锣尉。
(4)Application 會多次創(chuàng)建刻炒。
因為當一個組件跑在一個新的進程中的時候,由于系統(tǒng)要在創(chuàng)建新的進程同事分配獨立的虛擬機自沧,所以這個過程相當于就是啟動一個應用的過程坟奥,就會有兩個 Application。
系統(tǒng)提供了很多跨進程通信方法拇厢,雖然說不能直接地共享內(nèi)存爱谁,我們還可以使用 Intent 來傳遞數(shù)據(jù),共享文件 和 SharedPreferences 孝偎,基于 Binder 的 Messenger 和 AIDL 以及 Socekt 等等访敌。
2.3 IPC 基礎概念介紹
2.3.1 Serializable 接口
Serializable 是 Java所提供的一個序列化接口,它是一個空接口衣盾,為對象提供標準的序列化和反序列化操作寺旺。,具體實現(xiàn)可以參考以下代碼:
//序列化
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();
上述代碼演示了采用 Serializable 方式序列化對象的典型過程势决,只需要把實現(xiàn) Serializable 接口的 User 對象寫到文件中就可以快速回復了阻塑,恢復之后的內(nèi)容是完全一樣的。
private static final long serialVersionUID = 519067123721295773L;
serialVersionUID 的作用是:當序列化對比反序列的 serialVersionUID 一致的時候可以正常的反序列化果复。如果不指定叮姑,系統(tǒng)會計算當前類的 hash 值并賦值給 serialVersionUID,而當前類的有所改變据悔,比如增加或者刪除了某些成員變量,那么系統(tǒng)會重新計算當前類的 hash 值然后重新賦值給 耘沼,反序列化失敗這時候系統(tǒng)就會 crash极颓。修改類名和成員變量類型,就算 serialVersionUID 通過了群嗤,一樣會失敗菠隆。
盡量手動指定 serialVersionUID (比如 1 L)或者工具按當前目錄結(jié)構(gòu)自動生成。
- 靜態(tài)成員變量屬于類不屬于對象狂秘,所以不會參與序列化過程
- 其次用 transient 關鍵字標記的成員變量不參與序列化過程
2.3.2 Parcelable 接口
Parcel 內(nèi)部包裝了可序列化的數(shù)據(jù)骇径,可以在 Binder 中自由的傳遞。序列化功能由 writeToParcel 方法來完成者春,反序列化功能由 CREATOR 來完成破衔,其內(nèi)部標明了如何創(chuàng)建序列化對象和數(shù)組;內(nèi)容描述功能由 describeContents 方法來完成钱烟。
<div align = center>Parcelable 的方法說明</div>
方法 | 功能 | 標記位 |
---|---|---|
createFromParce l (Parcel in) | 從序列化后的對象中創(chuàng)建原始對象 | |
newArray(int size) | 創(chuàng)建指定長度的原始對象數(shù)組 | |
User(Parcel in) | 從序列化后的對象中創(chuàng)建原始對象 | |
writeToParcel(Parcel out, int flags) | 將當前對象寫入序列化結(jié)構(gòu)中晰筛,其中 flags 標識有兩種值:0或者 1(參見右側(cè)標記位)嫡丙。為 1 時標識當前對象需要作為返回值返回,不能立即釋放資源读第,幾乎所有情況都為 0 | PARCELABLE_WRITE_REYURN_VALUE |
describeContents() | 返回當前對象的內(nèi)容描述曙博,如果含有文件描述符,返回 1(參見右側(cè)標記)怜瞒,否則返回 0父泳,幾乎所有情況都返回 0 | CONTENTS_FILE_DESCRIPTOR |
Intent、Bundle吴汪、Bitmap 等都實現(xiàn)了 Parcelable 接口都可以直接序列化惠窄,同時 List 和 map 也可以序列化,前提它們里面每個元素都是可序列化的浇坐。
- Serializable 是 java 中的序列化接口睬捶,使用起來簡單但是開銷大,序列化和反序列化過程需要大量 I/O 操作近刘。
- Parcelable 是Android 的序列化方式擒贸,因此適合用在 Android 平臺上,缺點是使用起來稍微麻煩觉渴,但是它的效率高介劫,因此我們首選 Parcelable。
Parcelable 和 Serializable 都是實現(xiàn)序列化并且都可以用于 Intent 間的數(shù)據(jù)傳遞案淋。Parcelable 主要用于內(nèi)存序列化上座韵,通過 Parcelable 將對象序列化到存儲設備中或者將對象序列化后通過網(wǎng)絡傳輸,但這個過程會稍顯復雜踢京,因此這兩種情況建議大家適用 Serializable 誉碴。
2.3.3 Binder
- 實現(xiàn) IBinder 接口,主要應用在 Service 中瓣距,是客戶端和服務端進行通信的媒介黔帕,服務端向客戶端返回實現(xiàn)了業(yè)務接口的 Binder 對象(Service 的 onBind 方法)
- 是 Android 中一種跨進程通信的方式
- 可以理解成一個虛擬物理設備,設備驅(qū)動為 /dev/binder ?? 此處不理解...
- 是連接各種 Manager(ActivityManager蹈丸,WindowManager 等)和相應的 ManagerService 的橋梁
AIDL
AIDL 的流程:
- 創(chuàng)建一個 Service 和一個 AIDL 接口成黄;
- 創(chuàng)建一個類繼承自 AIDL 接口中的 Stub 類并實現(xiàn)抽象方法
- 在 Service 的 onBind 方法中返回這個類的對象
- 客戶端綁定服務端 Service,建立鏈接就可以訪問遠程服務端的方法了逻杖。
AIDL 作為接口定義文件奋岁,與普通的接口定義方法有些許不同,然而大致是類似的荸百。
- AIDL 支持的類型
- 基本數(shù)據(jù)類型
- String 和 CharSequence
- 實現(xiàn)了 Parcelable 接口的對象
- AIDL 接口本身的類型
- 對于集合闻伶,AIDL 僅支持兩種類型
- ArrayList,且里面的每個元素必須被 AIDL 支持
- HashMap管搪,且里面的每個 key和 value 必須被 AIDL 支持
- 如果在 AIDL 文件中出現(xiàn)了定義的 Parcelable 對象虾攻,需要新建一個與之同名的 AIDL 文件铡买,并聲明為 parcelable 類型。
package *.*.*;
parcelable Something;
3.定義業(yè)務接口霎箍,AIDL 中除了基本類型都要標識方向:in/out/inout奇钞,只支持方法,不支持靜態(tài)常量漂坏。
package *.*;
import *.Something; // 即便處于同一個包下景埃,也要 import 語句
interface ISomeManager {
List<Something> getSomethingList();
void doSomething(in Something sth);
}
源碼分析
Android 開發(fā)中,Binder 主要用在 Service 中顶别,包括 AIDL 和 Messager聪蘸,其中普通 Service中的 Binder 不涉及進程間通信拗胜,無法觸及 Binder 的核心胆数,而 Messager 的底層其實是 AIDL刻蚯,所以這里選擇用 AIDL 來分析 Binder 的工作機制。
AIDL 類似一個接口類剩失,通過 build 一下 project屈尼,構(gòu)建工具(build-tools 文中的 aidl.exe 程序)會編譯出一個類,在 build/generated/source/aidl/debug/ 的路徑下拴孤,我們找到了 AIDL 文件生成的類 IBookManager.java脾歧,下面我們就來閱讀這些源碼,來一窺究竟演熟。
public interface IBookManager extends android.os.IInterface {
public static abstract class Stub extends android.os.Binder implements com.ryg.chapter_2.aidl.IBookManager {
...
代碼很長鞭执,直接看目錄結(jié)構(gòu)( ctrl + 7 )
DESCRIPTOP 是 Binder 的唯一標識,一般用當前 Binder 的類名表示芒粹。
private static final java.lang.String DESCRIPTOR = "com.ryg.chapter_2.aidl.IBookManager";
這里的 Stub 類就是我們要關注的地方兄纺,因為它就是 AIDL 的實現(xiàn)類:
首先看它的 asInterface 方法,因為我們要在 ServiceConnection 連接成功后化漆,根據(jù)此方法將服務器返回的 Binder 轉(zhuǎn)換為遠程業(yè)務接口:
public static com.ryg.chapter_2.aidl.IBookManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.ryg.chapter_2.aidl.IBookManager))) {
return ((com.ryg.chapter_2.aidl.IBookManager) iin);
}
return new com.ryg.chapter_2.aidl.IBookManager.Stub.Proxy(obj);
}
語意很清晰囤热,首先拿一個字符串(接口類全名)到本地進程查詢(queryLocalInterface),查詢不到則說明這是一個跨進程調(diào)用获三;這里運用到了代理模式(Proxy),因為是跨進程锨苏,就需要額外的步驟疙教,就是將請求參數(shù)和返回值序列化的過程,而單進程就不需要這個步驟了:
@Override
public void addBook(com.ryg.chapter_2.aidl.Book book)
throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book != null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
/**
* 客戶端發(fā)起請求伞租,當前線程阻塞等待返回
* 所以請求遠程服務盡量不要在 UI 線程上發(fā)起
*/
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
/**
* 客戶端請求發(fā)送出去后
* 遠程請求通過底層封裝后回調(diào)服務端的 Binder 的 onTransact 方法進行處理
*/
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
在 public Boolean onTransact(int code贞谓,Parcelable data,Parcelable reply葵诈,int flags) 方法中裸弦,服務端通過 code 知道客戶端請求的目標方法祟同,從data中取出所需的參數(shù),調(diào)用對應的業(yè)務方法理疙,執(zhí)行完畢后將結(jié)果寫入到 reply 中晕城。返回 true 意味著請求成功。利用這個特性可以做驗證是否有權(quán)限調(diào)用該服務窖贤,見 BookManagerService:
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
// 檢測清單是否聲明了以下權(quán)限
int check = checkCallingOrSelfPermission(
"com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
Log.d(TAG, "check=" + check);
if (check == PackageManager.PERMISSION_DENIED) {
return false;
}
// 檢查應用包名是否以 "com.ryg" 開頭
String packageName = null;
String[] packages = getPackageManager().getPackagesForUid(
getCallingUid());
if (packages != null && packages.length > 0) {
packageName = packages[0];
}
Log.d(TAG, "onTransact: " + packageName);
if (!packageName.startsWith("com.ryg")) {
return false;
}
return super.onTransact(code, data, reply, flags);
}
清單中定義權(quán)限
<uses-permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE" />
- 當客戶端發(fā)起遠程請求時砖顷,由于當前線程會被掛起直至服務端進程返回數(shù)據(jù),所以若果一個遠程方法是很耗時的赃梧,那么不能在 UI 線程中發(fā)起此遠程請求滤蝠;
- 其次,由于服務端的 Binder 方法運行在 Binder 的線程池中授嘀,所以 Binder 方法不管是否耗時都應該采用同步的方式去實現(xiàn)物咳,因為它已經(jīng)運行在一個線程中了。
其實我們完全可以不提供 AIDL 文件即可實現(xiàn) Binder蹄皱,之所以提供 AIDL 文件览闰,是為了方便系統(tǒng)為我們生產(chǎn)代碼,系統(tǒng)根據(jù) AIDL 文件生成 Java 文件的格式是固定的夯接。
Binder兩種重要的方法 linkToDeath 和 unlinkToDeath
Binder運行在服務端焕济,如果由于某種服務端異常終止了的話會導致客戶端的遠程調(diào)用失敗、所以Binder提供了兩個配對的方法linkToDeath和unlinkToDeath盔几,通過linkToDeath方法可以給Binder設置一個死亡代理晴弃,當Binder死亡的時候客戶端就會收到通知,然后就可以重新發(fā)起連接從而恢復連接了逊拍。
如何給Binder設置死亡代理
1上鞠、聲明一個DeathRecipient對象、DeathRecipient是一個接口芯丧,其內(nèi)部只有一個方法bindDied芍阎,實現(xiàn)這個方法就可以在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);