banner
jzman

jzman

Coding、思考、自觉。
github

Detailed Explanation of Using Android AIDL

AIDL (Android Interface Definition Language) can be used to define the programming interface for inter-process communication (IPC) between client and server processes. In Android, processes cannot share memory (user space), and communication between different processes is generally handled using AIDL.

The main process involves defining the AIDL interface in a .aidl file and adding it to the src directory of the application project. After creation, rebuilding the project will cause the Android SDK tools to automatically generate an IBinder interface based on the .aidl file. The specific business object implements this interface, and this specific business object is also an IBinder object. When binding the service, it will return the specific communication object (local or proxy) based on the actual situation. Finally, the client binds to this service, and then it can call the methods in IBinder for inter-process communication (IPC). The following aspects will be covered in learning how to use AIDL:

  1. Creating .aidl files
  2. Specific business object implementation based on the generated .aidl file interface
  3. Exposing the interface to the client
  4. Remote calling from the client
  5. Validating AIDL

Creating .aidl Files#

In AIDL, interfaces can be declared with one or more methods that can have parameters and return values. The parameters and return values can be of any type. The data types supported in AIDL are as follows:

  1. 8 primitive data types in Java: byte, short, int, long, float, double, boolean, char
  2. In addition, it supports String, CharSequence, List, Map
  3. Custom data types

If the parameter or return value type in the business method is List or Map:

All elements in the List must be AIDL-supported data types, other AIDL-generated interfaces, or user-defined parcelable types. You can choose to use List as a "generic" class (e.g., List). The specific class actually received on the other end is always ArrayList, but the generated method uses the List interface.

All elements in the Map must be AIDL-supported data types, other AIDL-generated interfaces, or user-defined parcelable types. Generic Maps (like Map<String, Integer>) are not supported. The specific class actually received on the other end is always HashMap, but the generated method uses the Map interface.

Of course, AIDL also supports custom data types, which will be introduced later.

First, create a .aidl file in the src directory of the project, as shown in the figure below:

image

Then, add specific business methods to the .aidl file, with the content as follows:

// IPersonAidlInterface.aidl
package com.manu.aidldemo;
// Declare any non-default types here with import statements
interface IPersonAidlInterface {
    // Specific business
    void setName(String name);
    void setAge(int age);
    String getInfo();

    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

Then, rebuild the project, and the Android SDK tools will generate the corresponding .java interface file with the same name as the .aidl file in the appropriate directory, as shown below:

image

Specific Business Object Implementation Based on the Generated .aidl File Interface#

The previous step only defined basic business operations using AIDL. After rebuilding, a .java file with the same name as the .aidl file will be generated. This generated interface file contains a subclass named Stub, which is an abstract implementation of its parent interface, mainly used to generate all methods in the .aidl file. The Stub class is declared as follows:

// Stub
public static abstract class Stub extends android.os.Binder implements com.manu.aidldemo.IPersonAidlInterface

Clearly, Stub implements the local interface and inherits from the Binder object. Given that the Binder object is supported at the system level, the Stub object has the capability to transmit data remotely. When generating the Stub object, the asInterface method will be called, as follows:

// asInterface
public static com.manu.aidldemo.IPersonAidlInterface asInterface(android.os.IBinder obj){
    
    if ((obj==null)) {
        return null;
    }
    
    // Check if the Binder object is an implementation of the local interface
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    
    if (((iin!=null)&&(iin instanceof com.manu.aidldemo.IPersonAidlInterface))) {
        return ((com.manu.aidldemo.IPersonAidlInterface)iin);
    }
    
    return new com.manu.aidldemo.IPersonAidlInterface.Stub.Proxy(obj);
}

The asInterface method is called when creating the Stub, and its main function is to check whether the Binder object is an implementation of the local interface. It determines whether to use the proxy object based on the return value of the queryLocalInterface() method. This checking process should be supported by the system at a low level. If the return is null, a proxy object for the Stub is created; otherwise, the local object is used to transmit data. Next, let's look at why Binder has the capability for remote communication, as Stub inherits from the Binder class, which is defined as follows:

// Binder
public class Binder implements IBinder {
    //...
}

Here is the official description of the IBinder interface:

The fundamental interface for remote objects, the core part of a lightweight remote procedure call mechanism, designed for high performance during intra-process and inter-process calls. This interface describes the abstract protocol for interacting with remote objects. Do not implement this interface directly; instead, extend from Binder.

From this, we know that Binder implements the IBinder interface, meaning that Binder has the capability for remote communication. When communication occurs between different processes (remote), it is evident that the proxy object of Stub is used. The specific business logic is handled in this proxy class, as shown in the following method:

// Specific business
@Override 
public void setName(java.lang.String name) throws android.os.RemoteException{
    // Serialize the data
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        _data.writeString(name);
        // This method will ultimately call the onTransact method
        mRemote.transact(Stub.TRANSACTION_setName, _data, _reply, 0);
        _reply.readException();
    }
    finally {
        _reply.recycle();
        _data.recycle();
    }
}

Here, the data is mainly serialized, and under the support of the system for cross-process communication, the onTransact() method is ultimately called. Below is the onTransact() method:

@Override 
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException{
    switch (code){
        case INTERFACE_TRANSACTION:
        {
            reply.writeString(DESCRIPTOR);
            return true;
        }
        case TRANSACTION_setName:
        {
            //...
            // Ultimately calls the business method in Stub
            this.setName(_arg0);
            //...
        }
    }
}

Clearly, this method is called when the system callbacks to the developer, and the returned code is a constant. During cross-process communication, each specific service (method) corresponds to a number, and the corresponding service (business) is executed based on this number. Here, we mention the specific business to be executed. Since Stub is an abstract class, it must provide a concrete implementation class to complete the specific business. Below is the implementation of this specific business class:

/**
 * Created by jzman
 * Powered by 2018/3/8 0008.
 */
public class IPersonImpl extends IPersonAidlInterface.Stub {
    private String name;
    private int age;
    
    @Override
    public void setName(String name) throws RemoteException {
        this.name = name;
    }

    @Override
    public void setAge(int age) throws RemoteException {
        this.age = age;
    }

    @Override
    public String getInfo() throws RemoteException {
        return "My name is "+name+", age is "+age+"!";
    }

    @Override
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

    }
}

This class is the specific business class provided to the outside, and its instance is also a Binder object.

Exposing the Interface to the Client#

Create a Service to provide specific business externally, as follows:

// Service
public class PersonService extends Service {
    public PersonService() {
    }

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

When the external bindService() method is called to bind the service, the onBind() method will be invoked to return the IBinder object, which is also the specific business object. The onBind() method returns the specific business object, and both are unified. In addition, the created Service must be declared in the AndroidManifest.xml file, as follows:

<service
    android:name=".PersonService"
    android:enabled="true"
    android:exported="true"
    android:process=":remote">
</service>

The process keyword indicates that a separate process is opened for this service. The remote can be any name, indicating the process name. The ":" will add a new name as the name of the new process in the main process (the process name is the package name), for example, com.manu.study will become com.manu.study.

Remote Calling from the Client#

Through the above steps, the service has been set up and is running in a separate process. The following is the specific calling from the client, with the implementation as follows:

// Client
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    private IPersonAidlInterface iPersonAidlInterface;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void bindServiceClick(View view) {
        Log.i(TAG,"Binding service...");
        Intent intent = new Intent(this,PersonService.class);
        // Automatically create the service when binding
        bindService(intent,conn, Context.BIND_AUTO_CREATE);
    }

    public void unbindServiceClick(View view) {
        Log.i(TAG,"Unbinding service...");
        unbindService(conn);
    }

    public void callRemoteClick(View view) {
        Log.i(TAG,"Calling specific service remotely...");
        try {
            iPersonAidlInterface.setName("Tom");
            iPersonAidlInterface.setAge(10);
            String info = iPersonAidlInterface.getInfo();
            System.out.println("This is the remote service information: "+info);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // Return the local object or its proxy object based on the actual situation
            iPersonAidlInterface = IPersonAidlInterface.Stub.asInterface(service);
            System.out.println("Specific business object: "+iPersonAidlInterface);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            // Called when the Service is unexpectedly disconnected
        }
    };
}

The above code is the process of the client calling the specific service.

Validating AIDL#

After the previous steps, the server and client have been completed. Now let's validate whether we can call the specific business, which can be divided into two situations:

Same Process#

When creating the Service, do not use the process keyword in the AndroidManifest.xml file to open a separate process. At this point, the service process is by default in the same process as the client, and the result is as follows:

image

Different Processes#

When creating the Service, use the process keyword in the AndroidManifest.xml file to open a separate process, as mentioned earlier. At this point, the service process and the client process are in different processes, and the result is as follows:

image

Clearly, if the service and client are in different processes, which is often referred to as inter-process communication, it is specifically completed by the proxy object of the IBinder object; conversely, the local object is used, which is specifically completed by the object generated by the business class implementing AIDL.

Custom Types#

How to use custom types in AIDL is as follows:

Creating Custom Types#

Custom types transmit an entity object, and this entity class must implement the Parcelable interface, as follows:

// Custom type
public class Work implements Parcelable {
    private String title;
    private String content;
    // getter, setter, Parcelable omitted
}

Declaring Custom Types#

Create a .aidl file to declare the type defined earlier. Note that this is different from the specific business .aidl file, declared as follows:

// Declare custom types in the .aidl file
package com.manu.aidldemo;
parcelable Work;

In the .aidl file defining specific business, define the business related to custom types, as follows:

// Declare any non-default types here with import statements
import com.manu.aidldemo.Work; // Note
interface IPersonAidlInterface {
    // Define business related to custom types
    Work getWorkInfo();
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

Note: Import the custom type; otherwise, it will not be recognized.

Overriding Business Entity Class#

Since a specific business has been added, namely getWorkInfo(), the specific business class needs to implement this specific business, as follows:

// Add business related to custom types
@Override
public Work getWorkInfo() throws RemoteException {
    Work work = new Work();
    work.setTitle("Learning AIDL");
    work.setContent("Android developer");
    return work;
}

Remote Calling#

After successfully binding the Service, you will get the IBinder object, and then you can call it by obtaining the IPersonAidlInterface object through asInterface(), as follows:

public void callRemoteClick(View view) {
    Log.i(TAG,"Calling specific service remotely...");
    try {
        // Remote call
        Work work = iPersonAidlInterface.getWorkInfo();
        System.out.println("This is the remote service information: title="+work.getTitle()+", content="+work.getContent());
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

The log running screenshot is as follows:

image

This concludes the introduction to AIDL.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.