Android IPC簡(jiǎn)介
IPC是Inter-Process Communication縮寫,含義為進(jìn)程間通信. 按照操作系統(tǒng)中的描述,線程是cpu調(diào)度的最小單元,而進(jìn)程一般指一個(gè)執(zhí)行單元. 進(jìn)程中可以有一個(gè)或者多個(gè)線程.
不同的操作系統(tǒng)有著不同的IPC機(jī)制:
- Windows: 通過剪切板, 管道, 信號(hào)量來進(jìn)行進(jìn)程間通信
- Linux: 通過命名管道, 共享內(nèi)存, 信號(hào)量等來進(jìn)行進(jìn)行進(jìn)程間通信
- android: 雖然基于Linux內(nèi)核,但是使用了獨(dú)有的Binder機(jī)制, 也可以Socket進(jìn)行通信
使用場(chǎng)景: 可能有些模塊因?yàn)樘厥庠蛐枰\(yùn)行在單獨(dú)的進(jìn)程中; 或者為了加大一個(gè)應(yīng)用可使用的內(nèi)存; 又或者我們需要去另外一個(gè)進(jìn)程去獲取數(shù)據(jù),必然需要跨進(jìn)程.
Android中的多進(jìn)程的模式
開啟多進(jìn)程模式
如果你想在一個(gè)應(yīng)用中使用多個(gè)進(jìn)程,通過清單文件給四大組件添加android:process
屬性,就可以很方便的開啟多進(jìn)程.
還有一種非常規(guī)的創(chuàng)建方式,通過JNI在native層去fork一個(gè)新的進(jìn)程.這種只做了解.
例如這樣,當(dāng)我們依次打開MainActivity, SecondActivity, ThirdActivity.此時(shí)應(yīng)該打開了三個(gè)進(jìn)程.
我們來檢測(cè)一下, 你可以直接使用DDMS來查看進(jìn)程,這里使用命令行來測(cè)試
$ adb shell ps | grep com.szysky
你可以直接使用adb shell ps
這會(huì)把系統(tǒng)所有進(jìn)程展示出來, 你可以加上過濾信息| grep xxx
xxx替換你需要過濾出來信息即可
你可能已經(jīng)發(fā)現(xiàn)在創(chuàng)建新進(jìn)程的時(shí)候使用兩種不同的方式
- 當(dāng)以
:
開頭的進(jìn)程,屬于當(dāng)前應(yīng)用的私有進(jìn)程,其他應(yīng)用的組件不可以和它跑在同一個(gè)進(jìn)程 - 當(dāng)不以
:
開頭,那么進(jìn)程屬于全局進(jìn)程,其他應(yīng)用通過ShareUID
方法可以和它跑在同一個(gè)進(jìn)程
Android系統(tǒng)會(huì)為每一個(gè)應(yīng)用分配唯一的UID. 相同UID的應(yīng)用才能共享數(shù)據(jù). 但是兩個(gè)應(yīng)用通過ShareUID跑在同一個(gè)進(jìn)程是有要求的. 除了具有相同的ShareUID并且還要簽名相同才可以. 這時(shí)如果不在同一進(jìn)程他們之間可以共享data目錄,組件信息等. 如果還在同一進(jìn)程, 那么他們還能共享內(nèi)存數(shù)據(jù).
進(jìn)程模式的運(yùn)行機(jī)制
開啟多進(jìn)程簡(jiǎn)單,但是如果不能處理好其中的特性,那么受傷的總會(huì)是你.
先說第一個(gè)比較嚴(yán)重的問題. 靜態(tài)變量不在共享. 還是直接三個(gè)類的例子,如果在mainActivity中對(duì)靜態(tài)變量進(jìn)行修改, 在SecondActivity取出這個(gè)靜態(tài)發(fā)現(xiàn)是main沒修改之前的. 這說明兩個(gè)進(jìn)程間即使是靜態(tài)屬性也是無法共享.
其實(shí)這是因?yàn)檫@兩個(gè)類運(yùn)行在兩個(gè)進(jìn)程間,而每個(gè)單獨(dú)的進(jìn)程又會(huì)分配一個(gè)獨(dú)立的虛擬機(jī), 所以每個(gè)虛擬機(jī)在內(nèi)存分配上有不同的地址空間.對(duì)于不同虛擬機(jī)訪問同一個(gè)對(duì)象就會(huì)產(chǎn)生多份副本. 副本之間互相獨(dú)立不干擾彼此.
一般情況下多進(jìn)程可能面臨的問題:
- 靜態(tài)成員和單例模式完全失效
- 線程同步機(jī)制完全失效
- SharedPreferences的可靠性下降
- Application會(huì)多次創(chuàng)建
2中因?yàn)椴皇且粔K內(nèi)存,所以不管是鎖對(duì)象還是鎖全局都無法保證線程同步,因?yàn)椴皇峭粋€(gè)對(duì)象. 3中因?yàn)镾p不支持兩個(gè)進(jìn)程同時(shí)讀寫,因?yàn)榈讓邮峭ㄟ^讀寫XML文件實(shí)現(xiàn)的,并發(fā)可能會(huì)觸發(fā)異常. 4中運(yùn)行在多個(gè)進(jìn)程中,那么就會(huì)創(chuàng)建多個(gè)虛擬機(jī),每個(gè)虛擬機(jī)都有一個(gè)對(duì)應(yīng)Application并需要啟動(dòng)加載這個(gè)文件.
一個(gè)應(yīng)用的多進(jìn)程:它就相當(dāng)于兩個(gè)不同的應(yīng)用采用了ShareUID的模式. 每個(gè)進(jìn)程都會(huì)擁有獨(dú)立的虛擬機(jī), Application以及內(nèi)存空間
IPC基礎(chǔ)概念
關(guān)于IPC主要包含三方面的內(nèi)容: Serializable接口, Parcelable接口, 以及Binder
Serializable接口
Serializable是Java提供的一個(gè)序列化接口,這個(gè)一個(gè)空接口. 如果我們想使用只需要實(shí)現(xiàn)Serializable
接口,并聲明一個(gè)long類型的常量serialVersionUID
(不聲明也是可以,但是在反序列化會(huì)出現(xiàn)錯(cuò)誤).
javabean 的實(shí)現(xiàn)
public class Student implements Serializable{
public static final long serialVersionUID = 123456789L;
//.....省略創(chuàng)建屬性,打印等操作
}
序列化的代碼如下圖,并附上結(jié)果.
好了說一下serialVersionUID
這個(gè)屬性. 即使我們不聲明系統(tǒng)會(huì)根據(jù)當(dāng)前類結(jié)構(gòu)(成員變量等)生成一個(gè)hash為serialVersionUID
, 雖然這樣也可以但是如果在你把一個(gè)對(duì)象序列化的到磁盤的一個(gè)文件的時(shí)候. 對(duì)這個(gè)對(duì)象增加了一個(gè)成員變量,那么在反序列的時(shí)候就會(huì)報(bào)錯(cuò). 因?yàn)楫?dāng)你反序列化的時(shí)候?qū)ο笕绻麤]有serialVersionUID
還會(huì)重新計(jì)算.這時(shí)反序列化的hash和序列化的hash就不一致了.
關(guān)于根據(jù)當(dāng)前類結(jié)構(gòu)計(jì)算hash值,有兩點(diǎn)需要注意:
- 靜態(tài)成員變量屬于類不屬于對(duì)象,所以不參與序列化的過程
- 其次用
transient
關(guān)鍵字標(biāo)記的成員變量不參與序列化的過程.
系統(tǒng)默認(rèn)的序列化過程是可以改變的,通過實(shí)現(xiàn)writeObject
和readObject
可以重寫默認(rèn)的序列化和反序列化過程. 這里就不詳細(xì)說明
Parcelable接口
系統(tǒng)已經(jīng)為我們提供了很多實(shí)現(xiàn)了Parcelable接口的類,他們都可以直接序列化. 例如intent
, Bundle
, Bitmap
, 同時(shí)List和Map也可以序列化.前提是他們里面的每個(gè)元素都可以序列化.
實(shí)現(xiàn)Parcelable接口主要復(fù)寫四個(gè), 我們可以直接定義好javabean直接讓AS幫我們實(shí)現(xiàn).
- writeToParcel() 主要完成序列化功能
- CREATOR 主要完成反序列化
- 接收參數(shù)parcel的構(gòu)造函數(shù) 用于從序列化后的對(duì)象中創(chuàng)建原始對(duì)象
- describeContents() 幾乎所有情況下都返回0,只有當(dāng)前對(duì)象中存在文件描述符時(shí)返回1
關(guān)于Parcelable和Serializable的取舍
- Serializable: 適合序列化到設(shè)備或者序列化后通過網(wǎng)絡(luò)傳輸.
- Parcelable: 主要用在內(nèi)存序列化上. 不需要大量的I/O操作,所以在內(nèi)存中使用高效.
了解Binder[]
- 代碼層面: Binder是Android中的一個(gè)類,它實(shí)現(xiàn)了IBinder接口
- IPC角度: Binder是Android中的一種跨進(jìn)程通信方式.
- 物理設(shè)備角度: Binder也可以認(rèn)為是一種虛擬的物理設(shè)備,設(shè)備驅(qū)動(dòng)是/dev/binder
- Framework角度: Binder是ServiceManager連接各種Manager和相應(yīng)的ManagerService的橋梁.
- Android應(yīng)用角度:Binder是客戶端和服務(wù)端進(jìn)行通信的媒介.
日常開發(fā)中,Binder主要用在Service
包括AIDL
和Messenger
. 而普通的Service中的Binder不涉及進(jìn)程間的通信,無法觸及Binder的核心. 而Messenger底層其實(shí)就是AIDL.所以我們利用AIDL來分析Binder的工作機(jī)制
創(chuàng)建AIDL實(shí)例
新建三個(gè)文件 Book.java
Book.aidl
和IBookManager.aidl
首先創(chuàng)建一個(gè)Book類并實(shí)現(xiàn)Parcelable接口,然后在這個(gè)類所在的包上右鍵,如圖所示
如果名字不能為Book,可以先隨便寫一個(gè),創(chuàng)建之后修改. 然后按圖修改,
文件聲明完,我們只需要重新Make一下工程就可以.Build --> Rebuild Project或者M(jìn)ake Project
Make之后會(huì)在app -> build -> generated -> source -> aidl -> debug -> … 出現(xiàn)系統(tǒng)自動(dòng)生成好的java類. 我們需要對(duì)其進(jìn)行分析
看下圖了解一個(gè)大體結(jié)構(gòu)
上圖圈出了兩個(gè)部分,部分二應(yīng)該很清楚就是我們定義在aidl中的兩個(gè)抽象方法. 而部分一在圖上的內(nèi)部類Stub寫了說明. 我們自定義的兩個(gè)抽象方法,在內(nèi)部類中用了兩個(gè)整形int值來標(biāo)識(shí)兩個(gè)抽象方法,用在transact()
中可以識(shí)別客戶端請(qǐng)求哪個(gè)方法.
這個(gè)繼承了IInterface的接口的核心實(shí)現(xiàn):就是內(nèi)部類Stub和Stub的內(nèi)部代理Proxy
先看一下內(nèi)部類的結(jié)構(gòu)圖:
說明直接放圖,寫在代碼上,在git上的根目錄的aidl的java類說明文件夾也有添加了注釋的類.
有兩點(diǎn)需要注意:
- 客戶端發(fā)起遠(yuǎn)程請(qǐng)求時(shí),當(dāng)前線程會(huì)被掛起直到服務(wù)器進(jìn)程返回?cái)?shù)據(jù),所以注意線程是否在意耗時(shí)
- 由于服務(wù)端Binder方法運(yùn)行在Binder線程池中,所以不管Binder方法是否耗時(shí)都應(yīng)該采用同步方式,因?yàn)橐呀?jīng)在一個(gè)線程中了
我們也可以手動(dòng)實(shí)現(xiàn)Binder類,這里不再細(xì)說,在git倉庫的項(xiàng)目中有一個(gè)manual
包里面是關(guān)于手動(dòng)實(shí)現(xiàn)Binder的代碼.
其實(shí) 不管是手動(dòng)實(shí)現(xiàn)Binder也好,或者AIDL文件實(shí)現(xiàn)Binder也好. 其實(shí)兩者的工作原理都是一樣的, AIDL文件的存在意義是系統(tǒng)為我們提供了一種快速實(shí)現(xiàn)Binder的工具,僅此而已.
Binder生命狀態(tài)的監(jiān)聽
由于Binder是運(yùn)行在服務(wù)端,如果服務(wù)端進(jìn)程異常終止,那么我們到服務(wù)端的Binder連接也就斷裂(Binder死亡).就會(huì)導(dǎo)致調(diào)用失敗,所里系統(tǒng)提供了死亡代理的方法 就是當(dāng)Binder死亡時(shí),我們就會(huì)收到通知,這個(gè)時(shí)候就可以重新發(fā)起連接請(qǐng)求而恢復(fù)連接
首先創(chuàng)建監(jiān)聽的DeathRecipient對(duì)象
IBinder.DeathRecipient mDeat = new IBinder.DeathRecipient() {
// 當(dāng)Binder死亡的時(shí)候,系統(tǒng)會(huì)回調(diào)binderDied()方法
@Override
public void binderDied() {
if (mBookManager == null)
return ;
//清除掉已經(jīng)無用的Binder連接
mBookManager.asBinder().unlinkToDeath(mDeat,0);
mBookManager == null;
//TODO 進(jìn)行重新綁定遠(yuǎn)程服務(wù)
}
};
當(dāng)客戶端綁定遠(yuǎn)程服務(wù)成功的時(shí)候,給binder設(shè)置死亡代理
binder.linkToDeath(mDeat,0);
linkToDeath的第二個(gè)參數(shù)是個(gè)標(biāo)志位,直接設(shè)0即可. 另外也可以通過Binder的方法isBindAlive
也可以判斷Binder是否死亡.
Android的幾種跨進(jìn)程的方式
使用Bundle
由于Bundle實(shí)現(xiàn)了Parcelable接口,所以在四大組件中的三大組件(Activity
, Service
, Receiver
)都支持在Intent中傳遞Bundle.
所以如果在一個(gè)進(jìn)程中啟動(dòng)了另一個(gè)進(jìn)程的三大組件,就可以在Bundle中附加我們需要的信息通過Intent發(fā)送出去. 當(dāng)然傳遞的類型必須是能夠被序列化的, 例如基本數(shù)據(jù)類型,實(shí)現(xiàn)了Parcelable和Serializable接口的對(duì)象和一些Android支持的特殊對(duì)象.
使用文件共享
文件共享適合在對(duì)數(shù)據(jù)同步要求不高的進(jìn)程之間進(jìn)行通信,并且要妥善的處理并發(fā)讀寫的問題.
兩個(gè)進(jìn)程通過讀/寫同一個(gè)文件來交換數(shù)據(jù). 例如進(jìn)程A把數(shù)據(jù)寫入文件中,而進(jìn)程B從文件中讀取出來數(shù)據(jù).
Android是基于Linux系統(tǒng), 所以對(duì)于并發(fā)讀寫文件可以沒有限制的執(zhí)行. 這里不像Windows系統(tǒng),對(duì)于一個(gè)文件如果加了排斥鎖將會(huì)導(dǎo)致其他線程無法對(duì)其進(jìn)行訪問.
關(guān)于這部分的練習(xí), 在之前練習(xí)Binder的時(shí)候已經(jīng)練習(xí)過了. 代碼在項(xiàng)目中的MainActivity中
雖然序列化反序列達(dá)到的效果是可以恢復(fù)對(duì)象里面的屬性值,但是反序列每回都是一個(gè)新的對(duì)象.
SharePreferencess是Android提供的一個(gè)輕量級(jí)方案,通過鍵值對(duì)存儲(chǔ)數(shù)據(jù),底層采用XML文件來進(jìn)行存儲(chǔ). 存儲(chǔ)路徑/data/data/package name/shared_prefs
目錄下. 也屬于文件的一種,但是由于系統(tǒng)對(duì)SP的讀寫存在一定的緩存策略,內(nèi)存中會(huì)有一份緩存,所以多進(jìn)程下,系統(tǒng)對(duì)它的讀寫也就變得不可靠.
使用Messenger
Messenger
(信使). 不同的進(jìn)程中可以傳遞Message對(duì)象, 在Message中放入我們需要傳遞的數(shù)據(jù),就可實(shí)現(xiàn)進(jìn)程間傳遞. Messenger是一種輕量級(jí)的IPC方案,它的底層實(shí)現(xiàn)AIDL
. 看一些構(gòu)造函數(shù)
public Messenger(Handler target) {
mTarget = target.getIMessenger();
}
public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}
無論是IMessenger還是Stub.asInterface. 可以明顯看出AIDL的痕跡.
因?yàn)镸essenger對(duì)AIDL
進(jìn)行了封裝,使得在使用時(shí)更加簡(jiǎn)單,并且它的處理方式是一次處理一個(gè)請(qǐng)求,因此服務(wù)器端不用考慮線程同步因?yàn)榉?wù)端不存在并發(fā)執(zhí)行的情形.
具體實(shí)現(xiàn)Messenger
1.服務(wù)端
創(chuàng)建一個(gè)Service
作為服務(wù)端來處理客戶端的請(qǐng)求, 同時(shí)創(chuàng)建一個(gè)Handle
并通過它來創(chuàng)建一個(gè)Messenger
對(duì)象,然后在Service的onBind()
方法中返回這個(gè)Messenger
對(duì)象底層的Binder
.
最后這個(gè)組件在清單文件中聲明加上android:process="com.szysky.test"
屬性.已達(dá)到模擬多進(jìn)程的場(chǎng)景
public class MessengerService extends Service {
/**
* 編寫一個(gè)類繼承Handler,并對(duì)客戶端發(fā)來的消息進(jìn)行處理操作進(jìn)行添加
*/
private static class MessengerHandler extends Handler{
private static final String TAG = "MessengerHandler";
@Override
public void handleMessage(Message msg) {
switch (msg.what){
//客戶端發(fā)來的信息標(biāo)識(shí)
case MessengerActivity.FROM_CLIENT:
Log.d(TAG, "handleMessage: receive msg form clinet-->" +msg.getData().getString("msg"));
//對(duì)客戶端進(jìn)行reply回答
// 1\. 通過接收到的到客戶端的Message對(duì)象獲取到Messenger信使
Messenger client = msg.replyTo;
// 2\. 創(chuàng)建一個(gè)信息Message對(duì)象,并把一些數(shù)據(jù)加入到這個(gè)對(duì)象中
Message replyMessage = Message.obtain(null, MessengerActivity.FROM_SERVICE);
Bundle bundle = new Bundle();
bundle.putString("reply", "我是服務(wù)端發(fā)送的消息,我已經(jīng)接收到你的消息了,你應(yīng)該在你的客戶端可以看到");
replyMessage.setData(bundle);
// 3\. 通過信使Messenger發(fā)送封裝好的Message信息
try {
client.send(replyMessage);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
}
/**
* 創(chuàng)建一個(gè)Messenger信使
*/
private final Messenger mMessenger = new Messenger(new MessengerHandler());
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
}
別忘了清單文件
<service android:name=".message.MessengerService"
android:process="com.szysky.test"/>
2.客戶端
直接用一個(gè)activity
作為客戶端, 首先綁定之前創(chuàng)建的服務(wù)端的Service
, 綁定成功時(shí)通過ServiceConnection
對(duì)象接收到服務(wù)端返回的IBinder
,用IBinder
對(duì)象創(chuàng)建一個(gè)Messenger
. 通過這個(gè)Messenger
就可以往服務(wù)端發(fā)送消息. 如果我們需要服務(wù)端也能夠回應(yīng)客戶端. 那么就要在客戶端同之前服務(wù)端一樣通過Handle
創(chuàng)建一個(gè)Messenger
對(duì)象, 并把這個(gè)Messenger
在通過連接成功返回的IBinder創(chuàng)建的Message對(duì)象通過replyTo
參數(shù)傳遞給服務(wù)器. 這個(gè)服務(wù)器就可以通過replyTo
參數(shù)來回應(yīng)客戶端.
/**
* 聲明一個(gè)本進(jìn)程的信使 用來監(jiān)聽并處理服務(wù)端傳入的消息
*/
private Messenger mGetReplyMessenger = new Messenger(new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case FROM_SERVICE:
Log.d(TAG, "handleMessage: 這里是客戶端:::"+msg.getData().getString("reply"));
break;
default:
super.handleMessage(msg);
}
}
});
/**
* 創(chuàng)建一個(gè)服務(wù)監(jiān)聽連接對(duì)象 并在成功的時(shí)候給服務(wù)器發(fā)送一條消息
*/
private ServiceConnection mConnection = new ServiceConnection() {
//綁定成功回調(diào)
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 利用服務(wù)端返回的binder對(duì)象創(chuàng)建Messenger并使用此對(duì)象想服務(wù)端發(fā)送消息
Messenger mService = new Messenger(service);
Message obtain = Message.obtain(null, FROM_CLIENT);
Bundle bundle = new Bundle();
bundle.putString("msg", "你好啊, 我是從客戶端來");
obtain.setData(bundle);
// 需要把接收服務(wù)端回復(fù)的Messenger通過Message的replyTo傳遞給服務(wù)端
obtain.replyTo = mGetReplyMessenger;
try {
mService.send(obtain);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
//進(jìn)行遠(yuǎn)端服務(wù)的連接
Intent intent = new Intent(MessengerActivity.this, MessengerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
本例測(cè)試代碼在倉庫對(duì)應(yīng)項(xiàng)目的message包中
在使用Messenger進(jìn)行數(shù)據(jù)傳遞必須將數(shù)據(jù)放入到Message中. 而Messenger和Message都實(shí)現(xiàn)了序列化接口. 所以可以在進(jìn)程間通信.
Message
的能使用的載體只有what, arg1, arg2, Bundle 以及replyTo. 這里有一個(gè)載體需要注意object,它在同一個(gè)進(jìn)程很實(shí)用,但是在版本2.2之前是不支持跨進(jìn)程的,雖然進(jìn)行了改進(jìn)之后,但是也只是支持系統(tǒng)提供實(shí)現(xiàn)的某些對(duì)象才可以. 所以使用的時(shí)候需要注意.
順便繪制了一個(gè)Messenger通信的流程圖, 可以對(duì)代碼的調(diào)用順序理解的更清楚.
使用AIDL
雖然Messenger使用方便, 但是要清楚它是以串行的方式處理客戶端發(fā)來的消息,如果有大量并發(fā)的請(qǐng)求. 或者需求是跨進(jìn)程調(diào)用服務(wù)端的方法時(shí). 就無法使用Messenger. 這個(gè)時(shí)候就該AIDL
對(duì)于使用AIDL的流程簡(jiǎn)單梳理一遍
服務(wù)端
服務(wù)端創(chuàng)建一個(gè)Service
用來監(jiān)聽客戶端的連接請(qǐng)求, 然后創(chuàng)建一個(gè)AIDL文件,將暴露給客戶端的接口在這個(gè)AIDL文件中聲明,最后在Service中實(shí)現(xiàn)這個(gè)AIDL接口并在onBind()
返回即可.
客戶端
綁定服務(wù)端的Service,綁定成功后,將服務(wù)端返回來的Binder對(duì)象轉(zhuǎn)成AIDL接口所屬的類型,接著就可以直接調(diào)用AIDL中的方法了.
AIDL中所支持的類型
- 基本數(shù)據(jù)類型
- String 和 CharSequence
- List: 只支持ArrayList, 里面每個(gè)元素都必須能被AIDL支持
- Map: 只支持HashMap, 里面的每個(gè)元素都必須被AIDL支持
- Parcelable: 所有實(shí)現(xiàn)了Parcelable接口的對(duì)象
- AIDL: 所有的AIDL接口本身也可以在AIDL文件中使用
這里請(qǐng)注意,上面支持類型中Parcelable和AIDL比較特殊,自定義的Parcelable對(duì)象和AIDL對(duì)象必須要顯示的import引入, 這是AIDL的規(guī)范需要遵循, 如下Book類
import com.szysky.note.androiddevseek_02.aidl.Book;//必須Book的全限定名
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book); //這里標(biāo)明輸入型
}
如果用到了自定義對(duì)象實(shí)現(xiàn)了Parcelable那么就需要?jiǎng)?chuàng)建一個(gè)同名的aidl文件
package com.szysky.note.androiddevseek_02.aidl;
parcelable Book;
AIDL中除了基本數(shù)據(jù)類型外,其他類型的參數(shù)
必須標(biāo)上方向out , in, inout.分別表示輸入,輸出,輸入輸出型. 按需而定可以節(jié)省不必要的操作在底層實(shí)現(xiàn)的開銷. 最后一點(diǎn)AIDL接口中只支持方法,不支持聲明靜態(tài)常量.
在服務(wù)端用了CopyOnWriteArrayList
數(shù)組來保存所有書籍. 這個(gè)集合的特性是支持并發(fā)讀寫. 在說Binder的時(shí)候提到過, AIDL方法是在服務(wù)端Binder線程池中執(zhí)行的, 所以當(dāng)多個(gè)客戶端同時(shí)連接,會(huì)存在多線程并發(fā)的問題. 所以使用CopyOnWriteArrayList
集合可以進(jìn)行自動(dòng)的線程同步.與之相似的還有ConcurrentHashMap
這個(gè)在LRU機(jī)制中使用到過
這里有知識(shí)點(diǎn). 之前說過AIDL中能過使用的只有ArrayList. 而CopyOnWriteArrayList
也并不是ArrayList的子類. 其實(shí)AIDL所支持的是抽象的List, 而List只是一個(gè)接口, 雖然服務(wù)端返回的是CopyOnWriteArrayList
,但是在Binder中會(huì)按照List的規(guī)范去訪問數(shù)據(jù)并最終形成一個(gè)新的ArrayList傳遞給客戶端.
看下面的log圖在客戶端接收返回的CopyOnWriteArrayList
實(shí)際上是ArrayList類型
git倉庫的代碼的aidl包中
最后保存的是實(shí)現(xiàn)了客戶端和服務(wù)端的觀察者模式(可以通過git版本切換之前代碼), 通過客戶端注冊(cè)監(jiān)聽接口,
在服務(wù)端每當(dāng)有新書來的時(shí)候,通知已經(jīng)注冊(cè)了的客戶端.
需要注意的幾點(diǎn)
線程問題
當(dāng)有新書的時(shí)候,服務(wù)端回調(diào)的是客戶端實(shí)現(xiàn)的接口里面的方法. 這個(gè)方法實(shí)際是在客戶端的線程池中執(zhí)行的. 所以要處理處理UI的問題, 解決方案可以創(chuàng)建一個(gè)Handler,將其切換到客戶端的主線程中
private INewBookArrivedListener mNewBookListener = new INewBookArrivedListener.Stub() {
@Override
public void onNewBookArrived(Book book) throws RemoteException {
// 如果有新書 那么此方法會(huì)被回調(diào), 并且由于調(diào)用處服務(wù)端的Binder線程池, 所以給主線程的Handler發(fā)送消息,以切換線程
mhandler.obtainMessage(NEW_BOOK_ARRIVED, book).sendToTarget();
}
};
對(duì)象不一致,導(dǎo)致接觸綁定失敗
服務(wù)端不能再用CopyOnWriteArrayList來記錄綁定過的客戶端. 因?yàn)檫@里一定要清楚對(duì)象是不能跨進(jìn)程的當(dāng)我們客戶端注冊(cè)監(jiān)聽傳入一個(gè)監(jiān)聽對(duì)象到服務(wù)端, 在解綁的時(shí)候再次傳入一個(gè)進(jìn)行判斷與注冊(cè)時(shí)相同的對(duì)象時(shí)刪除達(dá)到解除綁定效果時(shí)是無效的. 因?yàn)榉?wù)端在注冊(cè)和解綁的時(shí)候是兩個(gè)反序列化的對(duì)象完全不一致.
RemoteCallbackList
是系統(tǒng)專門提供的用于刪除跨進(jìn)程listener的接口. 接收的是一個(gè)泛型,支持管理任意的AIDL接口,從聲明就可以看出, 因?yàn)锳IDL接口都繼承IInterface
內(nèi)部實(shí)現(xiàn)是一個(gè)Map結(jié)構(gòu) key是IBinder
類型, value是Callback
類型.
ArrayMap<IBinder, Callback> mCallbacks= new ArrayMap<IBinder, Callback>();
IBinder key = listener.asBinder();
Callback value = new Callback(listener, cookie); //這里的Callback封裝了真正的監(jiān)聽對(duì)象
不管是注冊(cè)還是解注冊(cè),多進(jìn)程到服務(wù)端都會(huì)生成不同的對(duì)象. 但是這些不同的對(duì)象有一個(gè)共同點(diǎn), 底層的Binder對(duì)象是同一個(gè), 利用這個(gè)特性可解決上面的問題.
RemoteCallbackList
當(dāng)客戶端進(jìn)程終止后, 它能夠自動(dòng)移出客戶端所注冊(cè)的listener. 并且內(nèi)部實(shí)現(xiàn)了線程同步的功能, 所以在注冊(cè)和解注冊(cè)的時(shí)候不需要做額外的線程工作.
在使用的使用,雖然名字有List但是他并不是一個(gè)List我們要遍歷的通知監(jiān)聽者的時(shí)候,要使用bigenBroadcast和finishBroadcase成對(duì)出現(xiàn).
//遍歷集合 去調(diào)用客戶端方法
int N = mListeners.beginBroadcast();
for (int i = 0; i<N; i++){
INewBookArrivedListener listener = mListeners.getBroadcastItem(i);
if (listener != null){
listener.onNewBookArrived(newBook);
}
}
mListeners.finishBroadcast();
當(dāng)客戶端調(diào)用遠(yuǎn)程服務(wù)的方法,被調(diào)用的方法運(yùn)行在服務(wù)端的Binder線程池中,同時(shí)客戶端會(huì)被掛起, 所以你如果在主線程(客戶端的onServiceConnected
和onServiceDisconnected
就是UI線程)調(diào)用服務(wù)端的耗時(shí)方法, 你多點(diǎn)幾次就很容易出現(xiàn)ANR. 比方說在服務(wù)端的getBookList()
睡上十秒,可以復(fù)現(xiàn)ANR.
監(jiān)聽死亡狀態(tài): 在整理Binder的時(shí)候有說了一種DeathRecipient的方式,下兩種都可以
-
onServiceDisconnected()
UI線程被回調(diào) -
binderDied()
在客戶端的Binder線程池中被回調(diào)
還記得在綁定的時(shí)候bindService(intent,mConnection, Context.BIND_AUTO_CREATE);
其中參數(shù)3如果設(shè)置這個(gè)模式, 當(dāng)服務(wù)或線程死亡,還會(huì)重新啟動(dòng)的.
權(quán)限驗(yàn)證
- 在服務(wù)端的onBinder()回調(diào)中判斷權(quán)限.
- 在服務(wù)端實(shí)現(xiàn)的AIDL接口中的onTransact()進(jìn)行包名判斷或者權(quán)限
第一種:
先清單文件中注冊(cè)一個(gè)自定義的權(quán)限
<permission
android:name="com.szysky.permission.ACCESS_BOOK_SERVICE"
android:protectionLevel="normal"/>
在清單文件中添加這個(gè)權(quán)限的使用資格
<uses-permission android:name="com.szysky.permission.ACCESS_BOOK_SERVICE"/>
然后在onBinder()進(jìn)行判斷,如果沒有那么就返回null, 這樣客戶端是無法綁定服務(wù)的
public IBinder onBind(Intent intent) {
//做一下權(quán)限的驗(yàn)證 在清單文件中聲明了一個(gè), 并添加了使用權(quán)限
int check = checkCallingOrSelfPermission("com.szysky.permission.ACCESS_BOOK_SERVICE");
if (check == PackageManager.PERMISSION_DENIED){
return null;
}
return mBinder;
}
第二種
可以判斷客戶端的包名是否滿足我們的需求,這里用com.szysky開頭為例. 如果不符合方法返回false.那么調(diào)用服務(wù)的方法也會(huì)失效
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
String packageName = null;
String[] packagesForUid = getPackageManager().getPackagesForUid(getCallingUid());
//下面是獲得客戶端的包名
if (packagesForUid != null && packagesForUid.length >0){
packageName = packagesForUid[0];
}
Log.e(TAG, "onTransact: -----------------------------" + packageName);
if (!packageName.startsWith("com.szysky")){
return false;
}
return super.onTransact(code, data, reply, flags);
}
使用ContentProvider"
相關(guān)代碼在倉庫項(xiàng)目的java路徑下的provider包中
ContentProvider
是Android提供專門用于不用應(yīng)用進(jìn)行數(shù)據(jù)共享的方式. 它的底層同樣也是Binder. 因?yàn)橄到y(tǒng)封裝, 所以它的使用比起AIDL要簡(jiǎn)單很多.
要實(shí)現(xiàn)一個(gè)內(nèi)容提供者, 只需要寫一個(gè)類繼承ContentProvider,并復(fù)寫六個(gè)抽象方法. 其中有四個(gè)是CURD操作方法. 一個(gè)onCreate()用來做初始化. 一個(gè)getType()用來返回一個(gè)Uri請(qǐng)求所對(duì)應(yīng)的MIME類型,比如圖片還是視頻等. 如果我們不關(guān)心那么可是直接返回NULL
或者*/*
.
這六個(gè)方法根據(jù)Binder工作原理,都是運(yùn)行在ContentProvider的進(jìn)程中. 除了onCreate()是被系統(tǒng)回調(diào)運(yùn)行在主線程, 其余的都在Binder的線程池中.
主要存儲(chǔ)方式是表格的形式, 也可以支持文件格式,例如圖片視頻, 可以返回這類文件的句柄給外界來訪問ContentProvider中的文件信息.
<provider
android:authorities="com.szysky.note.androiddevseek_02.provider"
android:name=".provider.BookProvider"
android:permission="com.szysky.PROVIDER"/>
-
authorities
: 后面的值是指定這個(gè)ContentProvider的唯一標(biāo)識(shí). -
permission
: 添加一個(gè)權(quán)限認(rèn)證, 對(duì)于訪問者必須添加了這個(gè)使用權(quán)限的聲明.
查詢的時(shí)候通過Uri
對(duì)authorities
聲明值得解析就可以找到對(duì)應(yīng)的ContentProvider
Uri uri = Uri.parse("content://com.szysky.note.androiddevseek_02.provider");
getContentResolver().query(uri, null, null, null, null);
為了后續(xù)操作, 這里利用SQLiteOpenHelper來管理數(shù)據(jù)庫,并創(chuàng)建兩個(gè)表user和book,代碼在倉庫有,這里不寫實(shí)現(xiàn)過程.
由于有兩個(gè)表支持被訪問, 所以應(yīng)該為每一個(gè)不同的表設(shè)定單獨(dú)的Uri和Uri_Code 并將其關(guān)聯(lián). 這樣外界訪問的時(shí)候可以根據(jù)Uri得到Uri_Code. 也就在ContentProvider知道要處理的具體事件.
在新建的ContentProvider類中進(jìn)行關(guān)聯(lián), 如下
private static final String AUTHORITY = "com.szysky.note.androiddevseek_02.provider";
/**
* 指定兩個(gè)操作的Uri
*/
private static final Uri BOOK_CONTENT_URI = Uri.parse("content://" +AUTHORITY + "/book");
private static final Uri USER_CONTENT_URI = Uri.parse("content://" +AUTHORITY + "/user");
/**
* 創(chuàng)建Uri對(duì)應(yīng)的Uri_Code
*/
private static final int BOOK_URI_CODE = 1;
private static final int USER_URI_CODE = 2;
/**
* 創(chuàng)建一個(gè)管理Uri和Uri_Code的對(duì)象
*/
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
//進(jìn)行關(guān)聯(lián)
sUriMatcher.addURI(AUTHORITY, "book",BOOK_URI_CODE);
sUriMatcher.addURI(AUTHORITY, "user",USER_URI_CODE);
}
針對(duì)query方法進(jìn)行演示,其他三個(gè)類似,代碼有全部實(shí)現(xiàn)的例子, 在自定義Provider文件中.
/**
* 通過自動(dòng)以的Uri來判斷對(duì)應(yīng)的數(shù)據(jù)庫表名
*/
private String getTableName(Uri uri){
String tableName = null;
switch (sUriMatcher.match(uri)){
case BOOK_URI_CODE:
tableName = DbHelper.BOOK_TABLE_NAME;
break;
case USER_URI_CODE:
tableName = DbHelper.USER_TABLE_NAME;
break;
default:break;
}
return tableName;
}
/**
* 在query中, 獲取到Uri傳入要查詢的具體表名, 使用SQLiteOpenHelper來進(jìn)行query的查詢,并把結(jié)果返回
*/
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
//獲取表名
String tableName = getTableName(uri);
if (tableName == null)
throw new IllegalArgumentException("不被支持的Uri參數(shù)-->"+uri );
return mDb.query(tableName, projection, selection, selectionArgs, null, null, sortOrder,null);
}
如果需要監(jiān)聽Provider內(nèi)容的變化, 那么可以在Provider中update
, delete
, insert
. 中操作完數(shù)據(jù)庫之后. 使用getContentResolver().notifyChange(uri,null);
來通知外界當(dāng)前ContentProvider中數(shù)據(jù)已經(jīng)發(fā)生改變. 而外部要想觀察其變化. 使用ContentResolver
的rigisterContentObserver
方法來注冊(cè)觀察者.
線程安全問題
如果只有一個(gè)SQLiteDataBase對(duì)象被使用, 那么增刪改查不會(huì)出現(xiàn)線程安全問題, 因?yàn)槠鋬?nèi)部對(duì)數(shù)據(jù)庫的操作是有同步處理. 但是如果多個(gè)SQLiteDataBase對(duì)象來操作數(shù)據(jù)庫就無法保證其線程安全. 這個(gè)時(shí)候就要注意了.
使用Socket
Socket也稱為套接字. 是網(wǎng)絡(luò)通信中的概念, 它分為流式套接字和用戶數(shù)據(jù)包套接字兩種. 分別對(duì)應(yīng)于網(wǎng)絡(luò)的傳輸控制層中TCP和UDP協(xié)議.
使用Socket通信, 需要在清單文件添加權(quán)限的申請(qǐng)
<!--Socket通信額外需要的線程-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
服務(wù)端 需要啟動(dòng)服務(wù), 并且在線程中建立TCP
服務(wù), 然后監(jiān)聽一個(gè)端口. 當(dāng)客戶端建立連接的時(shí)候就會(huì)生成一個(gè)Socket
流. 可以保持后續(xù)的持續(xù)通信. 每一個(gè)客戶端都對(duì)應(yīng)一個(gè)Socket. 如果客戶端斷開連接. 服務(wù)端需要做好相應(yīng)的Socket流關(guān)閉并結(jié)束通話線程. 可以通過在客戶端斷開的時(shí)候服務(wù)端的接收字節(jié)流會(huì)是null來判斷連接是否還存活.
首先需要開啟一個(gè)新線程, Runnable
接口這樣實(shí)現(xiàn), 以下是偽代碼, 沒有捕捉異常
// 監(jiān)聽 3333 端口
ServerSocket serverSocket = new ServerSocket(3333);
// 判斷服務(wù)是否斷開 沒有斷開就繼續(xù)監(jiān)聽端口
while (!mIsServiceDestoryed) {
//這個(gè)是阻塞方法, 當(dāng)有新的客戶端連接,才會(huì)返回Socket值
final Socket accept = serverSocket.accept();
// 有了新的客戶端 那就需要?jiǎng)?chuàng)建一個(gè)新的線程去維護(hù)
new Thread() {
public void run() {
// 這里做對(duì)一個(gè)Socket的具體操作
responseClient(accept);
}}.start();
}
private void responseClient(Socket client) throws IOException {
//接收客戶端消息
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
//發(fā)送到客戶端 , 設(shè)置true參數(shù)就不需要手動(dòng)的刷新輸出流
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(client.getOutputStream())),true);
out.println("歡迎來到直播間");
//判斷服務(wù)標(biāo)志是否銷毀, 沒有銷毀那么就一直監(jiān)聽此鏈接的Socket流
while (!mIsServiceDestoryed) {
String str = in.readLine(); //這是一個(gè)阻塞方法
//判斷如果取出來的是null,那么就說明連接已經(jīng)斷開
if (str == null)
break;
// 對(duì)客戶端進(jìn)行回復(fù)
out.println("我是回復(fù)消息");
}
//準(zhǔn)備關(guān)閉一系列的流
......
}
最后就是要在onDestroy()
中把循環(huán)中判斷服務(wù)存活的標(biāo)識(shí)置為false
, 讓開啟的線程都能自動(dòng)走完關(guān)閉.
客戶端
在onCreate()
先startService開啟TCP的服務(wù), 然后開啟一個(gè)線程準(zhǔn)備連接Socket. 可以加上失敗重連的的機(jī)制. 只要獲取到了Socket, 就和之前服務(wù)端一樣獲取輸入輸出流進(jìn)行對(duì)應(yīng)的操作.
貼出客戶端的核心代碼, Runnable接口實(shí)現(xiàn)的 ,同樣是偽代碼
Socket socket = null;
// 試圖連接服務(wù)器, 如果失敗休眠一秒重試
while(socket == null){
try {
// 如果可以連接 3333 端口成功那么socket就不為null, 此循環(huán)也就結(jié)束
socket = new Socket("localhost", 3333);
mClientSocket = socket;
// 獲得輸出流, 并設(shè)置true,自動(dòng)刷新輸出流里面的內(nèi)容
mPrintWrite = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mClientSocket.getOutputStream())),true );
} catch (IOException e) {
SystemClock.sleep(1000);
e.printStackTrace();
}
}
//準(zhǔn)備接收服務(wù)器的消息.
BufferedReader in = new BufferedReader(new InputStreamReader(mClientSocket.getInputStream()));
//獲得了socket流的讀入段 只要activity不關(guān)閉一直循環(huán)讀
while(!SocketActivity.this.isFinishing()){
// readLine()同樣也是阻塞方法
String strLine = in.readLine();
if (strLine != null){
//TODO 獲取到了服務(wù)端發(fā)來的數(shù)據(jù), 做一些事情
//....
}
}
//走到這里 說明界面已經(jīng)不存在, 進(jìn)行掃尾動(dòng)作
in.close();
mPrintWrite.close();
socket.close();
demo圖例Socket相關(guān)代碼存在倉庫的socket包中
各種IPC的差異以及選擇
名稱 | 優(yōu)點(diǎn) | 缺點(diǎn) | 使用場(chǎng)景 |
---|---|---|---|
Bundle |
簡(jiǎn)單易用 | 只能傳輸Bundle支持的數(shù)據(jù)類型 | 四大組件間的進(jìn)程間通信 |
文件共享 |
簡(jiǎn)單易用 | 不適合高并發(fā)場(chǎng)景,并且無法做到進(jìn)程間的即時(shí)通 | 無法并發(fā)訪問情形, 交換簡(jiǎn)單的數(shù)據(jù)實(shí)時(shí)性不高的場(chǎng)景 |
AIDL |
功能強(qiáng)大 | 使用稍復(fù)雜,需要處理好線程同步 | 一對(duì)多通信且有RPC需求 |
ContentProvider |
在數(shù)據(jù)源訪問方面功能強(qiáng)大,支持一對(duì)多并發(fā)數(shù)據(jù)共享 | 可以理解為受約束的AIDL,主要提供數(shù)據(jù)源的CRUD操作 | 一對(duì)多的進(jìn)程間的數(shù)據(jù)共享 |
Messenger |
功能一般, 支持一對(duì)多串行通信,支持實(shí)時(shí)通信 | 不能很好處理高并發(fā),不支持RPC,數(shù)據(jù)通過Message進(jìn)行傳輸, 因此只能傳輸Bundle支持的數(shù)據(jù)類型 | 低并發(fā)的一對(duì)多即時(shí)通信,無RPC需求,或者無需要返回結(jié)果的RPC需求 |
Socket |
功能強(qiáng)大,可以通過網(wǎng)絡(luò)傳輸字節(jié)流,支持一對(duì)多并發(fā)實(shí)時(shí)通信 | 實(shí)現(xiàn)細(xì)節(jié)稍微有點(diǎn)繁瑣,不支持直接的 | 網(wǎng)絡(luò)數(shù)據(jù)交換 |
參看文章
《Android 開發(fā)藝術(shù)探索》書集
《Android 開發(fā)藝術(shù)探索》 02-IPC機(jī)制
https://github.com/feiwodev/AndroidDevelopmentArt