零 燙燙燙燙燙燙
單例模式糯而,也叫單子模式账嚎,是一種常用的軟件設(shè)計(jì)模式涂身。在應(yīng)用這個(gè)模式時(shí)雄卷,單例對(duì)象的類必須保證只有一個(gè)實(shí)例存在。許多時(shí)候整個(gè)系統(tǒng)只需要擁有一個(gè)的全局對(duì)象蛤售,這樣有利于我們協(xié)調(diào)系統(tǒng)整體的行為丁鹉。
但這種設(shè)計(jì)模式有局限:只能在一個(gè)進(jìn)程內(nèi)生效。但項(xiàng)目開發(fā)中又難免會(huì)出現(xiàn)開啟多個(gè)進(jìn)程的情況悴能。這個(gè)時(shí)候揣钦,原本設(shè)計(jì)的單例,在整個(gè)應(yīng)用的范圍來(lái)看漠酿,變成了兩個(gè)單例冯凹。兩個(gè)進(jìn)程內(nèi)的單例的內(nèi)部狀態(tài)(變量的取值)也就無(wú)法同步了,這也是這個(gè)問題的核心(單例的行為(方法)在不同進(jìn)程是一致的炒嘲,內(nèi)部狀態(tài)會(huì)影響到行為的結(jié)果)宇姚。
轉(zhuǎn)載請(qǐng)注明原帖地址:http://sr1.me/think-when-god-laugh/2016/03/22/across-process-singleton-implement.html
一 如何解決
解決數(shù)據(jù)不同步問題的方法很多,簡(jiǎn)單的做法有兩種:持久化或者跨進(jìn)程調(diào)用摸吠。
1 持久化
Android可用的持久化的方式有本地文件空凸、SharedPreference和數(shù)據(jù)庫(kù)這幾種。
通過將數(shù)據(jù)持久化到本地寸痢,數(shù)據(jù)讀寫都通過操作持久化數(shù)據(jù),可以實(shí)現(xiàn)數(shù)據(jù)的同步紊选。
這種方案會(huì)引入新的問題:同時(shí)寫文件的問題(數(shù)據(jù)可能會(huì)亂掉)啼止,同時(shí)會(huì)增加讀寫本地IO的耗費(fèi)。
在以上三種持久化方式里兵罢,本地文件献烦、SharedPreference都有可能出現(xiàn)同時(shí)寫文件的問題。數(shù)據(jù)庫(kù)還好卖词,而且Android組件里有ContentProvider可以幫助我們簡(jiǎn)化一些操作巩那。
但這三種方法,都要額外做一些事情此蜈,比如數(shù)據(jù)存儲(chǔ)格式(本地文件)即横、字段名的定義和維護(hù)(SharedPreference)、表的定義和維護(hù)和增刪改查的實(shí)現(xiàn)(數(shù)據(jù)庫(kù))裆赵。光想一想就很頭大东囚。
最開始有想過ContentProvider的方式實(shí)現(xiàn),但實(shí)現(xiàn)起來(lái)也挺麻煩挺蛋疼的战授,后來(lái)就不了了之了页藻。
2 IPC(進(jìn)程間調(diào)用)
IPC機(jī)制很適合用于解決這個(gè)問題桨嫁,這個(gè)實(shí)現(xiàn)方式更接近后端的RPC(遠(yuǎn)程過程調(diào)用)。Android的進(jìn)程間通訊機(jī)制采用AIDL來(lái)實(shí)現(xiàn)份帐。
這種實(shí)現(xiàn)方式的方法步驟也不算簡(jiǎn)單:
- 定義AIDL接口
- 實(shí)現(xiàn)AIDL接口里的方法
- 實(shí)現(xiàn)一個(gè)Service璃吧,在綁定的時(shí)候返回實(shí)現(xiàn)了AIDL接口Binder對(duì)象(被調(diào)用方)
- 綁定Service,獲得Binder對(duì)象废境,通過Binder對(duì)象進(jìn)行方法調(diào)用(調(diào)用方)
雖然不簡(jiǎn)單肚逸,但也不復(fù)雜。但怎么應(yīng)用到現(xiàn)有代碼里呢彬坏?
依舊是最簡(jiǎn)單的解決思路:
- 為每個(gè)單例的調(diào)用都封裝一層(實(shí)際是兩層朦促,一層給業(yè)務(wù),一層是AIDL栓始,用于跨進(jìn)程調(diào)用)
- 在調(diào)用的時(shí)候务冕,封裝層里判斷當(dāng)前調(diào)用的執(zhí)行環(huán)境,如果在單例所在的進(jìn)程幻赚,則調(diào)用單例的對(duì)應(yīng)方法禀忆,否則,發(fā)起一次進(jìn)程間調(diào)用落恼。
這個(gè)解決思路里箩退,大部分是體力活:
- 把單例里定義的方法添加到AIDL文件里
- 實(shí)現(xiàn)AIDL文件里的方法(跨進(jìn)程調(diào)用的封裝)
- 添加封裝層(if (在單例的進(jìn)程) { 調(diào)用單例的方法; } else { 發(fā)起跨進(jìn)程調(diào)用; })
- 修改原有業(yè)務(wù)的調(diào)用代碼,把它改為封裝層的調(diào)用
(我們不生產(chǎn)代碼佳谦,我們只是代碼的搬運(yùn)工)
3 完(卒)
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
為什么要這樣對(duì)我(抱頭痛哭)
為什么要這樣對(duì)我(抱頭痛哭)
為什么要這樣對(duì)我(抱頭痛哭)
為什么要這樣對(duì)我(抱頭痛哭)
為什么要這樣對(duì)我(抱頭痛哭)
為什么要這樣對(duì)我(抱頭痛哭)
難道就沒有更簡(jiǎn)單的方式了嗎戴涝?!
難道就沒有更簡(jiǎn)單的方式了嗎钻蔑?啥刻!
難道就沒有更簡(jiǎn)單的方式了嗎?咪笑!
難道就沒有更簡(jiǎn)單的方式了嗎可帽?!
難道就沒有更簡(jiǎn)單的方式了嗎窗怒?映跟!
難道就沒有更簡(jiǎn)單的方式了嗎?扬虚!
二 你說(shuō)你要更簡(jiǎn)單的?
讓我們來(lái)審視下上面的方案的實(shí)現(xiàn)步驟:
- 定義AIDL接口
- 實(shí)現(xiàn)AIDL接口里的方法
- 實(shí)現(xiàn)一個(gè)Service努隙,在綁定的時(shí)候返回實(shí)現(xiàn)了AIDL接口Binder對(duì)象(被調(diào)用方)
- 綁定Service,獲得Binder對(duì)象孔轴,通過Binder對(duì)象進(jìn)行方法調(diào)用(調(diào)用方)
1 簡(jiǎn)化封裝層
慢著剃法!
既然我們都需要實(shí)現(xiàn)AIDL接口了,為什么不把單例的實(shí)現(xiàn)和AIDL接口的實(shí)現(xiàn)整合起來(lái)?
也就是說(shuō):通過這種方式實(shí)現(xiàn)的單例的實(shí)例贷洲,是一個(gè)可以用于跨進(jìn)程傳輸?shù)膶?duì)象收厨!
進(jìn)一步說(shuō):我們可以在綁定的時(shí)候,把這個(gè)單例(Binder)返回优构,其他進(jìn)程只有得到這個(gè)Binder(RPC里的Proxy)诵叁,就能操作到我們這個(gè)單例了,而這個(gè)單例也就成為了我們應(yīng)用程序范疇內(nèi)所需要的單例钦椭。
想到了這一點(diǎn)拧额,我們的封裝層就可以廢掉了。80%的體力活瞬間蒸發(fā)彪腔!
2 簡(jiǎn)化綁定處理過程
剩下的20%的體力活就變成了:
- 定義AIDL接口侥锦,用單例對(duì)象實(shí)現(xiàn)這個(gè)AIDL接口
- 使用到這個(gè)單例的都要執(zhí)行一次綁定,綁定成功后德挣,作為單例的實(shí)例保存下來(lái)即可恭垦。
第一點(diǎn)怎么都省不了了。但第二點(diǎn)呢格嗅?看起來(lái)是重復(fù)性很強(qiáng)的編碼過程呢:
- 修改Service實(shí)現(xiàn)番挺,返回實(shí)現(xiàn)了AIDL的單例
- onServiceConnected里,把得到的單例的代理屯掖,設(shè)為本進(jìn)程的單例對(duì)象
如果能一次性就把所有的單例都傳遞過來(lái)玄柏,不就能少掉多次綁定調(diào)用,同時(shí)還統(tǒng)一了入口和出口贴铜。
寫過AIDL的一定會(huì)跟另一個(gè)類打交道:Parcelable粪摘。Parcelable的實(shí)現(xiàn)需要需要我們處理數(shù)據(jù)的序列化和反序列化。在這里我們的入口和出口能實(shí)現(xiàn)統(tǒng)一阀湿,同時(shí)赶熟,Parcel對(duì)象還有兩個(gè)重要的方法:writeStrongInterface
和readStrongBinder
,這兩個(gè)方法實(shí)現(xiàn)了Binder對(duì)象的序列化和反序列化操作陷嘴。
因此我們可以在這里把所有的單例通過writeStrongInterface
序列化,傳遞到另一個(gè)進(jìn)程间坐,另一個(gè)進(jìn)程再進(jìn)行readStrongBinder
灾挨,把對(duì)應(yīng)的代理給取出來(lái),并放置到單例里竹宋。
這樣以來(lái)劳澄,我們的綁定處理過程就得到了簡(jiǎn)化。
3 Word is cheap, show me the code
3.1 核心
說(shuō)完了以上那么多秒拔,其實(shí)也就兩個(gè)關(guān)鍵點(diǎn):
- 單例對(duì)象實(shí)現(xiàn)AIDL接口,以支持跨進(jìn)程
- Parcelable里統(tǒng)一序列化(Stub)和反序列化(Proxy)單例對(duì)象
3.2 實(shí)例-單例
這里假定有以下幾個(gè)單例:
SingletonA(A表示是在A進(jìn)程)
SingletonA.aidl
是它的AIDL接口飒硅;
SingletonAImp.java
是這個(gè)單例的實(shí)現(xiàn)砂缩。
SingletonB(B表示是在B進(jìn)程)
SingletonB.aidl
是它的AIDL接口作谚;
SingletonBImp.java
是這個(gè)單例的實(shí)現(xiàn)。
SingletonC(C表示是在C進(jìn)程)
SingletonC.aidl
是它的AIDL接口庵芭;
SingletonCImp.java
是這個(gè)單例的實(shí)現(xiàn)妹懒。
獲取他們的實(shí)例的方法統(tǒng)一為靜態(tài)方法getInstance
,代碼如下双吆,這里也是單例實(shí)現(xiàn)中唯一需要判斷所處進(jìn)程的地方:
public static synchronized SingletonA getInstance() {
if (ProcessUtils.isProcessA()) {
if (INSTANCE == null) {
INSTANCE = new SingletonAImp();
}
return INSTANCE;
} else {
if (INSTANCE == null) {
/** 自發(fā)重連 */
Intent intent = new Intent(
App.getContext(), ServiceA.class);
App.getContext().bindService(intent,
new InstanceReceiver(),
Context.BIND_AUTO_CREATE);
}
return INSTANCE;
}
}
這個(gè)getInstance跟傳統(tǒng)的單例不一樣眨唬,它可能返回為空。
這里面有兩個(gè)東西需要我們注意:
- ServiceA.class
- InstanceReceiver
3.3 實(shí)例-Service
ServiceA.class是A進(jìn)程提供單例給其他進(jìn)程的服務(wù)的類好乐,每個(gè)進(jìn)程都需要有一個(gè)(這樣別的進(jìn)程才能綁定過來(lái))匾竿。所以在這個(gè)例子,會(huì)有ServiceB.class蔚万,ServiceC.class岭妖,這幾個(gè)類的實(shí)現(xiàn)都是一樣的,因此這里他們其實(shí)只是簡(jiǎn)單的繼承了一個(gè)基類BaseService笛坦,并沒有做其他改動(dòng)区转,需要派生出來(lái)的原因是需要在AndroidManifest.xml里為不同進(jìn)程指定一個(gè)Service。
代碼如下:
public class BaseService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new InstanceTransferImp();
}
}
public class ServiceA extends BaseService {}
public class ServiceB extends BaseService {}
public class ServiceC extends BaseService {}
3.4 實(shí)例-InstanceReceiver
InstanceReceiver.class是一個(gè)ServiceConnection的實(shí)現(xiàn)版扩,這里把接收到的Binder對(duì)象轉(zhuǎn)為一個(gè)InstanceTransfer
废离,也就是封裝的一個(gè)AIDL對(duì)象,這個(gè)對(duì)象的作用是把我們的單例傳輸過來(lái)礁芦。
代碼:
@Override
public void onServiceConnected(ComponentName name, IBinder service){
Log.i(TAG, "[onServiceConnected]" + name);
try {
/** 調(diào)用這句就會(huì)將單例(代理)實(shí)例傳遞過來(lái)了 */
InstanceTransfer.Stub.asInterface(service).transfer();
} catch (Exception e) {
Log.e(TAG, "[onServiceConnected][exception when transfer instance]" + name, e);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
/** 意外斷開綁定的情況,這里可以重寫成發(fā)起重連 */
Log.e(TAG, "[onServiceDisconnected][exception when service disconnected]" + name);
}
InstanceTransfer的定義:
interface InstanceTransfer {
InstanceCarrier transfer();
}
3.5 InstanceCarrier
這里冒出了一個(gè)InstanceCarrier蜻韭,這個(gè)InstanceCarrier實(shí)際上就是我們定義的一個(gè)Parcelable
類,這個(gè)類干的事情柿扣,就是前面提到的:統(tǒng)一序列化(Stub)和反序列化(Proxy)單例對(duì)象肖方。
代碼大概是這樣的:
private static final String TAG = "InstanceCarrier";
private static final int PROCESS_A = 1;
private static final int PROCESS_B = 2;
private static final int PROCESS_C = 3;
/**
* 在這里把單例轉(zhuǎn)成IBinder傳輸?shù)狡渌M(jìn)程
* @param dest
* @param flags
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
if (ProcessUtils.isProcessA()) {
dest.writeInt(PROCESS_A);
dest.writeStrongInterface(SingletonAImp.getInstance());
Log.i(TAG, String.format(
"[write][PROCESS_A][processCode=%s]", PROCESS_A));
}else if (ProcessUtils.isProcessB()) {
dest.writeInt(PROCESS_B);
dest.writeStrongInterface(SingletonBImp.getInstance());
Log.i(TAG, String.format(
"[write][PROCESS_B][processCode=%s]", PROCESS_B));
}else if (ProcessUtils.isProcessC()) {
dest.writeInt(PROCESS_C);
dest.writeStrongInterface(SingletonCImp.getInstance());
Log.i(TAG, String.format(
"[write][PROCESS_C][processCode=%s]", PROCESS_C));
}
}
/**
* 在這里把跨進(jìn)程傳遞過來(lái)的IBinder賦值給對(duì)應(yīng)的實(shí)例
* @param in
*/
protected InstanceCarrier(Parcel in) {
int processCode = in.readInt();
switch (processCode) {
case PROCESS_A:
SingletonAImp.INSTANCE =
SingletonA.Stub.asInterface(in.readStrongBinder());
Log.i(TAG, String.format(
"[read][PROCESS_A][processCode=%s]", processCode));
break;
case PROCESS_B:
SingletonBImp.INSTANCE =
SingletonB.Stub.asInterface(in.readStrongBinder());
Log.i(TAG, String.format(
"[read][PROCESS_B][processCode=%s]", processCode));
break;
case PROCESS_C:
SingletonCImp.INSTANCE =
SingletonC.Stub.asInterface(in.readStrongBinder());
Log.i(TAG, String.format(
"[read][PROCESS_C][processCode=%s]", processCode));
break;
default:
Log.w(TAG, String.format(
"[unknown][processCode=%s]", processCode));
}
}
public InstanceCarrier() {}
@Override
public int describeContents() {
return 0;
}
public static final Creator<InstanceCarrier> CREATOR = new Creator<InstanceCarrier>() {
@Override
public InstanceCarrier createFromParcel(Parcel in) {
return new InstanceCarrier(in);
}
@Override
public InstanceCarrier[] newArray(int size) {
return new InstanceCarrier[size];
}
};
這么一套下來(lái),整個(gè)實(shí)現(xiàn)機(jī)制就搞好未状。
后續(xù)添加新的單例俯画,只需要:
- 定義單例的AIDL
- 實(shí)現(xiàn)單例
- 在InstanceCarrier里添加序列化和反序列化的兩行代碼
- 如果添加了進(jìn)程,需要在那個(gè)進(jìn)程添加一個(gè)BaseService的派生類
如果是新增接口的話司草,也就簡(jiǎn)單的修改下AIDL文件艰垂,然后實(shí)現(xiàn)新的接口。
三 存在的問題或不足
- 單例內(nèi)使用到的數(shù)據(jù)類型埋虹,必須支持AIDL(Android IPC通訊的要求)猜憎,對(duì)于簡(jiǎn)單的數(shù)據(jù)革砸,可以使用系統(tǒng)的Bundle對(duì)象
- 實(shí)現(xiàn)的調(diào)用方法的時(shí)候本缠,需要考慮到執(zhí)行的線程可能不是調(diào)用的線程(跨進(jìn)程調(diào)用的情況下是在Binder線程),因?yàn)檎{(diào)用是同步的肮疗,對(duì)返回結(jié)果沒有影響,但對(duì)于需要在主線程執(zhí)行的邏輯來(lái)說(shuō)柬讨,需要主動(dòng)異步放到主線程去崩瓤。
- 線程安全:這個(gè)是編寫單例的時(shí)候需要注意的問題,因?yàn)槿魏我粋€(gè)線程都能夠訪問到這個(gè)單例姐浮,使用這個(gè)方式支持跨進(jìn)程可能會(huì)放大這個(gè)問題谷遂。
- Android的IPC通訊機(jī)制本身的限制:Android的IPC通訊共享1M的內(nèi)存,因此需要避免傳輸大量的數(shù)據(jù)卖鲤,同時(shí)肾扰,處理邏輯也不宜很耗時(shí)(否則消費(fèi)數(shù)據(jù)不及時(shí),消費(fèi)者處理能力低于生產(chǎn)者的生產(chǎn)力蛋逾,遲早會(huì)耗光1M的內(nèi)存)集晚。、
- AIDL不支持方法重載(弱弱的...)