(自以為)優雅的跨進程單例的實現思路

零 燙燙燙燙燙燙

單例模式,也叫單子模式,是一種常用的軟件設計模式。在應用這個模式時,單例對象的類必須保證只有一個實例存在。許多時候整個系統只需要擁有一個的全局對象,這樣有利于我們協調系統整體的行為。

但這種設計模式有局限:只能在一個進程內生效。但項目開發中又難免會出現開啟多個進程的情況。這個時候,原本設計的單例,在整個應用的范圍來看,變成了兩個單例。兩個進程內的單例的內部狀態(變量的取值)也就無法同步了,這也是這個問題的核心(單例的行為(方法)在不同進程是一致的,內部狀態會影響到行為的結果)。

轉載請注明原帖地址: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來實現。

這種實現方式的方法步驟也不算簡單:

  1. 定義AIDL接口
  2. 實現AIDL接口里的方法
  3. 實現一個Service,在綁定的時候返回實現了AIDL接口Binder對象(被調用方)
  4. 綁定Service,獲得Binder對象,通過Binder對象進行方法調用(調用方)

雖然不簡單,但也不復雜。但怎么應用到現有代碼里呢?

依舊是最簡單的解決思路:

  1. 為每個單例的調用都封裝一層(實際是兩層,一層給業務,一層是AIDL,用于跨進程調用)
  2. 在調用的時候,封裝層里判斷當前調用的執行環境,如果在單例所在的進程,則調用單例的對應方法,否則,發起一次進程間調用。

這個解決思路里,大部分是體力活:

  1. 把單例里定義的方法添加到AIDL文件里
  2. 實現AIDL文件里的方法(跨進程調用的封裝)
  3. 添加封裝層(if (在單例的進程) { 調用單例的方法; } else { 發起跨進程調用; })
  4. 修改原有業務的調用代碼,把它改為封裝層的調用

(我們不生產代碼,我們只是代碼的搬運工)

3 完(卒)

(╯‵□′)╯︵┻━┻

(╯‵□′)╯︵┻━┻

(╯‵□′)╯︵┻━┻

(╯‵□′)╯︵┻━┻

(╯‵□′)╯︵┻━┻

(╯‵□′)╯︵┻━┻

為什么要這樣對我(抱頭痛哭)

為什么要這樣對我(抱頭痛哭)

為什么要這樣對我(抱頭痛哭)

為什么要這樣對我(抱頭痛哭)

為什么要這樣對我(抱頭痛哭)

為什么要這樣對我(抱頭痛哭)

難道就沒有更簡單的方式了嗎?!

難道就沒有更簡單的方式了嗎?!

難道就沒有更簡單的方式了嗎?!

難道就沒有更簡單的方式了嗎?!

難道就沒有更簡單的方式了嗎?!

難道就沒有更簡單的方式了嗎?!

二 你說你要更簡單的?

讓我們來審視下上面的方案的實現步驟:

  1. 定義AIDL接口
  2. 實現AIDL接口里的方法
  3. 實現一個Service,在綁定的時候返回實現了AIDL接口Binder對象(被調用方)
  4. 綁定Service,獲得Binder對象,通過Binder對象進行方法調用(調用方)

1 簡化封裝層

慢著!

既然我們都需要實現AIDL接口了,為什么不把單例的實現和AIDL接口的實現整合起來?

也就是說:通過這種方式實現的單例的實例,是一個可以用于跨進程傳輸的對象!

進一步說:我們可以在綁定的時候,把這個單例(Binder)返回,其他進程只有得到這個Binder(RPC里的Proxy),就能操作到我們這個單例了,而這個單例也就成為了我們應用程序范疇內所需要的單例。

想到了這一點,我們的封裝層就可以廢掉了。80%的體力活瞬間蒸發!

2 簡化綁定處理過程

剩下的20%的體力活就變成了:

  1. 定義AIDL接口,用單例對象實現這個AIDL接口
  2. 使用到這個單例的都要執行一次綁定,綁定成功后,作為單例的實例保存下來即可。

第一點怎么都省不了了。但第二點呢?看起來是重復性很強的編碼過程呢:

  1. 修改Service實現,返回實現了AIDL的單例
  2. onServiceConnected里,把得到的單例的代理,設為本進程的單例對象

如果能一次性就把所有的單例都傳遞過來,不就能少掉多次綁定調用,同時還統一了入口和出口。

寫過AIDL的一定會跟另一個類打交道:Parcelable。Parcelable的實現需要需要我們處理數據的序列化和反序列化。在這里我們的入口和出口能實現統一,同時,Parcel對象還有兩個重要的方法:writeStrongInterfacereadStrongBinder,這兩個方法實現了Binder對象的序列化和反序列化操作。

因此我們可以在這里把所有的單例通過writeStrongInterface序列化,傳遞到另一個進程,另一個進程再進行readStrongBinder,把對應的代理給取出來,并放置到單例里。

這樣以來,我們的綁定處理過程就得到了簡化。

3 Word is cheap, show me the code

GayHub提交地址,基本框架和使用Sample

3.1 核心

說完了以上那么多,其實也就兩個關鍵點:

  1. 單例對象實現AIDL接口,以支持跨進程
  2. 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跟傳統的單例不一樣,它可能返回為空。

這里面有兩個東西需要我們注意:

  1. ServiceA.class
  2. 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];
    }
};

這么一套下來,整個實現機制就搞好。

后續添加新的單例,只需要:

  1. 定義單例的AIDL
  2. 實現單例
  3. 在InstanceCarrier里添加序列化和反序列化的兩行代碼
  4. 如果添加了進程,需要在那個進程添加一個BaseService的派生類

如果是新增接口的話,也就簡單的修改下AIDL文件,然后實現新的接口。

三 存在的問題或不足

  1. 單例內使用到的數據類型,必須支持AIDL(Android IPC通訊的要求),對于簡單的數據,可以使用系統的Bundle對象
  2. 實現的調用方法的時候,需要考慮到執行的線程可能不是調用的線程(跨進程調用的情況下是在Binder線程),因為調用是同步的,對返回結果沒有影響,但對于需要在主線程執行的邏輯來說,需要主動異步放到主線程去。
  3. 線程安全:這個是編寫單例的時候需要注意的問題,因為任何一個線程都能夠訪問到這個單例,使用這個方式支持跨進程可能會放大這個問題。
  4. Android的IPC通訊機制本身的限制:Android的IPC通訊共享1M的內存,因此需要避免傳輸大量的數據,同時,處理邏輯也不宜很耗時(否則消費數據不及時,消費者處理能力低于生產者的生產力,遲早會耗光1M的內存)。、
  5. AIDL不支持方法重載(弱弱的...)
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,837評論 18 139
  • 今天是端午節放假第一天,祝各位小伙伴端午快樂!今天我也給大家送個“粽子”-AIDL之進程間共享單例。關于AIDL的...
    juexingzhe閱讀 1,806評論 0 10
  • 一、Android IPC簡介 IPC是Inter-Process Communication的縮寫,含義就是進程...
    SeanMa閱讀 1,894評論 0 8
  • Android跨進程通信IPC整體內容如下 1、Android跨進程通信IPC之1——Linux基礎2、Andro...
    隔壁老李頭閱讀 10,808評論 13 43
  • Jianwei's blog 首頁 分類 關于 歸檔 標簽 巧用Android多進程,微信,微博等主流App都在用...
    justCode_閱讀 5,955評論 1 23