Mobile Development 17 min read

Cross-Process Singleton Implementation in Android Using AIDL

The article explains how to turn a traditional Android singleton into a cross‑process object by having the singleton implement an AIDL interface, exposing it through a bound Service, and using Parcelable‑based Binder serialization so multiple processes share the same instance while handling IPC, threading, and data‑type constraints.

Tencent Music Tech Team
Tencent Music Tech Team
Tencent Music Tech Team
Cross-Process Singleton Implementation in Android Using AIDL

Cross-Process Singleton in Android

Singleton pattern, also known as the single-instance pattern, is a common software design pattern. When applying this pattern, the class of the singleton object must guarantee that only one instance exists. Often a system only needs a single global object, which helps coordinate overall system behavior.

However, this design pattern has a limitation: it only works within a single process. In many projects multiple processes are started, causing the originally designed singleton to become two separate singletons across the application. The internal state of the singleton in each process cannot be synchronized, which is the core of the problem (the singleton's behavior is consistent across processes, but its internal state influences the results).

How to Solve

There are many ways to solve the data‑desynchronization problem; two simple approaches are persistence and cross‑process calls.

1 Persistence

Android provides three persistence mechanisms: local files, SharedPreference, and databases.

By persisting data locally, all reads and writes operate on the persisted data, achieving data synchronization.

This solution introduces new issues such as concurrent file writes (data may become corrupted) and increased I/O overhead.

Among the three, local files and SharedPreference can suffer from concurrent write problems, while databases are more reliable. Android also offers ContentProvider to simplify some database operations.

All three methods require additional work: defining storage formats for files, defining field names for SharedPreference, and defining tables, CRUD operations, etc., for databases.

Initially I considered using ContentProvider, but the implementation turned out to be cumbersome and was abandoned.

2 IPC (Inter‑Process Communication)

IPC is well‑suited for this problem and resembles backend RPC (Remote Procedure Call). Android uses AIDL for inter‑process communication.

The implementation steps are not trivial:

Define the AIDL interface.

Implement the methods defined in the AIDL interface.

Create a Service that returns a Binder object implementing the AIDL interface when bound (the callee).

Bind to the Service, obtain the Binder, and invoke methods through it (the caller).

Although not simple, it is manageable. The simplest solution is to wrap each singleton call with an extra layer (actually two layers: one for business logic, one for AIDL to enable cross‑process calls).

When invoking, the wrapper checks whether the current process is the one that owns the singleton; if so, it calls the singleton directly, otherwise it initiates an IPC call.

Add each singleton method to an AIDL file.

Implement the AIDL methods (the cross‑process wrapper).

Add a wrapper layer (if (in singleton process) { call method; } else { make cross‑process call; }).

Modify existing business code to call through the wrapper.

(We do not generate code; we only transport existing code.)

3 End of Story

(╯‵□′)╯︵┻━┻ ... (repeated emoticons expressing frustration)

Why is this happening to me? (crying)

Is there a simpler way?!

4 You Want Simpler?

Review the implementation steps above:

Define AIDL interface.

Implement the AIDL interface.

Implement a Service that returns the Binder.

Bind to the Service and obtain the Binder.

4.1 Simplify the Wrapper Layer

Since we already need to implement the AIDL interface, why not merge the singleton implementation with the AIDL implementation?

In this way, the singleton instance itself becomes a cross‑process transferable object.

When binding, we can return this singleton (Binder) and other processes receive a proxy that can operate on the same singleton.

This eliminates most of the boilerplate wrapper code.

4.2 Simplify Binding Process

The remaining work is:

Define the AIDL interface and let the singleton implement it.

Every usage of the singleton must perform a bind once, then keep the obtained proxy as the singleton instance.

The first step cannot be avoided; the second step involves repetitive code:

Modify the Service implementation to return the AIDL‑implemented singleton.

In onServiceConnected, set the received proxy as the local singleton instance.

If we could transfer all singletons at once, we would reduce multiple bind calls and unify entry/exit points.

When using AIDL, we also deal with Parcelable. Two important methods are writeStrongInterface and readStrongBinder , which handle Binder serialization and deserialization.

Thus we can serialize all singletons with writeStrongInterface , transfer them to another process, and reconstruct them with readStrongBinder .

5 "Word is cheap, show me the code"

GitHub repository (https://github.com/SR1s/AndroidPlayground) contains the basic framework and sample.

5.1 Core Points

Two key points:

Singleton objects implement the AIDL interface to support cross‑process.

Parcelable unifies serialization (Stub) and deserialization (Proxy) of singleton objects.

5.2 Example – Singleton

Assume three singletons:

SingletonA (process A) with SingletonA.aidl and SingletonAImp.java .

SingletonB (process B) with SingletonB.aidl and SingletonBImp.java .

SingletonC (process C) with SingletonC.aidl and SingletonCImp.java .

All expose a static getInstance method. The implementation distinguishes the current process:

public static synchronized SingletonA getInstance() {
    if (ProcessUtils.isProcessA()) {
        if (INSTANCE == null) {
            INSTANCE = new SingletonAImp();
        }
        return INSTANCE;
    } else {
        if (INSTANCE == null) {
            /** Auto‑reconnect */
            Intent intent = new Intent(App.getContext(), ServiceA.class);
            App.getContext().bindService(intent, new InstanceReceiver(), Context.BIND_AUTO_CREATE);
        }
        return INSTANCE;
    }
}

This getInstance may return null, unlike a traditional singleton.

5.3 Example – Service

Each process provides a Service that returns the singleton via a Binder. All services inherit from a common BaseService :

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 {}

5.4 Example – InstanceReceiver

InstanceReceiver implements ServiceConnection . It converts the received Binder to an InstanceTransfer (the AIDL wrapper) and triggers the transfer of the singleton:

@Override
public void onServiceConnected(ComponentName name, IBinder service){
    Log.i(TAG, "[onServiceConnected]" + name);
    try {
        // This call transfers the singleton (proxy) instance
        InstanceTransfer.Stub.asInterface(service).transfer();
    } catch (Exception e) {
        Log.e(TAG, "[onServiceConnected][exception when transfer instance]" + name, e);
    }
}

@Override
public void onServiceDisconnected(ComponentName name) {
    // Handle unexpected disconnection, possibly reconnect
    Log.e(TAG, "[onServiceDisconnected][exception when service disconnected]" + name);
}

The AIDL definition:

interface InstanceTransfer {
    InstanceCarrier transfer();
}

5.5 Example – InstanceCarrier

InstanceCarrier is a Parcelable that serializes and deserializes the singleton Binder:

private static final int PROCESS_A = 1;
private static final int PROCESS_B = 2;
private static final int PROCESS_C = 3;

@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));
    }
}

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
CREATOR = new Creator
() {
    @Override
    public InstanceCarrier createFromParcel(Parcel in) { return new InstanceCarrier(in); }
    @Override
    public InstanceCarrier[] newArray(int size) { return new InstanceCarrier[size]; }
};

Adding a new singleton only requires defining its AIDL, implementing it, adding serialization/deserialization code in InstanceCarrier , and creating a derived Service for the new process.

Existing Issues and Limitations

Data types used inside the singleton must be AIDL‑compatible; simple data can use the system Bundle object.

Method implementations must consider that the execution thread may not be the caller's thread (IPC calls run on a Binder thread). Synchronous calls are fine for return values, but UI‑related logic must be posted to the main thread.

Thread safety: any thread can access the singleton, and cross‑process usage may amplify concurrency problems.

Android IPC limits shared memory to 1 MB; large data transfers should be avoided, and processing should not be time‑consuming to prevent memory exhaustion.

Mobile DevelopmentAndroidIPCSingletonCross-ProcessAIDL
Tencent Music Tech Team
Written by

Tencent Music Tech Team

Public account of Tencent Music's development team, focusing on technology sharing and communication.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.