
Android IPC簡介
IPC是Inter-Process Communication縮寫,含義為進程間通信. 按照操作系統(tǒng)中的描述,線程是cpu調(diào)度的最小單元,而進程一般指一個執(zhí)行單元. 進程中可以有一個或者多個線程.
不同的操作系統(tǒng)有著不同的IPC機制:
- Windows: 通過剪切板, 管道, 信號量來進行進程間通信
- Linux: 通過命名管道, 共享內(nèi)存, 信號量等來進行進行進程間通信
- android: 雖然基于Linux內(nèi)核,但是使用了獨有的Binder機制, 也可以Socket進行通信
使用場景: 可能有些模塊因為特殊原因需要運行在單獨的進程中; 或者為了加大一個應(yīng)用可使用的內(nèi)存; 又或者我們需要去另外一個進程去獲取數(shù)據(jù),必然需要跨進程.
Android中的多進程的模式
開啟多進程模式
如果你想在一個應(yīng)用中使用多個進程,通過清單文件給四大組件添加android:process屬性,就可以很方便的開啟多進程.
還有一種非常規(guī)的創(chuàng)建方式,通過JNI在native層去fork一個新的進程.這種只做了解.

例如這樣,當(dāng)我們依次打開MainActivity, SecondActivity, ThirdActivity.此時應(yīng)該打開了三個進程.
我們來檢測一下, 你可以直接使用DDMS來查看進程,這里使用命令行來測試
$ adb shell ps | grep com.szysky 你可以直接使用adb shell ps這會把系統(tǒng)所有進程展示出來, 你可以加上過濾信息| grep xxx xxx替換你需要過濾出來信息即可

你可能已經(jīng)發(fā)現(xiàn)在創(chuàng)建新進程的時候使用兩種不同的方式
- 當(dāng)以
:開頭的進程,屬于當(dāng)前應(yīng)用的私有進程,其他應(yīng)用的組件不可以和它跑在同一個進程 - 當(dāng)不以
:開頭,那么進程屬于全局進程,其他應(yīng)用通過ShareUID方法可以和它跑在同一個進程
Android系統(tǒng)會為每一個應(yīng)用分配唯一的UID. 相同UID的應(yīng)用才能共享數(shù)據(jù). 但是兩個應(yīng)用通過ShareUID跑在同一個進程是有要求的. 除了具有相同的ShareUID并且還要簽名相同才可以. 這時如果不在同一進程他們之間可以共享data目錄,組件信息等. 如果還在同一進程, 那么他們還能共享內(nèi)存數(shù)據(jù).
進程模式的運行機制
開啟多進程簡單,但是如果不能處理好其中的特性,那么受傷的總會是你.
先說第一個比較嚴(yán)重的問題. 靜態(tài)變量不在共享. 還是直接三個類的例子,如果在mainActivity中對靜態(tài)變量進行修改, 在SecondActivity取出這個靜態(tài)發(fā)現(xiàn)是main沒修改之前的. 這說明兩個進程間即使是靜態(tài)屬性也是無法共享.
其實這是因為這兩個類運行在兩個進程間,而每個單獨的進程又會分配一個獨立的虛擬機, 所以每個虛擬機在內(nèi)存分配上有不同的地址空間.對于不同虛擬機訪問同一個對象就會產(chǎn)生多份副本. 副本之間互相獨立不干擾彼此.
一般情況下多進程可能面臨的問題:
- 靜態(tài)成員和單例模式完全失效
- 線程同步機制完全失效
- SharedPreferences的可靠性下降
- Application會多次創(chuàng)建
2中因為不是一塊內(nèi)存,所以不管是鎖對象還是鎖全局都無法保證線程同步,因為不是同一個對象. 3中因為Sp不支持兩個進程同時讀寫,因為底層是通過讀寫XML文件實現(xiàn)的,并發(fā)可能會觸發(fā)異常. 4中運行在多個進程中,那么就會創(chuàng)建多個虛擬機,每個虛擬機都有一個對應(yīng)Application并需要啟動加載這個文件.
一個應(yīng)用的多進程:它就相當(dāng)于兩個不同的應(yīng)用采用了ShareUID的模式. 每個進程都會擁有獨立的虛擬機, Application以及內(nèi)存空間
IPC基礎(chǔ)概念
關(guān)于IPC主要包含三方面的內(nèi)容: Serializable接口, Parcelable接口, 以及Binder
Serializable接口
Serializable是Java提供的一個序列化接口,這個一個空接口. 如果我們想使用只需要實現(xiàn)Serializable接口,并聲明一個long類型的常量serialVersionUID(不聲明也是可以,但是在反序列化會出現(xiàn)錯誤).
javabean 的實現(xiàn)
public class Student implements Serializable{
public static final long serialVersionUID = 123456789L;
//.....省略創(chuàng)建屬性,打印等操作
}
序列化的代碼如下圖,并附上結(jié)果.

好了說一下serialVersionUID這個屬性. 即使我們不聲明系統(tǒng)會根據(jù)當(dāng)前類結(jié)構(gòu)(成員變量等)生成一個hash為serialVersionUID, 雖然這樣也可以但是如果在你把一個對象序列化的到磁盤的一個文件的時候. 對這個對象增加了一個成員變量,那么在反序列的時候就會報錯. 因為當(dāng)你反序列化的時候?qū)ο笕绻麤]有serialVersionUID還會重新計算.這時反序列化的hash和序列化的hash就不一致了.
關(guān)于根據(jù)當(dāng)前類結(jié)構(gòu)計算hash值,有兩點需要注意:
- 靜態(tài)成員變量屬于類不屬于對象,所以不參與序列化的過程
- 其次用
transient關(guān)鍵字標(biāo)記的成員變量不參與序列化的過程.
系統(tǒng)默認(rèn)的序列化過程是可以改變的,通過實現(xiàn)writeObject和readObject可以重寫默認(rèn)的序列化和反序列化過程. 這里就不詳細(xì)說明
Parcelable接口
系統(tǒng)已經(jīng)為我們提供了很多實現(xiàn)了Parcelable接口的類,他們都可以直接序列化. 例如intent, Bundle, Bitmap, 同時List和Map也可以序列化.前提是他們里面的每個元素都可以序列化.
實現(xiàn)Parcelable接口主要復(fù)寫四個, 我們可以直接定義好javabean直接讓AS幫我們實現(xiàn).
- writeToParcel() 主要完成序列化功能
- CREATOR 主要完成反序列化
- 接收參數(shù)parcel的構(gòu)造函數(shù) 用于從序列化后的對象中創(chuàng)建原始對象
- describeContents() 幾乎所有情況下都返回0,只有當(dāng)前對象中存在文件描述符時返回1
關(guān)于Parcelable和Serializable的取舍
- Serializable: 適合序列化到設(shè)備或者序列化后通過網(wǎng)絡(luò)傳輸.
- Parcelable: 主要用在內(nèi)存序列化上. 不需要大量的I/O操作,所以在內(nèi)存中使用高效.
了解Binder[]

- 代碼層面: Binder是Android中的一個類,它實現(xiàn)了IBinder接口
- IPC角度: Binder是Android中的一種跨進程通信方式.
- 物理設(shè)備角度: Binder也可以認(rèn)為是一種虛擬的物理設(shè)備,設(shè)備驅(qū)動是/dev/binder
- Framework角度: Binder是ServiceManager連接各種Manager和相應(yīng)的ManagerService的橋梁.
- Android應(yīng)用角度:Binder是客戶端和服務(wù)端進行通信的媒介.
日常開發(fā)中,Binder主要用在Service包括AIDL和Messenger. 而普通的Service中的Binder不涉及進程間的通信,無法觸及Binder的核心. 而Messenger底層其實就是AIDL.所以我們利用AIDL來分析Binder的工作機制
創(chuàng)建AIDL實例
新建三個文件 Book.java Book.aidl 和IBookManager.aidl
首先創(chuàng)建一個Book類并實現(xiàn)Parcelable接口,然后在這個類所在的包上右鍵,如圖所示

如果名字不能為Book,可以先隨便寫一個,創(chuàng)建之后修改. 然后按圖修改,

文件聲明完,我們只需要重新Make一下工程就可以.Build --> Rebuild Project或者Make Project

Make之后會在app -> build -> generated -> source -> aidl -> debug -> … 出現(xiàn)系統(tǒng)自動生成好的java類. 我們需要對其進行分析
看下圖了解一個大體結(jié)構(gòu)

上圖圈出了兩個部分,部分二應(yīng)該很清楚就是我們定義在aidl中的兩個抽象方法. 而部分一在圖上的內(nèi)部類Stub寫了說明. 我們自定義的兩個抽象方法,在內(nèi)部類中用了兩個整形int值來標(biāo)識兩個抽象方法,用在transact()中可以識別客戶端請求哪個方法.
這個繼承了IInterface的接口的核心實現(xiàn):就是內(nèi)部類Stub和Stub的內(nèi)部代理Proxy
先看一下內(nèi)部類的結(jié)構(gòu)圖:

說明直接放圖,寫在代碼上,在git上的根目錄的aidl的java類說明文件夾也有添加了注釋的類.

有兩點需要注意:
- 客戶端發(fā)起遠(yuǎn)程請求時,當(dāng)前線程會被掛起直到服務(wù)器進程返回數(shù)據(jù),所以注意線程是否在意耗時
- 由于服務(wù)端Binder方法運行在Binder線程池中,所以不管Binder方法是否耗時都應(yīng)該采用同步方式,因為已經(jīng)在一個線程中了
我們也可以手動實現(xiàn)Binder類,這里不再細(xì)說,在git倉庫的項目中有一個manual包里面是關(guān)于手動實現(xiàn)Binder的代碼.
其實 不管是手動實現(xiàn)Binder也好,或者AIDL文件實現(xiàn)Binder也好. 其實兩者的工作原理都是一樣的, AIDL文件的存在意義是系統(tǒng)為我們提供了一種快速實現(xiàn)Binder的工具,僅此而已.
Binder生命狀態(tài)的監(jiān)聽
由于Binder是運行在服務(wù)端,如果服務(wù)端進程異常終止,那么我們到服務(wù)端的Binder連接也就斷裂(Binder死亡).就會導(dǎo)致調(diào)用失敗,所里系統(tǒng)提供了死亡代理的方法 就是當(dāng)Binder死亡時,我們就會收到通知,這個時候就可以重新發(fā)起連接請求而恢復(fù)連接
首先創(chuàng)建監(jiān)聽的DeathRecipient對象
IBinder.DeathRecipient mDeat = new IBinder.DeathRecipient() {
// 當(dāng)Binder死亡的時候,系統(tǒng)會回調(diào)binderDied()方法
@Override
public void binderDied() {
if (mBookManager == null)
return ;
//清除掉已經(jīng)無用的Binder連接
mBookManager.asBinder().unlinkToDeath(mDeat,0);
mBookManager == null;
//TODO 進行重新綁定遠(yuǎn)程服務(wù)
}
};
當(dāng)客戶端綁定遠(yuǎn)程服務(wù)成功的時候,給binder設(shè)置死亡代理
binder.linkToDeath(mDeat,0);
linkToDeath的第二個參數(shù)是個標(biāo)志位,直接設(shè)0即可. 另外也可以通過Binder的方法isBindAlive也可以判斷Binder是否死亡.
Android的幾種跨進程的方式
使用Bundle
由于Bundle實現(xiàn)了Parcelable接口,所以在四大組件中的三大組件(Activity, Service, Receiver)都支持在Intent中傳遞Bundle.
所以如果在一個進程中啟動了另一個進程的三大組件,就可以在Bundle中附加我們需要的信息通過Intent發(fā)送出去. 當(dāng)然傳遞的類型必須是能夠被序列化的, 例如基本數(shù)據(jù)類型,實現(xiàn)了Parcelable和Serializable接口的對象和一些Android支持的特殊對象.
使用文件共享
文件共享適合在對數(shù)據(jù)同步要求不高的進程之間進行通信,并且要妥善的處理并發(fā)讀寫的問題.
兩個進程通過讀/寫同一個文件來交換數(shù)據(jù). 例如進程A把數(shù)據(jù)寫入文件中,而進程B從文件中讀取出來數(shù)據(jù).
Android是基于Linux系統(tǒng), 所以對于并發(fā)讀寫文件可以沒有限制的執(zhí)行. 這里不像Windows系統(tǒng),對于一個文件如果加了排斥鎖將會導(dǎo)致其他線程無法對其進行訪問.
關(guān)于這部分的練習(xí), 在之前練習(xí)Binder的時候已經(jīng)練習(xí)過了. 代碼在項目中的MainActivity中
雖然序列化反序列達(dá)到的效果是可以恢復(fù)對象里面的屬性值,但是反序列每回都是一個新的對象.
SharePreferencess是Android提供的一個輕量級方案,通過鍵值對存儲數(shù)據(jù),底層采用XML文件來進行存儲. 存儲路徑/data/data/package name/shared_prefs目錄下. 也屬于文件的一種,但是由于系統(tǒng)對SP的讀寫存在一定的緩存策略,內(nèi)存中會有一份緩存,所以多進程下,系統(tǒng)對它的讀寫也就變得不可靠.
使用Messenger
Messenger(信使). 不同的進程中可以傳遞Message對象, 在Message中放入我們需要傳遞的數(shù)據(jù),就可實現(xiàn)進程間傳遞. Messenger是一種輕量級的IPC方案,它的底層實現(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的痕跡.
因為Messenger對AIDL進行了封裝,使得在使用時更加簡單,并且它的處理方式是一次處理一個請求,因此服務(wù)器端不用考慮線程同步因為服務(wù)端不存在并發(fā)執(zhí)行的情形.
具體實現(xiàn)Messenger
1.服務(wù)端
創(chuàng)建一個Service作為服務(wù)端來處理客戶端的請求, 同時創(chuàng)建一個Handle并通過它來創(chuàng)建一個Messenger對象,然后在Service的onBind()方法中返回這個Messenger對象底層的Binder.
最后這個組件在清單文件中聲明加上android:process="com.szysky.test"屬性.已達(dá)到模擬多進程的場景
public class MessengerService extends Service {
/**
* 編寫一個類繼承Handler,并對客戶端發(fā)來的消息進行處理操作進行添加
*/
private static class MessengerHandler extends Handler{
private static final String TAG = "MessengerHandler";
@Override
public void handleMessage(Message msg) {
switch (msg.what){
//客戶端發(fā)來的信息標(biāo)識
case MessengerActivity.FROM_CLIENT:
Log.d(TAG, "handleMessage: receive msg form clinet-->" +msg.getData().getString("msg"));
//對客戶端進行reply回答
// 1\. 通過接收到的到客戶端的Message對象獲取到Messenger信使
Messenger client = msg.replyTo;
// 2\. 創(chuàng)建一個信息Message對象,并把一些數(shù)據(jù)加入到這個對象中
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)建一個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.客戶端
直接用一個activity作為客戶端, 首先綁定之前創(chuàng)建的服務(wù)端的Service, 綁定成功時通過ServiceConnection對象接收到服務(wù)端返回的IBinder,用IBinder對象創(chuàng)建一個Messenger. 通過這個Messenger就可以往服務(wù)端發(fā)送消息. 如果我們需要服務(wù)端也能夠回應(yīng)客戶端. 那么就要在客戶端同之前服務(wù)端一樣通過Handle創(chuàng)建一個Messenger對象, 并把這個Messenger在通過連接成功返回的IBinder創(chuàng)建的Message對象通過replyTo參數(shù)傳遞給服務(wù)器. 這個服務(wù)器就可以通過replyTo參數(shù)來回應(yīng)客戶端.
/**
* 聲明一個本進程的信使 用來監(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)建一個服務(wù)監(jiān)聽連接對象 并在成功的時候給服務(wù)器發(fā)送一條消息
*/
private ServiceConnection mConnection = new ServiceConnection() {
//綁定成功回調(diào)
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 利用服務(wù)端返回的binder對象創(chuàng)建Messenger并使用此對象想服務(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) {
}
};
//進行遠(yuǎn)端服務(wù)的連接
Intent intent = new Intent(MessengerActivity.this, MessengerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
在使用Messenger進行數(shù)據(jù)傳遞必須將數(shù)據(jù)放入到Message中. 而Messenger和Message都實現(xiàn)了序列化接口. 所以可以在進程間通信.
Message的能使用的載體只有what, arg1, arg2, Bundle 以及replyTo. 這里有一個載體需要注意object,它在同一個進程很實用,但是在版本2.2之前是不支持跨進程的,雖然進行了改進之后,但是也只是支持系統(tǒng)提供實現(xiàn)的某些對象才可以. 所以使用的時候需要注意.
順便繪制了一個Messenger通信的流程圖, 可以對代碼的調(diào)用順序理解的更清楚.

使用AIDL
雖然Messenger使用方便, 但是要清楚它是以串行的方式處理客戶端發(fā)來的消息,如果有大量并發(fā)的請求. 或者需求是跨進程調(diào)用服務(wù)端的方法時. 就無法使用Messenger. 這個時候就該AIDL
對于使用AIDL的流程簡單梳理一遍
服務(wù)端
服務(wù)端創(chuàng)建一個Service用來監(jiān)聽客戶端的連接請求, 然后創(chuàng)建一個AIDL文件,將暴露給客戶端的接口在這個AIDL文件中聲明,最后在Service中實現(xiàn)這個AIDL接口并在onBind()返回即可.
客戶端
綁定服務(wù)端的Service,綁定成功后,將服務(wù)端返回來的Binder對象轉(zhuǎn)成AIDL接口所屬的類型,接著就可以直接調(diào)用AIDL中的方法了.
AIDL中所支持的類型
- 基本數(shù)據(jù)類型
- String 和 CharSequence
- List: 只支持ArrayList, 里面每個元素都必須能被AIDL支持
- Map: 只支持HashMap, 里面的每個元素都必須被AIDL支持
- Parcelable: 所有實現(xiàn)了Parcelable接口的對象
- AIDL: 所有的AIDL接口本身也可以在AIDL文件中使用
這里請注意,上面支持類型中Parcelable和AIDL比較特殊,自定義的Parcelable對象和AIDL對象必須要顯示的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)明輸入型
}
如果用到了自定義對象實現(xiàn)了Parcelable那么就需要創(chuàng)建一個同名的aidl文件
package com.szysky.note.androiddevseek_02.aidl;
parcelable Book;
AIDL中除了基本數(shù)據(jù)類型外,其他類型的參數(shù)必須標(biāo)上方向out , in, inout.分別表示輸入,輸出,輸入輸出型. 按需而定可以節(jié)省不必要的操作在底層實現(xiàn)的開銷. 最后一點AIDL接口中只支持方法,不支持聲明靜態(tài)常量.
在服務(wù)端用了CopyOnWriteArrayList數(shù)組來保存所有書籍. 這個集合的特性是支持并發(fā)讀寫. 在說Binder的時候提到過, AIDL方法是在服務(wù)端Binder線程池中執(zhí)行的, 所以當(dāng)多個客戶端同時連接,會存在多線程并發(fā)的問題. 所以使用CopyOnWriteArrayList集合可以進行自動的線程同步.與之相似的還有ConcurrentHashMap這個在LRU機制中使用到過
這里有知識點. 之前說過AIDL中能過使用的只有ArrayList. 而CopyOnWriteArrayList也并不是ArrayList的子類. 其實AIDL所支持的是抽象的List, 而List只是一個接口, 雖然服務(wù)端返回的是CopyOnWriteArrayList,但是在Binder中會按照List的規(guī)范去訪問數(shù)據(jù)并最終形成一個新的ArrayList傳遞給客戶端.
看下面的log圖在客戶端接收返回的CopyOnWriteArrayList實際上是ArrayList類型

git倉庫的代碼的aidl包中 最后保存的是實現(xiàn)了客戶端和服務(wù)端的觀察者模式(可以通過git版本切換之前代碼), 通過客戶端注冊監(jiān)聽接口,
在服務(wù)端每當(dāng)有新書來的時候,通知已經(jīng)注冊了的客戶端.
需要注意的幾點
線程問題
當(dāng)有新書的時候,服務(wù)端回調(diào)的是客戶端實現(xiàn)的接口里面的方法. 這個方法實際是在客戶端的線程池中執(zhí)行的. 所以要處理處理UI的問題, 解決方案可以創(chuàng)建一個Handler,將其切換到客戶端的主線程中
private INewBookArrivedListener mNewBookListener = new INewBookArrivedListener.Stub() {
@Override
public void onNewBookArrived(Book book) throws RemoteException {
// 如果有新書 那么此方法會被回調(diào), 并且由于調(diào)用處服務(wù)端的Binder線程池, 所以給主線程的Handler發(fā)送消息,以切換線程
mhandler.obtainMessage(NEW_BOOK_ARRIVED, book).sendToTarget();
}
};
對象不一致,導(dǎo)致接觸綁定失敗
服務(wù)端不能再用CopyOnWriteArrayList來記錄綁定過的客戶端. 因為這里一定要清楚對象是不能跨進程的當(dāng)我們客戶端注冊監(jiān)聽傳入一個監(jiān)聽對象到服務(wù)端, 在解綁的時候再次傳入一個進行判斷與注冊時相同的對象時刪除達(dá)到解除綁定效果時是無效的. 因為服務(wù)端在注冊和解綁的時候是兩個反序列化的對象完全不一致.
RemoteCallbackList是系統(tǒng)專門提供的用于刪除跨進程listener的接口. 接收的是一個泛型,支持管理任意的AIDL接口,從聲明就可以看出, 因為AIDL接口都繼承IInterface
內(nèi)部實現(xiàn)是一個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)聽對象
不管是注冊還是解注冊,多進程到服務(wù)端都會生成不同的對象. 但是這些不同的對象有一個共同點, 底層的Binder對象是同一個, 利用這個特性可解決上面的問題.
RemoteCallbackList 當(dāng)客戶端進程終止后, 它能夠自動移出客戶端所注冊的listener. 并且內(nèi)部實現(xiàn)了線程同步的功能, 所以在注冊和解注冊的時候不需要做額外的線程工作.
在使用的使用,雖然名字有List但是他并不是一個List我們要遍歷的通知監(jiān)聽者的時候,要使用bigenBroadcast和finishBroadcase成對出現(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)用的方法運行在服務(wù)端的Binder線程池中,同時客戶端會被掛起, 所以你如果在主線程(客戶端的onServiceConnected和onServiceDisconnected就是UI線程)調(diào)用服務(wù)端的耗時方法, 你多點幾次就很容易出現(xiàn)ANR. 比方說在服務(wù)端的getBookList()睡上十秒,可以復(fù)現(xiàn)ANR.
監(jiān)聽死亡狀態(tài): 在整理Binder的時候有說了一種DeathRecipient的方式,下兩種都可以
-
onServiceDisconnected()UI線程被回調(diào) -
binderDied()在客戶端的Binder線程池中被回調(diào)
還記得在綁定的時候bindService(intent,mConnection, Context.BIND_AUTO_CREATE);其中參數(shù)3如果設(shè)置這個模式, 當(dāng)服務(wù)或線程死亡,還會重新啟動的.
權(quán)限驗證
- 在服務(wù)端的onBinder()回調(diào)中判斷權(quán)限.
- 在服務(wù)端實現(xiàn)的AIDL接口中的onTransact()進行包名判斷或者權(quán)限
第一種:
先清單文件中注冊一個自定義的權(quán)限
<permission
android:name="com.szysky.permission.ACCESS_BOOK_SERVICE"
android:protectionLevel="normal"/>
在清單文件中添加這個權(quán)限的使用資格
<uses-permission android:name="com.szysky.permission.ACCESS_BOOK_SERVICE"/>
然后在onBinder()進行判斷,如果沒有那么就返回null, 這樣客戶端是無法綁定服務(wù)的
public IBinder onBind(Intent intent) {
//做一下權(quán)限的驗證 在清單文件中聲明了一個, 并添加了使用權(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ù)的方法也會失效
@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)代碼在倉庫項目的java路徑下的provider包中
ContentProvider是Android提供專門用于不用應(yīng)用進行數(shù)據(jù)共享的方式. 它的底層同樣也是Binder. 因為系統(tǒng)封裝, 所以它的使用比起AIDL要簡單很多.
要實現(xiàn)一個內(nèi)容提供者, 只需要寫一個類繼承ContentProvider,并復(fù)寫六個抽象方法. 其中有四個是CURD操作方法. 一個onCreate()用來做初始化. 一個getType()用來返回一個Uri請求所對應(yīng)的MIME類型,比如圖片還是視頻等. 如果我們不關(guān)心那么可是直接返回NULL或者*/*.
這六個方法根據(jù)Binder工作原理,都是運行在ContentProvider的進程中. 除了onCreate()是被系統(tǒng)回調(diào)運行在主線程, 其余的都在Binder的線程池中.
主要存儲方式是表格的形式, 也可以支持文件格式,例如圖片視頻, 可以返回這類文件的句柄給外界來訪問ContentProvider中的文件信息.
<provider
android:authorities="com.szysky.note.androiddevseek_02.provider"
android:name=".provider.BookProvider"
android:permission="com.szysky.PROVIDER"/>
-
authorities: 后面的值是指定這個ContentProvider的唯一標(biāo)識. -
permission: 添加一個權(quán)限認(rèn)證, 對于訪問者必須添加了這個使用權(quán)限的聲明.
查詢的時候通過Uri對authorities聲明值得解析就可以找到對應(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)建兩個表user和book,代碼在倉庫有,這里不寫實現(xiàn)過程.
由于有兩個表支持被訪問, 所以應(yīng)該為每一個不同的表設(shè)定單獨的Uri和Uri_Code 并將其關(guān)聯(lián). 這樣外界訪問的時候可以根據(jù)Uri得到Uri_Code. 也就在ContentProvider知道要處理的具體事件.
在新建的ContentProvider類中進行關(guān)聯(lián), 如下
private static final String AUTHORITY = "com.szysky.note.androiddevseek_02.provider";
/**
* 指定兩個操作的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對應(yīng)的Uri_Code
*/
private static final int BOOK_URI_CODE = 1;
private static final int USER_URI_CODE = 2;
/**
* 創(chuàng)建一個管理Uri和Uri_Code的對象
*/
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
//進行關(guān)聯(lián)
sUriMatcher.addURI(AUTHORITY, "book",BOOK_URI_CODE);
sUriMatcher.addURI(AUTHORITY, "user",USER_URI_CODE);
}
針對query方法進行演示,其他三個類似,代碼有全部實現(xiàn)的例子, 在自定義Provider文件中.
/**
* 通過自動以的Uri來判斷對應(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來進行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方法來注冊觀察者.
線程安全問題
如果只有一個SQLiteDataBase對象被使用, 那么增刪改查不會出現(xiàn)線程安全問題, 因為其內(nèi)部對數(shù)據(jù)庫的操作是有同步處理. 但是如果多個SQLiteDataBase對象來操作數(shù)據(jù)庫就無法保證其線程安全. 這個時候就要注意了.
使用Socket
Socket也稱為套接字. 是網(wǎng)絡(luò)通信中的概念, 它分為流式套接字和用戶數(shù)據(jù)包套接字兩種. 分別對應(yīng)于網(wǎng)絡(luò)的傳輸控制層中TCP和UDP協(xié)議.
使用Socket通信, 需要在清單文件添加權(quán)限的申請
<!--Socket通信額外需要的線程-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>
服務(wù)端 需要啟動服務(wù), 并且在線程中建立TCP服務(wù), 然后監(jiān)聽一個端口. 當(dāng)客戶端建立連接的時候就會生成一個Socket流. 可以保持后續(xù)的持續(xù)通信. 每一個客戶端都對應(yīng)一個Socket. 如果客戶端斷開連接. 服務(wù)端需要做好相應(yīng)的Socket流關(guān)閉并結(jié)束通話線程. 可以通過在客戶端斷開的時候服務(wù)端的接收字節(jié)流會是null來判斷連接是否還存活.
首先需要開啟一個新線程, Runnable接口這樣實現(xiàn), 以下是偽代碼, 沒有捕捉異常
// 監(jiān)聽 3333 端口
ServerSocket serverSocket = new ServerSocket(3333);
// 判斷服務(wù)是否斷開 沒有斷開就繼續(xù)監(jiān)聽端口
while (!mIsServiceDestoryed) {
//這個是阻塞方法, 當(dāng)有新的客戶端連接,才會返回Socket值
final Socket accept = serverSocket.accept();
// 有了新的客戶端 那就需要創(chuàng)建一個新的線程去維護
new Thread() {
public void run() {
// 這里做對一個Socket的具體操作
responseClient(accept);
}}.start();
}
private void responseClient(Socket client) throws IOException {
//接收客戶端消息
BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
//發(fā)送到客戶端 , 設(shè)置true參數(shù)就不需要手動的刷新輸出流
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(); //這是一個阻塞方法
//判斷如果取出來的是null,那么就說明連接已經(jīng)斷開
if (str == null)
break;
// 對客戶端進行回復(fù)
out.println("我是回復(fù)消息");
}
//準(zhǔn)備關(guān)閉一系列的流
......
}
最后就是要在onDestroy()中把循環(huán)中判斷服務(wù)存活的標(biāo)識置為false, 讓開啟的線程都能自動走完關(guān)閉.
客戶端
在onCreate()先startService開啟TCP的服務(wù), 然后開啟一個線程準(zhǔn)備連接Socket. 可以加上失敗重連的的機制. 只要獲取到了Socket, 就和之前服務(wù)端一樣獲取輸入輸出流進行對應(yīng)的操作.
貼出客戶端的核心代碼, Runnable接口實現(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,自動刷新輸出流里面的內(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)不存在, 進行掃尾動作
in.close();
mPrintWrite.close();
socket.close();
demo圖例Socket相關(guān)代碼存在倉庫的socket包中

各種IPC的差異以及選擇
| 名稱 | 優(yōu)點 | 缺點 | 使用場景 |
|---|---|---|---|
Bundle |
簡單易用 | 只能傳輸Bundle支持的數(shù)據(jù)類型 | 四大組件間的進程間通信 |
文件共享 |
簡單易用 | 不適合高并發(fā)場景,并且無法做到進程間的即時通 | 無法并發(fā)訪問情形, 交換簡單的數(shù)據(jù)實時性不高的場景 |
AIDL |
功能強大 | 使用稍復(fù)雜,需要處理好線程同步 | 一對多通信且有RPC需求 |
ContentProvider |
在數(shù)據(jù)源訪問方面功能強大,支持一對多并發(fā)數(shù)據(jù)共享 | 可以理解為受約束的AIDL,主要提供數(shù)據(jù)源的CRUD操作 | 一對多的進程間的數(shù)據(jù)共享 |
Messenger |
功能一般, 支持一對多串行通信,支持實時通信 | 不能很好處理高并發(fā),不支持RPC,數(shù)據(jù)通過Message進行傳輸, 因此只能傳輸Bundle支持的數(shù)據(jù)類型 | 低并發(fā)的一對多即時通信,無RPC需求,或者無需要返回結(jié)果的RPC需求 |
Socket |
功能強大,可以通過網(wǎng)絡(luò)傳輸字節(jié)流,支持一對多并發(fā)實時通信 | 實現(xiàn)細(xì)節(jié)稍微有點繁瑣,不支持直接的 | 網(wǎng)絡(luò)數(shù)據(jù)交換 |
參看文章
《Android 開發(fā)藝術(shù)探索》書集
《Android 開發(fā)藝術(shù)探索》 02-IPC機制
https://github.com/feiwodev/AndroidDevelopmentArt