1. 程式人生 > >Android 中的 IPC 方式(六) Binder 連線池和選用合適的 IPC 方法

Android 中的 IPC 方式(六) Binder 連線池和選用合適的 IPC 方法

1. Binder 連線池

通過前面幾篇文章的介紹,我們知道,不同的 IPC 方式有不同的特點和適用場景。在這篇文章中,我們在介紹下 AIDL,原因是 AIDL 是一種最常用的程序間通訊方式,是日常開發中程序間通訊的首選,所以我們需要額外強調一下。

如何使用 AIDL 我們在前面已經介紹完了,這裡在回顧一下它的大致流程,首先建立一個 Service 和一個 AIDL 介面,接著建立一個類繼承自 AIDL 介面中的 Stub 類並實現 Stub 中的抽象方法,在 Service 的 onBind 方法中返回這個類的物件,然後客戶端就可以繫結服務端的 Service,建立連線後就可以訪問遠端服務端的方法了。

上述過程就是典型的 AIDL 的使用流程。這本來沒有什麼問題,但是現在考慮一種情況:公司的專案越來越大了,現在有十個業務模組都需要使用 AIDL 來進行程序間通訊,那麼我們該如何處理呢?也許你會說:“就按照 AIDL 的實現方式一個一個的實現唄”,這是可以的,如果用這種方法,我們首先需要建立 10 個Service,這好像有點多啊!如果有 100 個地方需要使用 AIDL 呢,先建立 100 個 Service?到這裡,或許我們應該明白問題所在了。隨著 AIDL 數量的增加,我們不能無限制的增加 Service,Service 是四大元件之一,本身就是一種系統資源,而且太多的 Service 會使得我們的應用看起來很重量級,因為正在執行的 Service 可以在應用詳情頁看到,當我們的詳情頁有十個服務正在執行時,這看起來並不是什麼好事,針對上述問題,我們需要減少 Service 的數量,將所有的 AIDL 放在他同一個 Service 中去處理。

在這種模式下,整個工作機制是這樣的:每個業務模組建立自己的 AIDL 介面並實現次介面,這個時候不同業務模組之間是不能有耦合的,所以實現細節我們要單獨開發,然後向服務端提供自己的唯一標識和其對應的 Binder 物件;對於服務端來說,只需要一個 Service 就夠了,服務端提供一個 queryBinder 介面,這個介面能更具業務模組的特徵來返回相應的 Binder 物件給他們,不同的業務模組拿到所需的 Binder 物件後就可以進行遠端方法呼叫了。由此可見,Binder 連線池的主要作用就是將每個業務模組的 Binder 請求統一轉發到遠端 Service 中去執行,從而避免了重複建立 Service 的過程,他的工作原理如下圖:

通過上面的理論,也許還有點不好理解,下面對 Binder 連線池的程式碼實現做一下說明。首先,為了說明問題,我們提供了兩個 AIDL 介面(ISecurityCenter 和 ICompute)來模擬上面提到的多個業務模組都要使用 AIDL 的情況,其中 ISecurityCenter 介面提供加解密功能,宣告如下:

// ISecurityCenter.aidl
package com.demo.text.demotext.aidl;

interface ISecurityCenter {

    String encrypt(String content);
    String decrypt(String password);

}

而 ICompute 介面提供計算假髮的功能,宣告如下:

// ICompute.aidl
package com.demo.text.demotext.aidl;

interface ICompute {

    int add(int a, int b);

}

雖然上面兩個介面的功能都比較假單,但是用來分析 Binder 連線池的工作原理已經足夠了。接著看一下上面兩個 AIDL 的實現,也比較簡單,程式碼如下:

public class ISecurityCenterImpl extends ISecurityCenter.Stub {

    @Override
    public String encrypt(String content) throws RemoteException {
        return getMD5(content);
    }

    @Override
    public String decrypt(String password) throws RemoteException {
        return encrypt(password);
    }

    public static String getMD5(String str) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(str.getBytes());
            return new BigInteger(1, md.digest()).toString(16);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}
public class IComputeImpl extends ICompute.Stub {

    @Override
    public int add(int a, int b) throws RemoteException {
        return a + b;
    }
}

現在業務模組的 AIDL 介面定義和實現都已經完成了,注意這裡並沒有為每個模組的 AIDL 單獨建立 Service,接下來就是服務端和 Binder 連線池的工作了。

首先,為 Binder 連線池建立 AIDL 介面 IBinderPool.aidl,程式碼如下:

// IBinderPool.aidl
package com.demo.text.demotext.aidl;

interface IBinderPool {

    IBinder queryBinder(int binderCode);

}

接著,為 Binder 連線池建立遠端 Service 並實現 IBinderPool,下面是 queryBinder 的具體實現,可以看到請求轉發的實現方法,當 Binder 連線池連線上遠端服務時,會根據不同模組的標識即 binderCode 返回不同的 Binder 物件,通過這個 Binder 物件所執行的操作全部發生在遠端服務端、

    @Override
    public IBinder queryBinder(int binderCode) throws RemoteException {
        IBinder binder = null;
        switch (binderCode) {
            case BINDER_SECURITY_CENTER:
                binder = new ISecurityCenterImpl();
                break;
            case BINDER_COMPUTE:
                binder = new IComputeImpl();
                break;
            default:
                break;
        }
        return binder;
    }

遠端 Service 的實現就比較簡單了,程式碼如下:

public class BinderPoolService extends Service {

    private static final String TAG = "BinderPoolService";

    private Binder mBinderPool = new IBinderPoolImpl();

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinderPool;
    }
}

下面還剩下 Binder 連線池的具體實現,在它的內部首先它要去繫結遠端服務,繫結成功後,客戶端就可以通過它的 queryBinder 方法去獲取各自對應的 Binder,拿到所需的 Binder 以後,不同業務模組就可以進行各自的操作了。Binder 連線池的程式碼如下所示:

public class IBinderPool {

    private static final String TAG = "IBinderPool";
    public static final int BINDER_COMPUTE = 0;
    public static final int BINDER_SECURITY_CENTER = 1;

    private Context context;
    private com.demo.text.demotext.aidl.IBinderPool mBinderPool;
    private static volatile IBinderPool sInstance;
    /**
     * 同步工具類,用來協調多個執行緒之間的同步,或者說起到執行緒之間的通訊
     */
    private CountDownLatch mConnectBinderPoolCountDownLatch;

    private IBinderPool(Context context) {
        this.context = context;
        connectBinderPoolService();
    }

    public static IBinderPool getsInstance(Context context) {
        if (sInstance == null) {
            synchronized (IBinderPool.class) {
                if (sInstance == null) {
                    sInstance = new IBinderPool(context);
                }
            }
        }
        return sInstance;
    }

    /**
     * 啟動服務端   設定最大並行數
     */
    private synchronized void connectBinderPoolService() {
        mConnectBinderPoolCountDownLatch = new CountDownLatch(1);
        Intent service = new Intent(context, BinderPoolService.class);
        context.bindService(service, mBinderPoolConnection, Context.BIND_AUTO_CREATE);
        try {
            mConnectBinderPoolCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public IBinder queryBinder(int binderCode) {
        IBinder binder = null;
        try {
            if (mBinderPool != null) {
                //呼叫 AIDL 介面的 queryBinder方法  找到對應的 Binder
                binder = mBinderPool.queryBinder(binderCode);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return binder;
    }

    public static class BinderPoolImpl extends com.demo.text.demotext.aidl.IBinderPool.Stub{

        @Override
        public IBinder queryBinder(int binderCode) throws RemoteException {
            IBinder binder = null;
            switch (binderCode) {
                case BINDER_SECURITY_CENTER:
                    binder = new ISecurityCenterImpl();
                    break;
                case BINDER_COMPUTE:
                    binder = new IComputeImpl();
                    break;
                default:
                    break;
            }
            return binder;
        }
    }

    /**
     * 處理 Binder 異常停止的問題
     */
    private ServiceConnection mBinderPoolConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mBinderPool = BinderPoolImpl.Stub.asInterface(service);
            try {
                mBinderPool.asBinder().linkToDeath(mBinderPoolDeathRecipient, 0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            //重置計數器變為0   多個執行緒同時被喚醒
            mConnectBinderPoolCountDownLatch.countDown();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    private IBinder.DeathRecipient mBinderPoolDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            mBinderPool.asBinder().unlinkToDeath(mBinderPoolDeathRecipient, 0);
            mBinderPool = null;
            connectBinderPoolService();
        }
    };

}

Binder 連線池的具體實現已經分析完了,至於裡邊用到的 CountDownLatch,這裡就不詳細說明了。它的好處是顯而易見的,針對上面的例子,我們只需要建立一個 Service 即可完成多個 AIDL 介面的工作,下面我們來驗證一下效果,新建立一個 Activity,線上程中執行如下操作:

    private void doWork() {
        //初始化
        IBinderPool binderPool = IBinderPool.getsInstance(this);
        //獲取對應的 Binder 物件
        IBinder securityBinder = binderPool.queryBinder(IBinderPool.BINDER_SECURITY_CENTER);
        //將 Binder 轉化成對應的 AIDL 介面
        mISecurityCenter = ISecurityCenterImpl.asInterface(securityBinder);
        Log.i(TAG, "visit ISecurityCenter");
        String msg = "Hello World 安卓";
        try {
            String password = mISecurityCenter.encrypt(msg);
            Log.i(TAG, "encrypt:" + password);
            Log.i(TAG, "decrypt:" + mISecurityCenter.decrypt(password));
        } catch (RemoteException e) {
            e.printStackTrace();
        }

        Log.i(TAG, "visit IComputeImpl");
        //獲取對應的 Binder 物件
        IBinder computeBinder = binderPool.queryBinder(IBinderPool.BINDER_COMPUTE);
        //將 Binder 轉化成對應的 AIDL 介面
        mCompute = IComputeImpl.asInterface(computeBinder);
        try {
            Log.i(TAG, "3+5=" + mCompute.add(3, 5));
        } catch (RemoteException e) {
            e.printStackTrace();
        }

    }

在上述程式碼中,我們先呼叫了 ISecurityCenter 和 ICompute 這兩個 AIDL 介面中的方法,看一下 log,很顯然,工作正常。

09-18 15:31:33.467 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: visit ISecurityCenter
09-18 15:31:33.469 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: encrypt:d043d5a0b4e48253c6a67d8ee16a23d2
09-18 15:31:33.470 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: decrypt:2f404b7110963654d94f38795110aea4
09-18 15:31:33.470 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: visit IComputeImpl
09-18 15:31:33.473 8059-8073/com.demo.text.demotext I/IBinderPoolActivity: 3+5=8

這裡額外說明一下,為什麼要線上程中執行呢?這是因為在 Binder 連線池的實現中,我們通過 CountDownLatch 將 bindService 這一非同步操作轉換成了同步操作,這就意味著它有可能是耗時的,然後就是通過 Binder 方法的呼叫過程也有可能是耗時的,因此不建議放在主執行緒中去執行。注意到 BinderPool 是一個單利實現,因此在同一個程序中只會初始化一次,所以如果我們提前初始化 BinderPool,那麼可以優化程式的體驗,比如我們可以放在 Application 中提前對 BinderPool 進行初始化,雖然這不能保證當前我們呼叫 BinderPool 時它一定是初始化好的,但是大多數情況下,這種初始化工作(繫結遠端服務)的時間開銷是可以接受的。另外,BinderPool 中有斷線重連的機制,當遠端服務異常終止,也需要手動去重新獲取最新的 Binder 物件,這個是需要注意的。

有了 BinderPool 可以大大方便日常的開發工作,比如如果有一個新的業務模組需要新增新的 AIDL,那麼在它實現了自己的 AIDL 介面後,只需要修改 BinderPoolImpl 中的 queryBinder 方法,給自己新增一個新的 binderCode 並返回對應的 Binder  物件即可,不需要做其他的修改,也不需要建立新的 Service。由此可見,BinderPool 能夠極大的提高 AIDL 的開發效率,並且可以沒變大量的 Service 建立,因此,建議在 AIDL 開發中引入 BinderPool 機制。

2.選用合適的 IPC 方法

在前面,我們介紹了各式各樣的 IPC 方式,那麼到底它們有什麼不同呢?我們到底該使用哪一種呢?這裡我們來解答下這個問題。如下表,可以明確的看出不同的 IPC 方式的優缺點和適用場景。那麼在實際開發中,只需要選擇合適的 IPC 方式就可以輕鬆完成多程序的開發。

IPC 方式的優缺點和適用場景
名稱 優點 缺點 適用場景
Bundle 簡單易用 只能傳輸 Bundle 支援的資料型別 四大元件的程序間通訊
檔案共享 簡單易用

不適合高併發場景,並且無法做到程序間的即時通訊

無開發訪問情形,交換簡單的資料實時性不高的場景。
AIDL 功能強大,支援一對多併發通訊,支援實時通訊。 使用複雜,需要處理好縣城同步 一對多通訊,且有 RPC 需求。
Message 功能一般,支援一對多序列通訊,支援實時通訊。

不能很好地處理高併發情形,不支援 RPC,

資料通過 Message 進行傳輸,因此只能傳說 Bundle 支援的資料型別

低併發的一對多即時通訊,無

RPC 需求,或者無需要返回結果的 RPC 請求。

ContentProvider

在資料來源訪問方面資料強大,支援一對多併發資料共享,可通過 Call 方法擴充套件其他操作。

可以理解為受約束的 AIDL,主要提供資料的 CRUD 操作。 一對多的程序間資料共享。
Socket

功能強大,可以通過網路傳輸位元組流,支援一對多併發實時通訊。

實現細節稍微有點繁瑣,不支援直接的 RPC. 網路資料傳輸。