零 燙燙燙燙燙燙
單例模式,也叫單子模式,是一種常用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利于我們協調系統整體的行為。
但這種設計模式有局限:只能在一個進程內生效。但項目開發中又難免會出現開啟多個進程的情況。這個時候,原本設計的單例,在整個應用的范圍來看,變成了兩個單例。兩個進程內的單例的內部狀態(變量的取值)也就無法同步了,這也是這個問題的核心(單例的行為(方法)在不同進程是一致的,內部狀態會影響到行為的結果)。
轉載請注明原帖地址:http://sr1.me/think-when-god-laugh/2016/03/22/across-process-singleton-implement.html
一 如何解決
解決數據不同步問題的方法很多,簡單的做法有兩種:持久化或者跨進程調用。
1 持久化
Android可用的持久化的方式有本地文件、SharedPreference和數據庫這幾種。
通過將數據持久化到本地,數據讀寫都通過操作持久化數據,可以實現數據的同步。
這種方案會引入新的問題:同時寫文件的問題(數據可能會亂掉),同時會增加讀寫本地IO的耗費。
在以上三種持久化方式里,本地文件、SharedPreference都有可能出現同時寫文件的問題。數據庫還好,而且Android組件里有ContentProvider可以幫助我們簡化一些操作。
但這三種方法,都要額外做一些事情,比如數據存儲格式(本地文件)、字段名的定義和維護(SharedPreference)、表的定義和維護和增刪改查的實現(數據庫)。光想一想就很頭大。
最開始有想過ContentProvider的方式實現,但實現起來也挺麻煩挺蛋疼的,后來就不了了之了。
2 IPC(進程間調用)
IPC機制很適合用于解決這個問題,這個實現方式更接近后端的RPC(遠程過程調用)。Android的進程間通訊機制采用AIDL來實現。
這種實現方式的方法步驟也不算簡單:
- 定義AIDL接口
- 實現AIDL接口里的方法
- 實現一個Service,在綁定的時候返回實現了AIDL接口Binder對象(被調用方)
- 綁定Service,獲得Binder對象,通過Binder對象進行方法調用(調用方)
雖然不簡單,但也不復雜。但怎么應用到現有代碼里呢?
依舊是最簡單的解決思路:
- 為每個單例的調用都封裝一層(實際是兩層,一層給業務,一層是AIDL,用于跨進程調用)
- 在調用的時候,封裝層里判斷當前調用的執行環境,如果在單例所在的進程,則調用單例的對應方法,否則,發起一次進程間調用。
這個解決思路里,大部分是體力活:
- 把單例里定義的方法添加到AIDL文件里
- 實現AIDL文件里的方法(跨進程調用的封裝)
- 添加封裝層(if (在單例的進程) { 調用單例的方法; } else { 發起跨進程調用; })
- 修改原有業務的調用代碼,把它改為封裝層的調用
(我們不生產代碼,我們只是代碼的搬運工)
3 完(卒)
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
(╯‵□′)╯︵┻━┻
為什么要這樣對我(抱頭痛哭)
為什么要這樣對我(抱頭痛哭)
為什么要這樣對我(抱頭痛哭)
為什么要這樣對我(抱頭痛哭)
為什么要這樣對我(抱頭痛哭)
為什么要這樣對我(抱頭痛哭)
難道就沒有更簡單的方式了嗎?!
難道就沒有更簡單的方式了嗎?!
難道就沒有更簡單的方式了嗎?!
難道就沒有更簡單的方式了嗎?!
難道就沒有更簡單的方式了嗎?!
難道就沒有更簡單的方式了嗎?!
二 你說你要更簡單的?
讓我們來審視下上面的方案的實現步驟:
- 定義AIDL接口
- 實現AIDL接口里的方法
- 實現一個Service,在綁定的時候返回實現了AIDL接口Binder對象(被調用方)
- 綁定Service,獲得Binder對象,通過Binder對象進行方法調用(調用方)
1 簡化封裝層
慢著!
既然我們都需要實現AIDL接口了,為什么不把單例的實現和AIDL接口的實現整合起來?
也就是說:通過這種方式實現的單例的實例,是一個可以用于跨進程傳輸的對象!
進一步說:我們可以在綁定的時候,把這個單例(Binder)返回,其他進程只有得到這個Binder(RPC里的Proxy),就能操作到我們這個單例了,而這個單例也就成為了我們應用程序范疇內所需要的單例。
想到了這一點,我們的封裝層就可以廢掉了。80%的體力活瞬間蒸發!
2 簡化綁定處理過程
剩下的20%的體力活就變成了:
- 定義AIDL接口,用單例對象實現這個AIDL接口
- 使用到這個單例的都要執行一次綁定,綁定成功后,作為單例的實例保存下來即可。
第一點怎么都省不了了。但第二點呢?看起來是重復性很強的編碼過程呢:
- 修改Service實現,返回實現了AIDL的單例
- onServiceConnected里,把得到的單例的代理,設為本進程的單例對象
如果能一次性就把所有的單例都傳遞過來,不就能少掉多次綁定調用,同時還統一了入口和出口。
寫過AIDL的一定會跟另一個類打交道:Parcelable。Parcelable的實現需要需要我們處理數據的序列化和反序列化。在這里我們的入口和出口能實現統一,同時,Parcel對象還有兩個重要的方法:writeStrongInterface
和readStrongBinder
,這兩個方法實現了Binder對象的序列化和反序列化操作。
因此我們可以在這里把所有的單例通過writeStrongInterface
序列化,傳遞到另一個進程,另一個進程再進行readStrongBinder
,把對應的代理給取出來,并放置到單例里。
這樣以來,我們的綁定處理過程就得到了簡化。
3 Word is cheap, show me the code
3.1 核心
說完了以上那么多,其實也就兩個關鍵點:
- 單例對象實現AIDL接口,以支持跨進程
- Parcelable里統一序列化(Stub)和反序列化(Proxy)單例對象
3.2 實例-單例
這里假定有以下幾個單例:
SingletonA(A表示是在A進程)
SingletonA.aidl
是它的AIDL接口;
SingletonAImp.java
是這個單例的實現。
SingletonB(B表示是在B進程)
SingletonB.aidl
是它的AIDL接口;
SingletonBImp.java
是這個單例的實現。
SingletonC(C表示是在C進程)
SingletonC.aidl
是它的AIDL接口;
SingletonCImp.java
是這個單例的實現。
獲取他們的實例的方法統一為靜態方法getInstance
,代碼如下,這里也是單例實現中唯一需要判斷所處進程的地方:
public static synchronized SingletonA getInstance() {
if (ProcessUtils.isProcessA()) {
if (INSTANCE == null) {
INSTANCE = new SingletonAImp();
}
return INSTANCE;
} else {
if (INSTANCE == null) {
/** 自發重連 */
Intent intent = new Intent(
App.getContext(), ServiceA.class);
App.getContext().bindService(intent,
new InstanceReceiver(),
Context.BIND_AUTO_CREATE);
}
return INSTANCE;
}
}
這個getInstance跟傳統的單例不一樣,它可能返回為空。
這里面有兩個東西需要我們注意:
- ServiceA.class
- InstanceReceiver
3.3 實例-Service
ServiceA.class是A進程提供單例給其他進程的服務的類,每個進程都需要有一個(這樣別的進程才能綁定過來)。所以在這個例子,會有ServiceB.class,ServiceC.class,這幾個類的實現都是一樣的,因此這里他們其實只是簡單的繼承了一個基類BaseService,并沒有做其他改動,需要派生出來的原因是需要在AndroidManifest.xml里為不同進程指定一個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 實例-InstanceReceiver
InstanceReceiver.class是一個ServiceConnection的實現,這里把接收到的Binder對象轉為一個InstanceTransfer
,也就是封裝的一個AIDL對象,這個對象的作用是把我們的單例傳輸過來。
代碼:
@Override
public void onServiceConnected(ComponentName name, IBinder service){
Log.i(TAG, "[onServiceConnected]" + name);
try {
/** 調用這句就會將單例(代理)實例傳遞過來了 */
InstanceTransfer.Stub.asInterface(service).transfer();
} catch (Exception e) {
Log.e(TAG, "[onServiceConnected][exception when transfer instance]" + name, e);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
/** 意外斷開綁定的情況,這里可以重寫成發起重連 */
Log.e(TAG, "[onServiceDisconnected][exception when service disconnected]" + name);
}
InstanceTransfer的定義:
interface InstanceTransfer {
InstanceCarrier transfer();
}
3.5 InstanceCarrier
這里冒出了一個InstanceCarrier,這個InstanceCarrier實際上就是我們定義的一個Parcelable
類,這個類干的事情,就是前面提到的:統一序列化(Stub)和反序列化(Proxy)單例對象。
代碼大概是這樣的:
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;
/**
* 在這里把單例轉成IBinder傳輸到其他進程
* @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));
}
}
/**
* 在這里把跨進程傳遞過來的IBinder賦值給對應的實例
* @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];
}
};
這么一套下來,整個實現機制就搞好。
后續添加新的單例,只需要:
- 定義單例的AIDL
- 實現單例
- 在InstanceCarrier里添加序列化和反序列化的兩行代碼
- 如果添加了進程,需要在那個進程添加一個BaseService的派生類
如果是新增接口的話,也就簡單的修改下AIDL文件,然后實現新的接口。
三 存在的問題或不足
- 單例內使用到的數據類型,必須支持AIDL(Android IPC通訊的要求),對于簡單的數據,可以使用系統的Bundle對象
- 實現的調用方法的時候,需要考慮到執行的線程可能不是調用的線程(跨進程調用的情況下是在Binder線程),因為調用是同步的,對返回結果沒有影響,但對于需要在主線程執行的邏輯來說,需要主動異步放到主線程去。
- 線程安全:這個是編寫單例的時候需要注意的問題,因為任何一個線程都能夠訪問到這個單例,使用這個方式支持跨進程可能會放大這個問題。
- Android的IPC通訊機制本身的限制:Android的IPC通訊共享1M的內存,因此需要避免傳輸大量的數據,同時,處理邏輯也不宜很耗時(否則消費數據不及時,消費者處理能力低于生產者的生產力,遲早會耗光1M的內存)。、
- AIDL不支持方法重載(弱弱的...)