Thursday, December 25, 2014

Android's alternative IPC approche

I hate Android's Services

To be more precise - I hate asynchronous nature of bindService method. Sometimes it is much more convenient to have synchronous way to get AIDL object.

Did you know that almost each Android's *Manager has its own *ManagerService object?
For example ActivityManager and ActivityManagerService or WindowManager and WindowManagerService, etc.

I was always curious about them. You can get each Manager through Context.getSystemService method and it works synchronously. It would be a waste of time and memory to bind all of them at application startup so they must be bound during getSystemService call. After some digging I found out that they are and it's done through hidden "global context object" which is Binder.


How is Binder connected to AIDL?

Generated AIDL Stub extends Binder and overrides onTransact method, while AIDL Proxy wraps Binder and calls its transact method. Actually Binder is just a "gateway" that passes bytes in one direction, so all function calls are serialized into a byte array, passed through a Binder and than deserialized.

So the problem is only to pass Binder and Parcel can do it. If Parcel can - Bundle can do it either. And here it struck me - Binder can be passed through Intents and ContentProviders.


Fun with Intents

You can also have some fun with Intents. I don't know why someone would start Activity and pass AIDL through Intent but it is possible to pass AIDL object to IntentService and track that task's progress.

But Intents are of no interest to me now.


Fun with ContentProviders (CP)

For a long time I hated CPs and their DB-related interface until I found interesting function - ContentProvider.exec. It has simple interface and can receive/return Bundles. And with bundles you can pass complex objects, for example AIDL objects. And it turns out that CPs are synchronous.

Eureka!!! Here is a way to synchronously "bind" to another process. You can just create AIDL stub and return it through CP.

For example this is my AIDL interface:
interface ICentral {
    int getRequestsCount();
    void sendRandomRequest(long userTime, String inputArg, ICallback callback);
}

And my ContentProvider:
public class CentralProvider extends ContentProvider {

    // ...

    @Override
    public Bundle call(String method, String arg, Bundle extras) {
        if (extras != null) {
            extras.setClassLoader(ParcelableBinder.class.getClassLoader());
        }
        switch (Central.Method.valueOf(method)) {
            case GetServiceBinder:
                Bundle bundle = new Bundle();
                bundle.putParcelable("binder", new ParcelableBinder((IBinder) CentralBinder.self));
                return bundle;
        }
        return super.call(method, arg, extras);
    }
}

Now to "client" side. To simplify things I need a wrapper:
public class Central {

    public static final Uri AUTHORITY = Uri.parse("content://dev.matrix.central/");

    private static ICentral sCentral;

    public static ICentral getProxy() {
        if (sCentral == null || !sCentral.asBinder().pingBinder()) {
            Bundle bundle = BaseApp.self().getContentResolver().call(AUTHORITY, Method.GetServiceBinder.name(), null, null);
            bundle.setClassLoader(ParcelableBinder.class.getClassLoader());
            sCentral = ICentral.Stub.asInterface(ParcelableBinder.getBinder(bundle, "binder"));
        }
        return sCentral;
    }
}

And here is how to use it:
int count = 0;
try {
    count = Central.getProxy().getRequestsCount();
} catch (Exception ex) {
    // ...
}

Good:
  • full AIDL support
  • fully synchronous "binding" to remote process
  • seamless handing and reconnected of dead Binders

Bad:
  • you don't have service "connection" so remote process can be killed at any time unless at least one service is running

Performance (tested on Nexus 5 with Android 5.0):
  • AIDL method invokes - 0.2-0.5 ms
  • Binder pinging to check if it is alive - 0.1-0.3 ms
  • hot "bind" (if remote app is running) - 1-2 ms
  • cold "bind" (if remote app is down) - 50-100 ms

1 comment:

  1. instead of "bundle.putParcelable" use "bundle.putBinder"

    ReplyDelete