The sample code can be found in the demo

1. The AIDL concept

The Android Interface Definition Language (AIDL) is similar to other interface languages (IDL) you may have used. You can use it to define programming interfaces that both clients and services agree on so that they can communicate with each other using interprocess communication (IPC). In Android, one process usually cannot access another process’s memory. Therefore, to communicate, a process needs to decompose its objects into primitives that the operating system can understand and group them into objects that you can manipulate. Writing the code to perform this marshalling operation is tedious, so Android uses AIDL to handle it for you.

AIDL is one of many ways to communicate across processes (IPC). There are also Bundle, file sharing, Messenger, ContentProvider, Socket, and other ways to communicate between processes. AIDL is an interface definition language, just a tool with Binder. Binder is Android’s unique cross-process communication method that requires only one copy, faster and more secure.

The official recommendation is to use Messenger for cross-process communication, but Messenger is a serial way of processing messages from clients. If a large number of messages are sent to the server at the same time, the server can only process them one by one. If there are a large number of concurrent requests, then using Messenger is not appropriate. That’s where the AIDL comes in. In fact, The bottom layer of Messenger is also AIDL, but the system has done a layer of encapsulation, simplify the use of.

2. Use AIDL

2.1 General Process

  1. Create an.aidl file: This file defines the programming interface with the method signature.
  2. Implementing interfaces: The Android SDK tools generate interfaces using the Java programming language, based on your.aidl files. This interface has an internal abstract class called Stub that extends Binder classes and implements methods in the AIDL interface. You must extend the Stub class and implement these methods.
  3. Expose the interface to the client: Implement Service and override onBind() to return an implementation of the Stub class.

2.2 case

2.2.1 Defining the AIDL Interface

The first is to define a good client and server communication AIDL interface, in which the declaration method for client call, server implementation. Create the aidl directory under SRC /main and create the iPersonManager.aidl file

package com.xfhy.allinone.ipc.aidl; import com.xfhy.allinone.ipc.aidl.Person; interface IPersonManager { List<Person> getPersonList(); Boolean addPerson(in Person Person); }Copy the code

This interface is not very different from the way we normally define interfaces. It is important to note that even if Person and IPersonManager are under the same package, we still have to guide the package. This is the rule of AIDL.

Data types supported by AIDL:

Not all data types are available in AIDL files. The following data types are supported:

  • All primitive types in the Java programming language (such as int, long, CHAR, Boolean, and so on)
  • String and CharSequence;
  • List: Only ArrayList is supported, where each element must be supported by AIDL.
  • Map: Only HashMap is supported. Every element in it must be supported by AIDL, including key and value.
  • Parcelable: all objects that implement the Parcelable interface;
  • AIDL: All AIDL interfaces themselves can also be used in AIDL files.

Note:

  • When objects need to be passed, they must implement the Parcelable interface
  • All non-primitive parameters require direction markers indicating where the data is going. Such tags can be in, out, or inout.
    • In: The client flows to the server
    • Out: The server flows to the client
    • Inout: bidirectional flow
  • The default primitive is in, and you should think about what the primitive tag is, because inout is quite expensive.

Define the transfer object:

On Kotlin’s side you need to define the object Person that you want to transfer

class Person(var name: String? = "") : Parcelable {
    constructor(parcel: Parcel) : this(parcel.readString())

    override fun toString(a): String {
        return "Person(name=$name) hashcode = ${hashCode()}"
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
    }

    fun readFromParcel(parcel: Parcel) {
        this.name = parcel.readString()
    }

    override fun describeContents(a): Int {
        return 0
    }

    companion object CREATOR : Parcelable.Creator<Person> {
        override fun createFromParcel(parcel: Parcel): Person {
            return Person(parcel)
        }

        override fun newArray(size: Int): Array<Person? > {return arrayOfNulls(size)
        }
    }


Copy the code

Then you need to declare the Person object in the same directory as aiDL. Create a new person.aidl

package com.xfhy.allinone.ipc.aidl;

parcelable Person;
Copy the code

After completing the rebuild,AS will automatically generate the following code iPersonManager.java:

Note: You cannot use Chinese annotations in AIDL, otherwise you may have problems automatically generating Java code. Oddly enough, I can generate it automatically on macOS, but not on Windows.

package com.xfhy.allinone.ipc.aidl;

public interface IPersonManager extends android.os.IInterface {
    /** * Default implementation for IPersonManager. */
    public static class Default implements com.xfhy.allinone.ipc.aidl.IPersonManager {
        @Override
        public java.util.List<com.xfhy.allinone.ipc.aidl.Person> getPersonList() throws android.os.RemoteException {
            return null;
        }

        @Override
        public boolean addPerson(com.xfhy.allinone.ipc.aidl.Person person) throws android.os.RemoteException {
            return false;
        }

        @Override
        public android.os.IBinder asBinder(a) {
            return null; }}/** * Local-side IPC implementation stub class. */
    public static abstract class Stub extends android.os.Binder implements com.xfhy.allinone.ipc.aidl.IPersonManager {
        private static final java.lang.String DESCRIPTOR = "com.xfhy.allinone.ipc.aidl.IPersonManager";

        /** * Construct the stub at attach it to the interface. */
        public Stub(a) {
            this.attachInterface(this, DESCRIPTOR);
        }

        /** * Cast an IBinder object into an com.xfhy.allinone.ipc.aidl.IPersonManager interface, * generating a proxy if needed. */
        public static com.xfhy.allinone.ipc.aidl.IPersonManager asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if(((iin ! =null) && (iin instanceof com.xfhy.allinone.ipc.aidl.IPersonManager))) {
                return ((com.xfhy.allinone.ipc.aidl.IPersonManager) iin);
            }
            return new com.xfhy.allinone.ipc.aidl.IPersonManager.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder(a) {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            java.lang.String descriptor = DESCRIPTOR;
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_getPersonList: {
                    data.enforceInterface(descriptor);
                    java.util.List<com.xfhy.allinone.ipc.aidl.Person> _result = this.getPersonList();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addPerson: {
                    data.enforceInterface(descriptor);
                    com.xfhy.allinone.ipc.aidl.Person _arg0;
                    if ((0! = data.readInt())) { _arg0 = com.xfhy.allinone.ipc.aidl.Person.CREATOR.createFromParcel(data); }else {
                        _arg0 = null;
                    }
                    boolean _result = this.addPerson(_arg0);
                    reply.writeNoException();
                    reply.writeInt(((_result) ? (1) : (0)));
                    return true;
                }
                default: {
                    return super.onTransact(code, data, reply, flags); }}}private static class Proxy implements com.xfhy.allinone.ipc.aidl.IPersonManager {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder(a) {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor(a) {
                return DESCRIPTOR;
            }

            @Override
            public java.util.List<com.xfhy.allinone.ipc.aidl.Person> getPersonList() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.xfhy.allinone.ipc.aidl.Person> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    boolean _status = mRemote.transact(Stub.TRANSACTION_getPersonList, _data, _reply, 0);
                    if(! _status && getDefaultImpl() ! =null) {
                        return getDefaultImpl().getPersonList();
                    }
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.xfhy.allinone.ipc.aidl.Person.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public boolean addPerson(com.xfhy.allinone.ipc.aidl.Person person) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                boolean _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if((person ! =null)) {
                        _data.writeInt(1);
                        person.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    boolean _status = mRemote.transact(Stub.TRANSACTION_addPerson, _data, _reply, 0);
                    if(! _status && getDefaultImpl() ! =null) {
                        return getDefaultImpl().addPerson(person);
                    }
                    _reply.readException();
                    _result = (0! = _reply.readInt()); }finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            public static com.xfhy.allinone.ipc.aidl.IPersonManager sDefaultImpl;
        }

        static final int TRANSACTION_getPersonList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_addPerson = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

        public static boolean setDefaultImpl(com.xfhy.allinone.ipc.aidl.IPersonManager impl) {
            // Only one user of this interface can use this function
            // at a time. This is a heuristic to detect if two different
            // users in the same process use this function.
            if(Stub.Proxy.sDefaultImpl ! =null) {
                throw new IllegalStateException("setDefaultImpl() called twice");
            }
            if(impl ! =null) {
                Stub.Proxy.sDefaultImpl = impl;
                return true;
            }
            return false;
        }

        public static com.xfhy.allinone.ipc.aidl.IPersonManager getDefaultImpl(a) {
            returnStub.Proxy.sDefaultImpl; }}public java.util.List<com.xfhy.allinone.ipc.aidl.Person> getPersonList() throws android.os.RemoteException;

    public boolean addPerson(com.xfhy.allinone.ipc.aidl.Person person) throws android.os.RemoteException;
}

Copy the code

I think that’s what AIDL does. After writing the AIDL file, THE AS will automatically generate some code for us to communicate with the Server. In fact, these codes can be written by ourselves, but it is a little more troublesome. We use tools when we have tools.

Taking a quick look at the file,IPersonManager is an interface that inherits an Android.os. IInterface, and there is a Default class that implements the IPersonManager by Default. Binder is then extended by an abstract class Stub that inherits Android.os. Binder and implements the IPersonManager interface. Since it inherited from Binder, it must have been used for IPC communications.

  • The asInterface method is used to convert the Binder object on the server to the interface object required by the client. This process is process-specific. If the process is the same, the server Stub object itself is returned, otherwise the encapsulated stub.proxy object is returned
  • OnTransact is a method that runs in the Binder threads on the server side and is handled by the underlying wrapper when the client initiates a remote request. Use code to distinguish the methods that the client requests. Note that if this method returns false, the client request will fail. It can be used for permission control

When the client initiates a remote request,_data writes parameters, then calls the Transact method to initiate an RPC(remote procedure call) request, suspends the current thread, and the onTransact method on the server is called Until the RPC procedure returns, the current thread continues, retrieves the return value (if any) from _reply, and returns the result

2.2.2 Interface implementation on the server

Define a Service and set its process to a new process, separate from the main process, to simulate cross-process access. It needs to implement an interface generated by.aiDL

class RemoteService : Service() {

    private valmPersonList = mutableListOf<Person? > ()private val mBinder: Binder = object : IPersonManager.Stub() {
        override fun getPersonList(a): MutableList<Person? > = mPersonListoverride fun addPerson(person: Person?).: Boolean {
            return mPersonList.add(person)
        }
    }

    override fun onBind(intent: Intent?).: IBinder? {
        return mBinder
    }

    override fun onCreate(a) {
        super.onCreate()
        mPersonList.add(Person("Garen"))
        mPersonList.add(Person("Darius"))}}Copy the code

The implemented iPersonManager.stub is a Binder that needs to be returned via onBind(), which is used by clients to call services across processes.

2.2.3 The Client communicates with the Server

The client side needs to connect to the Service through bindService() to communicate. The client’s onServiceConnected() callback receives binder instances returned by the service’s onBind() method. When a client in the onServiceConnected () callback of the IBinder are received, it must be called YourServiceInterface. The Stub. AsInterface (service), To convert the returned parameter to the YourServiceInterface type.

Because it’s a cross process of imitation, let’s do a thorough imitation of the cross app situation. We need to define an action for bindService().

<service
    android:name=".ipc.aidl.RemoteService"
    android:enabled="true"
    android:exported="true"
    android:process=":other">
    <intent-filter>
        <action android:name="com.xfhy.aidl.Server.Action" />
    </intent-filter>
</service>
Copy the code

After the service is defined, it communicates

class AidlActivity : TitleBarActivity() {

    companion object {
        const val TAG = "xfhy_aidl"
    }

    private var remoteServer: IPersonManager? = null

    private val serviceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName? , service:IBinder?). {
            log(TAG, "onServiceConnected")
            / / in onServiceConnected call IPersonManager. Stub. AsInterface interface type for instance
            // Use this instance to invoke the service on the server side
            remoteServer = IPersonManager.Stub.asInterface(service)
        }

        override fun onServiceDisconnected(name: ComponentName?). {
            log(TAG, "onServiceDisconnected")}}override fun getThisTitle(a): CharSequence {
        return "AIDL"
    }

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_aidl)

        btnConnect.setOnClickListener {
            connectService()
        }
        btnGetPerson.setOnClickListener {
            getPerson()
        }
        btnAddPerson.setOnClickListener {
            addPerson()
        }
    }

    private fun connectService(a) {
        val intent = Intent()
        // Action and package(app package name)
        intent.action = "com.xfhy.aidl.Server.Action"
        intent.setPackage("com.xfhy.allinone")
        val bindServiceResult = bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
        log(TAG, "bindService $bindServiceResult")
        
        // If targetSdk is 30, then you need to handle package visibility in Android 11 for details see: https://developer.android.com/about/versions/11/privacy/package-visibility
    }

    private fun addPerson(a) {
        // When the client calls the server method, it needs to catch the following exceptions:
        / / RemoteException:
        //DeadObjectException: An exception is thrown when the connection is broken;
        //SecurityException: An exception is thrown when an AIDL defined on the client and server conflicts;
        try {
            valaddPersonResult = remoteServer? .addPerson(Person("Galen"))
            log(TAG, "addPerson result = $addPersonResult")}catch (e: RemoteException) {
            e.printStackTrace()
        } catch (e: DeadObjectException) {
            e.printStackTrace()
        } catch (e: SecurityException) {
            e.printStackTrace()
        }
    }

    private fun getPerson(a) {
        valpersonList = remoteServer? .personList log(TAG,"The person list$personList")}override fun onDestroy(a) {
        super.onDestroy()
        // Finally remember unbindService
        unbindService(serviceConnection)
    }

}
Copy the code

The client first needs bindService(), and then calls the IBinder into its onServiceConnected() callback of the ServiceConnection instance. The IBinder is converted into an instance of the interface type defined in aiDL, from which it can be separated from the server Got into correspondence. In the demo above, we called the addPerson and getPerson methods on the server. I tested get, then add, then get, and then watched the output log

The 2020-12-24 12:41:00. 170, 24785-24785 / com. Xfhy. Allinone D/xfhy_aidl: BindService True 2020-12-24 12:41:00.906 24785-24785/com.xfhy.allinone D/xfhy_aidl: OnServiceConnected ected 2020-12-24 12:41:04.253 24785-24785/com.xfhy.allinone D/xfhy_aidl: [person (name=Garen), person (name=Darius)] 2020-12-24 12:41:05.952 24785-24785/com.xfhy.allinone D/xfhy_aidl: AddPerson result = true 2020-12-24 12:41:09.022 24785-24785/com.xfhy.allinone D/xfhy_aidl: addPerson result = true 2020-12-24 12:41:09.022 24785-24785/com.xfhy.allinone D/xfhy_aidl Person list [Person(name=Garen), person (name=Darius), person (name= Galen)]Copy the code

As you can see, on the second get, the data that was added before has been retrieved, so the communication is OK.

It is important to note that these remote methods are called synchronously when the client calls them. Calling them in the main thread may result in ANR, which should be called in the child thread.

The following exceptions may occur when the call is made and must be caught:

  • RemoteException:
  • DeadObjectException: An exception is thrown when the connection is broken;
  • SecurityException: An exception is thrown when an AIDL defined on the client and server conflicts;

2.3 In,out,inout keywords

In the definition of the AIDL interface, we used a keyword in, which is actually a directional tag, which is used to indicate the way data flows. There are also two tags: out and Inout. All non-basic parameters require an orientation tag to indicate the direction of the data. The orientation tag of the basic parameter is default and can only be in.

Write a demo to verify this:

Modify the AIDL interface first and arrange all three methods

interface IPersonManager {
    void addPersonIn(in Person person);
    void addPersonOut(out Person person);
    void addPersonInout(inout Person person);
}
Copy the code

Server-side implementation:

override fun addPersonIn(person: Person?). {
    log(TAG,"Server addPersonIn() person =$person") person? .name ="Modified by addPersonIn"
}

override fun addPersonOut(person: Person?). {
    log(TAG,"Server addPersonOut() person =$person}") person? .name ="Modified by addPersonOut"
}

override fun addPersonInout(person: Person?). {
    log(TAG,"Server addPersonInout() person =$person}") person? .name ="Modified by addPersonInout"
}
Copy the code

Client implementation:

private fun addPersonIn(a) {
    var person = Person("Ice")
    log(TAG, "Before the client addPersonIn() calls person =$person}") remoteServer? .addPersonIn(person) log(TAG,"Person = after the client addPersonIn() call$person}")}private fun addPersonOut(a) {
    var person = Person("Pretty is king")
    log(TAG, "The client addPersonOut() calls before person =$person}") remoteServer? .addPersonOut(person) log(TAG,"Person = after the client addPersonOut() call$person}")}private fun addPersonInout(a) {
    var person = Person("Ike")
    log(TAG, "Before the client addPersonInout() calls person =$person}") remoteServer? .addPersonInout(person) log(TAG,"Person = after the client addPersonInout() call$person}")}Copy the code

The log output is as follows:

Person = person (name= ice) hashCode = 142695478} Server addPersonIn() Person = person (name= ice) hashcode = 142695478} // The client can sense the modification of the server, and the client cannot send data to the server. // The server cannot get the data of the client. AddPersonOut () person = person (name= King) hashCode = 15787831} Server addPersonOut() person = person (name=) hashcode = 231395975} Person = person (name= changed by addPersonOut) hashCode = 15787831} // Inout mode the client can sense changes on the server AddPersonInout () person = person (name= Ike) hashCode = 143615140 Person = person (name= changed by addPersonInout) hashCode = 143615140Copy the code

The above demo makes it easier to understand the meaning of data flow. And we found the following patterns:

  • In allows you to transfer data from the client to the server, while out does not
  • The out mode can transmit data from the server to the client, while the IN mode cannot
  • Regardless of whether the server modifies the passed object data, the client object reference does not change, only the client data changes. Legitimately, cross-process manipulation is a serialization and deserialization approach to data.

2.4 Oneway keyword

Prefacing an AIDL interface method with the oneway keyword is an asynchronous call that does not block the calling thread. When a client calls a method on the server side and does not need to know the return result, asynchronous invocation can improve the execution efficiency of the client.

Validation: I define the AIDL interface method as oneway, add Thread.sleep(2000) to the server aiDL method implementation to block the method call, and then the client calls the method to check the time before and after the method call

private fun addPersonOneway(a) {
    log(TAG, "Oneway start time:${System.currentTimeMillis()}") remoteServer? .addPersonOneway(Person("oneway"))
    log(TAG, "Oneway end time:${System.currentTimeMillis()}")}// Log output
// Oneway start time: 1608858291371
//oneway End time: 1608858291372
Copy the code

As you can see, the client does not block when calling this method.

2.5 Thread Safety

AIDL’s methods are executed in a pool of Binder threads on the server side, so it is possible for multiple clients to connect and manipulate data at the same time. In this case, we need to handle multi-threaded synchronization in the server-side AIDL methods.

Let’s look at which thread the server AIDL method is in:

override fun addPerson(person: Person?).: Boolean {
    log(TAG, "Server addPerson() current thread:${Thread.currentThread().name}")
    return mPersonList.add(person)
}

// Log outputServer addPerson() current thread: Binder:3961 _3
Copy the code

As you can see, it is indeed executed in the non-main thread. That does have multithreaded safety issues, so we need to change the type of mPersonList to CopyOnWriteArrayList to be thread safe. Note that even though the data type here is CopyOnWriteArrayList, it will still be converted to an ArrayList when it is returned to the client. The conversion works because they implement the List interface, and AIDL supports lists.

To verify this, look at the returned mPersonList type on the client side:


/ / the server
private valmPersonList = CopyOnWriteArrayList<Person? > ()override fun getPersonList(a): MutableList<Person? > = mPersonList/ / the client
private fun getPerson(a) {
    valpersonList = remoteServer? .personList personList? .let { log(TAG,"personList ${it::class.java}")}}// Output logs
personList class java.util.ArrayList
Copy the code

This does end up being turned into an ArrayList, and the same goes for ConcurrentHashMap, which I won’t verify here.

2.6 AIDL listener (observer? Two-way communication?)

In the above case, we can only call the server method each time the client calls it and get the result, which is very passive. For example, when the client wants to observe the changes of the server data, just like LiveData, it can tell me when the data changes so that I can do something. Notify the client of changes in server data, which requires a listener.

Because the Listener Listener needs across processes, then here you first need to create an aidl for this Listener callback interface IPersonChangeListener. Aidl.

interface IPersonChangeListener {
    void onPersonDataChanged(in Person person);
}
Copy the code

Note that the data flow mode here is in, and the so-called “server” and “client” are opposite in Binder communication. Our Client can not only send messages as a “Client”, but also receive messages pushed by the Server, thus becoming a “Server”.

With listeners, you also need to add methods to register/unregister listeners in iPersonManager.aidl

interface IPersonManager {
    ......
    void registerListener(IPersonChangeListener listener);
    void unregisterListener(IPersonChangeListener listener);
}
Copy the code

Now we implement the register/unregister method on the server side. Isn’t that easy? Create a List

to hold a collection of Listeners, and loop through the collection to notify listeners when data changes.

Think it over, is this really ok? The IPersonChangeListener needs to be cross-process, so each object sent by the client is serialized and deserialized, and the server receives the object that is not sent by the client at all. The Listener is different, but the Binder is the same. Android provides a RemoteCallbackList using this principle. Specifically used to store a collection of listening interfaces. RemoteCallbackList internally stores the data in an ArrayMap, the key being the binder we transport, and the value being the wrapper of the listening interface.

/ / RemoteCallbackList. Java has cut
public class RemoteCallbackList<E extends IInterface> {
    ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();

    private final class Callback implements IBinder.DeathRecipient {
        final E mCallback;
        finalObject mCookie; Callback(E callback, Object cookie) { mCallback = callback; mCookie = cookie; }}public boolean register(E callback, Object cookie) {
        synchronized (mCallbacks) {
            IBinder binder = callback.asBinder();
            Callback cb = new Callback(callback, cookie);
            mCallbacks.put(binder, cb);
            return true; }}}Copy the code

RemoteCallbackList already does thread synchronization internally, so we don’t need to do additional thread synchronization separately. Now let’s implement the register/unregister method:

private valmListenerList = RemoteCallbackList<IPersonChangeListener? > ()private val mBinder: Binder = object : IPersonManager.Stub() {
    .....
    override fun registerListener(listener: IPersonChangeListener?). {
        mListenerList.register(listener)
    }

    override fun unregisterListener(listener: IPersonChangeListener?). {
        mListenerList.unregister(listener)
    }
}
Copy the code

RemoteCallbackList adds and removes data corresponding to register()/unregister() methods. We then simulate server updates by starting a thread to add a Person every 5 seconds and notifying the observer.

// The person is added every 5 seconds in an infinite loop to notify the observer
private val serviceWorker = Runnable {
    while(! Thread.currentThread().isInterrupted) { Thread.sleep(5000)
        val person = Person("name${Random().nextInt(10000)}")
        log(AidlActivity.TAG, "Server onDataChange() produces person =$person}")
        mPersonList.add(person)
        onDataChange(person)
    }
}
private val mServiceListenerThread = Thread(serviceWorker)

// Data changes -> Notify observer
private fun onDataChange(person: Person?). {
    //1. To use RemoteCallbackList, beginBroadcast() and finishBroadcast() must be called first. They have to come in pairs.
    // Get the number of listeners
    val callbackCount = mListenerList.beginBroadcast()
    for (i in 0 until callbackCount) {
        try {
            FinishBroadcast () is not called when an exception occurs.mListenerList.getBroadcastItem(i)? .onPersonDataChanged(person) }catch (e: RemoteException) {
            e.printStackTrace()
        }
    }
    //3. The final call to finishBroadcast() is necessary
    mListenerList.finishBroadcast()
}

override fun onCreate(a){... mServiceListenerThread.start() }override fun onDestroy(a) {
    super.onDestroy()
    mServiceListenerThread.interrupt()
}

Copy the code

When using RemoteCallbackList, you call beginBroadcast() to get the number of listeners, then getBroadcastItem() to get the specific listener object, then call back, and finally finishBroadcast() to end this BeginBroadcast () and finishBroadcast() must be paired. If beginBroadcast() is invoked, and finishBroadcast() is not invoked, begin will be thrown the next time beginBroadcast() is invoked Broadcast() called while already in a broadcast.

The server implementation is good, the client is easier to do

private val mPersonChangeListener = object : IPersonChangeListener.Stub() {
    override fun onPersonDataChanged(person: Person?). {
        log(TAG, "Client onPersonDataChanged() person =$person}")}}private fun registerListener(a){ remoteServer? .registerListener(mPersonChangeListener) }private fun unregisterListener(a){ remoteServer? .asBinder()? .isBinderAlive? .let { remoteServer? .unregisterListener(mPersonChangeListener) } }Copy the code

Because it is need to cross-process communication, so we need to inherit from IPersonChangeListener. Stub to generate a listener object.

The following logs are generated:

Person = Person(name=name9398) hashcode = 130037351} Client onPersonDataChanged() person = Person(name=name9398) hashcode = 217703225}Copy the code

All right, as expected.

2.7 Death notice of Binder

Server processes can be killed at any time, and the binder needs to be perceived as dead on the client side to do some final cleanup or process reconnection. There are four ways to check whether the server is down.

  1. Call binder’s pingBinder() check and return false to indicate that the remote service failed
  2. Binder’s linkToDeath() registers listeners and receives callbacks when the remote service fails
  3. The ServiceConnection used when binding the Service has an onServiceDisconnected() callback that can be received if the server is disconnected
  4. When a client calls a remote method, it throws a DeadObjectException(RemoteException)

Write a code to verify, on the client side change to the following:

private val mDeathRecipient = object : IBinder.DeathRecipient {
    override fun binderDied(a) {
        // Listen for binder died
        log(TAG, "binder died")
        // Remove the death notificationmService? .unlinkToDeath(this.0)
        mService = null
        // Reconnect
        connectService()
    }
}

private val serviceConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName? , service:IBinder?). {
        this@AidlActivity.mService = service
        log(TAG, "onServiceConnected")

        // Set a death proxy for binderservice? .linkToDeath(mDeathRecipient,0)

        / / in onServiceConnected call IPersonManager. Stub. AsInterface interface type for instance
        // Use this instance to invoke the service on the server side
        mRemoteServer = IPersonManager.Stub.asInterface(service)
    }

    override fun onServiceDisconnected(name: ComponentName?). {
        log(TAG, "onServiceDisconnected")}}Copy the code

After the service is bound, the server process is killed. The following log is generated:

// Connect to bindService true onServiceConnected thread = main; Thread = thread = main // Link bindService true onServiceConnected, Thread = mainCopy the code

It does listen for the moment when the server disconnects. And then it’s ok to reconnect. Note that the binderDied() method is run on the child thread and onServiceDisconnected() is run on the main thread, so be careful if you want to update the UI here.

2.8 Permission Verification

Service is completely exposed. Any app can access the Service and call the Service remotely. This is not safe. We can add custom permissions to the manifest file, and then verify that the client has this permission in the Service.

Define custom permissions:

<permission
    android:name="com.xfhy.allinone.ipc.aidl.ACCESS_PERSON_SERVICE"
    android:protectionLevel="normal" />
Copy the code

The client needs to declare this permission in the manifest file:

<uses-permission android:name="com.xfhy.allinone.ipc.aidl.ACCESS_PERSON_SERVICE"/>
Copy the code

Service verification permission:

override fun onBind(intent: Intent?).: IBinder? {
    val check = checkCallingOrSelfPermission("com.xfhy.allinone.ipc.aidl.ACCESS_PERSON_SERVICE")
    if (check == PackageManager.PERMISSION_DENIED) {
        log(TAG,"No access")
        return null
    }
    log(TAG,"Have access")
    return mBinder
}
Copy the code

Principle 3.

3.1 How does AIDL work?

After we wrote the AIDL file, we automatically had the ability to communicate across processes without doing anything. Thanks to Android The iPersonManager.java file generated by Studio from aiDL (which can be found by double-clicking Shift on iPersonManager.java) encapsulates the logic for cross-process communication (eventually with Binder) ), so the iPersonManager.java file will eventually be packaged into APK so that we can easily communicate across processes.

3.2 Detailed Interpretation

Ipersonmanager.aidl: List getPersonList(); iPersonManager.java: List getPersonList(); Void addPersonIn(in Person Person); :

/* * This file is auto-generated. DO NOT MODIFY. */
package com.xfhy.allinone.ipc.aidl;

public interface IPersonManager extends android.os.IInterface {
    /** * Default implementation for IPersonManager. */
    public static class Default implements com.xfhy.allinone.ipc.aidl.IPersonManager {
        @Override
        public java.util.List<com.xfhy.allinone.ipc.aidl.Person> getPersonList() throws android.os.RemoteException {
            return null;
        }

        @Override
        public void addPersonIn(com.xfhy.allinone.ipc.aidl.Person person) throws android.os.RemoteException {}@Override
        public android.os.IBinder asBinder(a) {
            return null; }}/** * Local-side IPC implementation stub class. */
    public static abstract class Stub extends android.os.Binder implements com.xfhy.allinone.ipc.aidl.IPersonManager {
        private static final java.lang.String DESCRIPTOR = "com.xfhy.allinone.ipc.aidl.IPersonManager";

        /** * Construct the stub at attach it to the interface. */
        public Stub(a) {
            this.attachInterface(this, DESCRIPTOR);
        }

        /** * Cast an IBinder object into an com.xfhy.allinone.ipc.aidl.IPersonManager interface, * generating a proxy if needed. */
        public static com.xfhy.allinone.ipc.aidl.IPersonManager asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if(((iin ! =null) && (iin instanceof com.xfhy.allinone.ipc.aidl.IPersonManager))) {
                return ((com.xfhy.allinone.ipc.aidl.IPersonManager) iin);
            }
            return new com.xfhy.allinone.ipc.aidl.IPersonManager.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder(a) {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            java.lang.String descriptor = DESCRIPTOR;
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_getPersonList: {
                    data.enforceInterface(descriptor);
                    java.util.List<com.xfhy.allinone.ipc.aidl.Person> _result = this.getPersonList();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addPersonIn: {
                    data.enforceInterface(descriptor);
                    com.xfhy.allinone.ipc.aidl.Person _arg0;
                    if ((0! = data.readInt())) { _arg0 = com.xfhy.allinone.ipc.aidl.Person.CREATOR.createFromParcel(data); }else {
                        _arg0 = null;
                    }
                    this.addPersonIn(_arg0);
                    reply.writeNoException();
                    return true;
                }
                default: {
                    return super.onTransact(code, data, reply, flags); }}}private static class Proxy implements com.xfhy.allinone.ipc.aidl.IPersonManager {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder(a) {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor(a) {
                return DESCRIPTOR;
            }

            @Override
            public java.util.List<com.xfhy.allinone.ipc.aidl.Person> getPersonList() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.xfhy.allinone.ipc.aidl.Person> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    boolean _status = mRemote.transact(Stub.TRANSACTION_getPersonList, _data, _reply, 0);
                    if(! _status && getDefaultImpl() ! =null) {
                        return getDefaultImpl().getPersonList();
                    }
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.xfhy.allinone.ipc.aidl.Person.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public void addPersonIn(com.xfhy.allinone.ipc.aidl.Person person) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if((person ! =null)) {
                        _data.writeInt(1);
                        person.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    boolean _status = mRemote.transact(Stub.TRANSACTION_addPersonIn, _data, _reply, 0);
                    if(! _status && getDefaultImpl() ! =null) {
                        getDefaultImpl().addPersonIn(person);
                        return;
                    }
                    _reply.readException();
                } finally{ _reply.recycle(); _data.recycle(); }}public static com.xfhy.allinone.ipc.aidl.IPersonManager sDefaultImpl;
        }

        static final int TRANSACTION_getPersonList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_addPersonIn = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

        public static boolean setDefaultImpl(com.xfhy.allinone.ipc.aidl.IPersonManager impl) {
            // Only one user of this interface can use this function
            // at a time. This is a heuristic to detect if two different
            // users in the same process use this function.
            if(Stub.Proxy.sDefaultImpl ! =null) {
                throw new IllegalStateException("setDefaultImpl() called twice");
            }
            if(impl ! =null) {
                Stub.Proxy.sDefaultImpl = impl;
                return true;
            }
            return false;
        }

        public static com.xfhy.allinone.ipc.aidl.IPersonManager getDefaultImpl(a) {
            returnStub.Proxy.sDefaultImpl; }}public java.util.List<com.xfhy.allinone.ipc.aidl.Person> getPersonList() throws android.os.RemoteException;

    public void addPersonIn(com.xfhy.allinone.ipc.aidl.Person person) throws android.os.RemoteException;
}
Copy the code

This code looks very long. First, IPersonManager is an interface, and then it inherits from the IInterface interface. The IInterface interface is the base class of Binder interfaces. Interfaces transmitted through Binder must inherit from IInterface. The methods in it are the two methods we declared in the AIDL file. The two integer ids are then used to identify which method is requested by the client during transact. The iPersonManager.stub inherits from Binder and implements the IPersonManager interface. When both client and server are in the same process, method calls do not go through transact procedures across processes. When both are in different processes, method calls need to go through Transact procedures. This logic is done by Stub’s internal Proxy.

The Default class is just the Default implementation of IPersonManager, so don’t worry about it.

Let’s examine the Stub class in isolation

IPersonManager.Stub

public static abstract class Stub extends android.os.Binder implements com.xfhy.allinone.ipc.aidl.IPersonManager {
    private static final java.lang.String DESCRIPTOR = "com.xfhy.allinone.ipc.aidl.IPersonManager";

    /** * Construct the stub at attach it to the interface. */
    public Stub(a) {
        this.attachInterface(this, DESCRIPTOR);
    }
    
    /** * Cast an IBinder object into an com.xfhy.allinone.ipc.aidl.IPersonManager interface, * generating a proxy if needed. */
    public static com.xfhy.allinone.ipc.aidl.IPersonManager asInterface(android.os.IBinder obj) {
        if ((obj == null)) {
            return null;
        }
        android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
        if(((iin ! =null) && (iin instanceof com.xfhy.allinone.ipc.aidl.IPersonManager))) {
            return ((com.xfhy.allinone.ipc.aidl.IPersonManager) iin);
        }
        return newcom.xfhy.allinone.ipc.aidl.IPersonManager.Stub.Proxy(obj); }}Copy the code
  • A DESCRIPTOR is a unique identifier for a Binder, usually represented by the class name of the current Binder.
  • AttachInterface () is to convert a Binder object into an AIDL interface type object required by the client. If you need to cross processes, encapsulate a stub.proxy object and return it. If cross-process is not required, simply return the Stub on the Service side.

Then look at the rest of the Stub methods:

@Override
public android.os.IBinder asBinder(a) {
    return this;
}

@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
    java.lang.String descriptor = DESCRIPTOR;
    switch (code) {
        case INTERFACE_TRANSACTION: {
            reply.writeString(descriptor);
            return true;
        }
        case TRANSACTION_getPersonList: {
            data.enforceInterface(descriptor);
            java.util.List<com.xfhy.allinone.ipc.aidl.Person> _result = this.getPersonList();
            reply.writeNoException();
            reply.writeTypedList(_result);
            return true;
        }
        case TRANSACTION_addPersonIn: {
            data.enforceInterface(descriptor);
            // Serialize a Person from data, then call addPersonIn() to add the Person
            com.xfhy.allinone.ipc.aidl.Person _arg0;
            if ((0! = data.readInt())) { _arg0 = com.xfhy.allinone.ipc.aidl.Person.CREATOR.createFromParcel(data); }else {
                _arg0 = null;
            }
            this.addPersonIn(_arg0);
            reply.writeNoException();
            return true;
        }
        default: {
            return super.onTransact(code, data, reply, flags); }}}Copy the code
  • AsBinder () returns the current Binder object
  • The onTransact() method is run in the server’s thread pool. This may look like a call within the same process, but it actually involves cross-process communication. When a client requests a server across processes, the remote request is processed by this method after being encapsulated at the bottom of the system. The server uses code to determine what the target method of the client request is, retrieves the parameters required by the target method from data, and then executes the target method. When the target method completes execution, write the return value to reply.

IPersonManager.Stub.Proxy

 private static class Proxy implements com.xfhy.allinone.ipc.aidl.IPersonManager {
    private android.os.IBinder mRemote;

    Proxy(android.os.IBinder remote) {
        mRemote = remote;
    }

    @Override
    public android.os.IBinder asBinder(a) {
        return mRemote;
    }

    public java.lang.String getInterfaceDescriptor(a) {
        returnDESCRIPTOR; }}Copy the code

The mRemote here is the Binder for remote requests. This Proxy Proxy is used by the client to make remote calls if you need to cross process. How does calling a method actually work

static final int TRANSACTION_getPersonList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addPersonIn = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

@Override
public java.util.List<com.xfhy.allinone.ipc.aidl.Person> getPersonList() throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    java.util.List<com.xfhy.allinone.ipc.aidl.Person> _result;
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        boolean _status = mRemote.transact(Stub.TRANSACTION_getPersonList, _data, _reply, 0);
        if(! _status && getDefaultImpl() ! =null) {
            return getDefaultImpl().getPersonList();
        }
        _reply.readException();
        _result = _reply.createTypedArrayList(com.xfhy.allinone.ipc.aidl.Person.CREATOR);
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    return _result;
}

@Override
public void addPersonIn(com.xfhy.allinone.ipc.aidl.Person person) throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        if((person ! =null)) {
            _data.writeInt(1);
            person.writeToParcel(_data, 0);
        } else {
            _data.writeInt(0);
        }
        boolean _status = mRemote.transact(Stub.TRANSACTION_addPersonIn, _data, _reply, 0);
        if(! _status && getDefaultImpl() ! =null) {
            getDefaultImpl().addPersonIn(person);
            return;
        }
        _reply.readException();
    } finally{ _reply.recycle(); _data.recycle(); }}Copy the code
  • Both methods run on the client side, and when the client calls the method, it first creates an input Parcel object that the method needs_data, the output Parcel object_replyAnd the return value, if any. Then write the input parameter to the client method_dataInside, by way of serialization. The transact method is then called to initiate the RPC remote call, the current thread is suspended, and the onTransact method on the server side is called until the RPC procedure returns and the current thread can resume execution. Then write the return value of the method to_reply, also through serialization, and finally return_replyData in.
  • A Parcel object is used to transfer data between the client and server, and only serializable data can be transferred

That’s all there is to the application layer. If you dig deeper, you’ll get to the bottom of the Binder mechanism. I’m not going to do the analysis here.

The following chart summarizes the process (from Android Development Art Exploration):

Because the iPersonManager.java file above is automatically generated, it is very regular. Binders can be customized even without AIDL to enable cross-process communication.

data

  • Text repository
  • Android Interface Definition Language (AIDL)
  • How AIDL works in Android
  • Do you really understand in, out, inout in AIDL?
  • AIDL (1)
  • RemoteCallbackList
  • Android: Learn AIDL in one Article
  • Android development art exploration