[Android] Binder connection pool

Code for this article: Github

Let’s talk about the problem first. AIDL needs a client and a server. The server is often a service, but this will cause problems. When there are more teams and more modules, each module has its own service. Obviously, this is very difficult. Crap. Therefore, the introduction of Binder connection pool.

First, the realization of ideas

Binder connection pool principle.png

For each AIDL interface, implement the corresponding Binder separately, unify a service, and then distribute the Binder through a connection pool each time the service is bound. In the queryBinder, the requested code is used to determine which Binder is allocated. In this way, it can be avoided that with the increase of project modules, the number of services will increase. Every time a new module interface is added, it is only necessary to add a new case judgment to the switch in the queryBinder, and the decoupling between modules can be realized. The decoupling between the service and the specific implementation of the module can kill two birds with one stone.

2. Implement Binder connection pool from scratch

Create two AIDLs.

// IUser.aidl
package com.cm.mybinderpool;

interface IUser {
    boolean login(String username, String password);
}

// ICompute.aidl
package com.cm.mybinderpool;

interface ICompute {
    int add(int x, int y);
}

Then implement the corresponding Binder respectively.

public class UserImpl extends IUser.Stub {
    @Override
    public boolean login(String username, String password) throws RemoteException {
        return true;
    }
}

public class ComputeImpl extends ICompute.Stub {
    @Override
    public int add(int x, int y) throws RemoteException {
        return x + y;
    }
}

It's simpler now. Next is our BinderPool, first create a new AIDL interface, which has only one queryBinder method.

// IBinderPool.aidl
package com.cm.mybinderpool;

interface IBinderPool {
    IBinder queryBinder(int binderCode);
}

Implement BinderPool.
First of all, since it is a thread pool, it should be a singleton pattern. This is implemented using a double-checked lock. When obtaining a single instance, you need to pass in the context context, mainly when you bind the service later, you need to use the upper and lower information.

private static volatile BinderPool sInstance;
private Context mContext;

private BinderPool(Context context) {
    mContext = context;
}

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

Implementing binder distribution, that is, the queryBinder interface of IBinderPool, is relatively simple to implement, which is switch-case judgment.

//binder code
public static final int BINDER_USER = 0;
public static final int BINDER_COMPUTE = 1;

public static class BinderPoolImpl extends IBinderPool.Stub {
    @Override
    public IBinder queryBinder(int binderCode) throws RemoteException {
        switch (binderCode) {
            case BINDER_COMPUTE:
                return new ComputeImpl();
            case BINDER_USER:
                return new UserImpl();
            default:
                return null;
        }
    }
}

Well, the next step is to create the corresponding service. This returns the Binder of IBinderPool.

public class BinderPoolService extends Service {
    public BinderPoolService() {
    }

    Binder binderPool = new BinderPool.BinderPoolImpl();

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

In addition to the distribution of binders, the role of the thread pool is the connection of services.
The connection is made during initialization.

private BinderPool(Context context) {
    mContext = context;
    connectService();
}

connectService completes the connection.
Java's CountDownLatch will be used here. CountDownLatch is implemented by a counter. The initial value of the counter is the number of threads. Whenever a thread completes its task, the value of the counter will decrease by 1. When the counter value reaches 0, it indicates that all threads have completed the task, and then the thread waiting on the latch can resume executing the task. The first interaction with CountDownLatch is that the main thread waits for other threads. The main thread must call the CountDownLatch.await() method immediately after starting other threads. In this way, the operation of the main thread will be blocked on this method. After other threads are completed, the main thread is notified, and the value is reduced by 1 through CountDownLatch.countDown(), because the main thread can continue after the binderservice callback, so CountDownLatch is used here to achieve.

private CountDownLatch mConnectBinderPoolCountDownLatch;
private IBinderPool mBinderPool;

private void connectService() {
    mConnectBinderPoolCountDownLatch = new CountDownLatch(1);
    Intent intent = new Intent(mContext, BinderPoolService.class);
    mContext.bindService(intent, conn, Context.BIND_AUTO_CREATE);
    try {
        mConnectBinderPoolCountDownLatch.await();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

ServiceConnection conn = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        mBinderPool = IBinderPool.Stub.asInterface(iBinder);
        mConnectBinderPoolCountDownLatch.countDown();
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {

    }
};

Here, set up a death listener for Binder. After the service connection is successful, get the binder, use iBinder.linkToDeath(mBinderPoolDeathRecipient, 0), when the connection pool finds that the service is disconnected, you need to reconnect to the service to maintain a long connection.

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

Then, we have to provide the most important distribution interface in BinderPool.

public IBinder queryBinder(int code) {
    if(mBinderPool == null) {
        return null;
    }
    try {
        return mBinderPool.queryBinder(code);
    } catch (RemoteException e){
        e.printStackTrace();
    }
    return null;
}

Here, the queryBinder method of service is removed, and then the specific implementation is still in BinderPool, so that the use process is all dealing with the BinderPool class instead of the service.

Well, this has implemented our Binder connection pool, now let's use it.
In our MainActivity, the call is a time-consuming operation, so we need to open another thread, otherwise it will block the UI thread and cause ANR. I won't talk about how to open a thread, but the method called directly by the thread.

private void testBinderPool() {
    BinderPool mBinderPool = BinderPool.getInstance(MainActivity.this);
    //测试ICompute
    IBinder mComputeBinder = mBinderPool.queryBinder(BinderPool.BINDER_COMPUTE);
    ICompute mCompute = ICompute.Stub.asInterface(mComputeBinder);
    try {
        Log.i("chenming", "1+2 = " + mCompute.add(1, 2));
    } catch (RemoteException e) {
        e.printStackTrace();
    }
    
    //测试IUser
    IBinder mUserBinder = mBinderPool.queryBinder(BinderPool.BINDER_USER);
    IUser mUser = IUser.Stub.asInterface(mUserBinder);
    try {
        Log.i("chenming", "login " + mUser.login("user", "psd"));
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

Run it, you can see the log of the running result.

05-30 16:38:28.964 32108 32132 I chenming: 1+2 = 3
05-30 16:38:28.965 32108 32132 I chenming: login true

3. Review

In the whole process, the client is actually dealing with BinderPool. BinderPool is a singleton, mainly because the access process is a concurrent process. If there are two BinderPool instances, there will be many uncontrollable problems. BinderPool deals with Service. BinderPool actually only provides two interfaces to the client, one is getInstance to obtain instances, and the other is queryBinder for binder distribution.

When getInstance, if the instance has not been initialized, a new instance will be created immediately, and the service connection will also be started. After connecting to the service, the connection state will be maintained, so it is necessary to monitor the death of Binder.

After the connection is successful, the Binder instance corresponding to the IBinderPool is obtained. The concrete implementation of this Binder class is still in BinderPool.

The next time you want to add AIDL to a new module, it is very simple. Modify the BinderPool to add a code, and then add a case branch to the queryBinder for instantiation.

Guess you like

Origin blog.csdn.net/ahou2468/article/details/122647327