Android 接口定义语言 (AIDL)

Android 接口定义语言 (AIDL) 与其他 IDL 类似:您可以通过它定义客户端和服务商定的编程接口,以便使用进程间通信 (IPC) 进行相互通信。

在 Android 上,一个进程通常无法访问另一个进程的内存。因此,为进行通信,进程需将其对象分解成可供操作系统理解的原语,并将其编组为可供您操作的对象。编写执行该编组操作的代码较为繁琐,因此 Android 会使用 AIDL 为您处理此问题。

注意:只有在需要不同应用的客户端通过 IPC 方式访问服务,并且希望在服务中进行多线程处理时,您才有必要使用 AIDL。如果您无需跨不同应用执行并发 IPC,请通过实现 Binder 来创建接口。如果您想执行 IPC,但不需要处理多线程,请使用 Messenger 实现接口。无论如何,请务必先了解绑定服务,然后再实现 AIDL。

在开始设计 AIDL 接口之前,请注意,AIDL 接口的调用是直接函数调用。不要假设发生调用的线程。实际情况的差异取决于调用是来自本地进程中的线程,还是远程进程中的线程:

  • 从本地进程发出的调用在进行调用的同一线程中执行。如果该线程是您的主界面线程,则该线程会继续在 AIDL 接口中执行。如果该线程是其他线程,则其便是在服务中执行您代码的线程。因此,只有在本地线程访问服务时,您才能完全控制哪些线程在服务中执行。但如果是这种情况,请完全不要使用 AIDL;而是通过实现 Binder 来创建接口。
  • 来自远程进程的调用分派自平台在您自己的进程内维护的线程池。为来自未知线程的多次并发传入调用做好准备。换言之,AIDL 接口的实现必须基于完全的线程安全。从同一远程对象上的一个线程发出的调用会按顺序到达接收器端。
  • oneway 关键字用于修改远程调用的行为。使用此关键字后,远程调用不会屏蔽。它发送交易数据并立即返回。 最终接收该数据时,接口的实现会将其视为来自 Binder 线程池的常规调用(普通的远程调用)。如果 oneway 用于本地调用,则不会有任何影响,调用仍是同步调用。

定义 AIDL 接口

使用 Java 编程语言语法在 .aidl 文件中定义 AIDL 接口,然后将其保存到托管服务的应用以及绑定到该服务的任何其他应用的源代码中的 src/ 目录中。

在构建每个包含 .aidl 文件的应用时,Android SDK 工具会生成基于该 .aidl 文件的 IBinder 接口,并将其保存到项目的 gen/ 目录中。服务必须视情况实现 IBinder 接口。然后,客户端应用可以绑定到该服务,并调用 IBinder 中的方法来执行 IPC。

如需使用 AIDL 创建绑定服务,请按照以下部分所述的步骤操作:

  1. 创建 .aidl 文件

    此文件定义带有方法签名的编程接口。

  2. 实现接口

    Android SDK 工具会基于您的 .aidl 文件,使用 Java 编程语言生成接口。此接口有一个名为 Stub 的内部抽象类,用于扩展 Binder 并实现 AIDL 接口中的方法。您必须扩展 Stub 类并实现这些方法。

  3. 向客户端公开接口

    实现 Service 并替换 onBind() 以返回 Stub 类的实现。

注意:如果您在首次发布 AIDL 接口后对其进行更改,则每次更改必须保持向后兼容性,以免中断其他应用使用您的服务。也就是说,因为必须将您的 .aidl 文件复制到其他应用,才能让这些应用访问您的服务的接口,因此您必须保留对原始接口的支持。

创建 .aidl 文件

AIDL 使用简单语法,使您能通过可带参数和返回值的一个或多个方法来声明接口。参数和返回值可以是任意类型,甚至可以是其他 AIDL 生成的接口。

您必须使用 Java 编程语言构造 .aidl 文件。每个 .aidl 文件都必须定义单个接口,并且只需包含接口声明和方法签名。

默认情况下,AIDL 支持下列数据类型:

  • Java 编程语言中的所有原语类型(如 intlongcharboolean 等)
  • 任何类型的数组,例如 int[]MyParcelable[]
  • String
  • CharSequence
  • List

    List 中的所有元素都必须属于此列表支持的数据类型,或者属于您声明的其他由 AIDL 生成的接口或 Parcelable 类型。List 可选择用作参数化类型类,例如 List<String>。尽管生成的方法旨在使用 List 接口,但另一方实际接收的具体类始终是 ArrayList

  • Map

    Map 中的所有元素都必须是以上列表中支持的数据类型,或者您声明的由 AIDL 生成的其他接口或 Parcelable 类型。不支持参数化类型映射(例如 Map<String,Integer> 形式的映射)。尽管生成的方法旨在使用 Map 接口,但另一方实际接收的具体类始终是 HashMap。考虑使用 Bundle 代替 Map

您必须为之前未列出的每个附加类型加入一个 import 语句,即使这些类型是在与您的接口相同的软件包中定义。

定义服务接口时,请注意:

  • 方法可带零个或多个参数,返回值或空值。
  • 所有非原语参数都需要指示数据走向的方向标记:inoutinout(见以下示例)。

    原语、StringIBinder 和 AIDL 生成的接口默认为 in,不能是其他方向。

    注意:请将方向限定为真正需要的方向,因为编组参数的开销较大。

  • .aidl 文件中包括的所有代码注释都包含在生成的 IBinder 接口中,import 和 package 语句之前的注释除外。
  • 字符串和 int 常量可以在 AIDL 接口(例如 const int VERSION = 1;)中定义。
  • 方法调用由 transact() 代码分派,该代码通常基于接口中的方法索引。由于这会增加版本控制的难度,因此您可以向方法手动配置事务代码:void method() = 10;
  • 必须使用 @nullable 为可为 null 的参数和返回类型进行注解。

以下是一个示例 .aidl 文件:

// IRemoteService.aidl
package com.example.android;

// Declare any non-default types here with import statements.

/** Example service interface */
interface IRemoteService {
    /** Request the process ID of this service. */
    int getPid();

    /** 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);
}

.aidl 文件保存在项目的 src/ 目录中。当您构建应用时,SDK 工具会在项目的 gen/ 目录中生成 IBinder 接口文件。生成的文件的名称与 .aidl 文件的名称一致,只是使用了 .java 扩展名。例如,IRemoteService.aidl 的结果为 IRemoteService.java

如果您使用 Android Studio,增量构建几乎会立即生成 binder 类。如果您不使用 Android Studio,Gradle 工具会在您下次构建应用时生成 binder 类。在编写完 .aidl 文件后,请立即使用 gradle assembleDebuggradle assembleRelease 构建项目,以便您的代码能够链接到生成的类。

实现接口

当您构建应用时,Android SDK 工具会生成一个以 .aidl 文件命名的 .java 接口文件。生成的接口包含一个名为 Stub 的子类,该子类是其父接口(例如 YourInterface.Stub)的抽象实现,并且会声明 .aidl 文件中的所有方法。

注意Stub 还定义了几个辅助方法,其中最值得注意的是 asInterface(),该方法会接收 IBinder(通常是传递给客户端 onServiceConnected() 回调方法的参数),并返回存根接口的实例。如需详细了解如何进行此类转换,请参阅调用 IPC 方法部分。

如需实现从 .aidl 生成的接口,请扩展生成的 Binder 接口(例如 YourInterface.Stub),并实现从 .aidl 文件继承的方法。

以下是一个使用匿名实例实现名为 IRemoteService 的接口(由前面的 IRemoteService.aidl 示例定义)的示例:

Kotlin

private val binder = object : IRemoteService.Stub() {

    override fun getPid(): Int =
            Process.myPid()

    override fun basicTypes(
            anInt: Int,
            aLong: Long,
            aBoolean: Boolean,
            aFloat: Float,
            aDouble: Double,
            aString: String
    ) {
        // Does nothing.
    }
}

Java

private final IRemoteService.Stub binder = new IRemoteService.Stub() {
    public int getPid(){
        return Process.myPid();
    }
    public void basicTypes(int anInt, long aLong, boolean aBoolean,
        float aFloat, double aDouble, String aString) {
        // Does nothing.
    }
};

现在,binderStub 类的一个实例(一个 Binder),用于定义服务的 IPC 接口。在下一步中,我们会向客户端公开此实例,以便客户端能与服务进行交互。

实现 AIDL 接口时,请注意几条规则:

  • 传入的调用不一定会在主线程上执行,因此您需要从一开始就考虑好多线程,并正确构建线程安全服务。
  • 默认情况下,IPC 调用是同步调用。如果您知道服务需要几毫秒以上的时间才能完成请求,请勿从 activity 的主线程调用该服务。这可能会导致应用挂起,进而导致 Android 显示“应用无响应”对话框。从客户端的单独线程中调用它。
  • 只有 Parcel.writeException() 参考文档下列出的异常类型会发送回调用方。

向客户端公开该接口

在为服务实现接口后,您需要向客户端公开该接口,以便客户端进行绑定。如需为您的服务公开该接口,请扩展 Service 并实现 onBind(),从而返回实现生成的 Stub 的类实例(如前文所述)。以下是一个向客户端公开 IRemoteService 示例接口的服务示例。

Kotlin

class RemoteService : Service() {

    override fun onCreate() {
        super.onCreate()
    }

    override fun onBind(intent: Intent): IBinder {
        // Return the interface.
        return binder
    }


    private val binder = object : IRemoteService.Stub() {
        override fun getPid(): Int {
            return Process.myPid()
        }

        override fun basicTypes(
                anInt: Int,
                aLong: Long,
                aBoolean: Boolean,
                aFloat: Float,
                aDouble: Double,
                aString: String
        ) {
            // Does nothing.
        }
    }
}

Java

public class RemoteService extends Service {
    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // Return the interface.
        return binder;
    }

    private final IRemoteService.Stub binder = new IRemoteService.Stub() {
        public int getPid(){
            return Process.myPid();
        }
        public void basicTypes(int anInt, long aLong, boolean aBoolean,
            float aFloat, double aDouble, String aString) {
            // Does nothing.
        }
    };
}

现在,当客户端(如 activity)调用 bindService() 以连接到此服务时,客户端的 onServiceConnected() 回调会收到服务的 onBind() 方法返回的 binder 实例。

客户端还必须有权访问接口类。因此,如果客户端和服务位于不同的应用中,则客户端应用的 src/ 目录中必须包含 .aidl 文件的副本,该文件会生成 android.os.Binder 接口,为客户端提供对 AIDL 方法的访问权限。

当客户端在 onServiceConnected() 回调中收到 IBinder 时,必须调用 YourServiceInterface.Stub.asInterface(service) 以将返回的参数转换为 YourServiceInterface 类型:

Kotlin

var iRemoteService: IRemoteService? = null

val mConnection = object : ServiceConnection {

    // Called when the connection with the service is established.
    override fun onServiceConnected(className: ComponentName, service: IBinder) {
        // Following the preceding example for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service.
        iRemoteService = IRemoteService.Stub.asInterface(service)
    }

    // Called when the connection with the service disconnects unexpectedly.
    override fun onServiceDisconnected(className: ComponentName) {
        Log.e(TAG, "Service has unexpectedly disconnected")
        iRemoteService = null
    }
}

Java

IRemoteService iRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established.
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the preceding example for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service.
        iRemoteService = IRemoteService.Stub.asInterface(service);
    }

    // Called when the connection with the service disconnects unexpectedly.
    public void onServiceDisconnected(ComponentName className) {
        Log.e(TAG, "Service has unexpectedly disconnected");
        iRemoteService = null;
    }
};

如需查看更多示例代码,请参阅 ApiDemos 中的 RemoteService.java 类。

通过 IPC 传递对象

在 Android 10(API 级别 29 或更高版本)中,您可以直接在 AIDL 中定义 Parcelable 对象。此处还支持作为 AIDL 接口参数和其他 Parcelable 的类型。这可避免手动编写编组代码和自定义类的额外工作。不过,这也会创建一个裸结构体。如果需要自定义访问器或其他功能,请改为实现 Parcelable

package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect {
    int left;
    int top;
    int right;
    int bottom;
}

上述代码示例会自动生成包含整数字段 lefttoprightbottom 的 Java 类。系统会自动实现所有相关的封装容器代码,并且无需添加任何实现即可直接使用该对象。

您还可以通过 IPC 接口,将自定义类从一个进程发送至另一个进程。不过,请确保 IPC 通道的另一端可使用该类的代码,并且该类必须支持 Parcelable 接口。支持 Parcelable 很重要,因为 Android 系统可通过它将对象分解成可编组到各进程的原语。

如需创建支持 Parcelable 的自定义类,请执行以下操作:

  1. 让您的类实现 Parcelable 接口。
  2. 实现 writeToParcel,它会获取对象的当前状态并将其写入 Parcel
  3. 为您的类添加一个名为 CREATOR 的静态字段,该字段是实现 Parcelable.Creator 接口的对象。
  4. 最后,创建一个声明可打包类的 .aidl 文件,如以下 Rect.aidl 文件所示。

    如果您使用的是自定义构建流程,请勿将 .aidl 文件添加到您的 build 中,.aidl 文件与 C 语言中的头文件类似,并未经过编译。

AIDL 会在其生成的代码中使用这些方法和字段来对您的对象进行编组和解组。

例如,以下这个 Rect.aidl 文件可创建一个可打包的 Rect 类:

package android.graphics;

// Declare Rect so AIDL can find it and knows that it implements
// the parcelable protocol.
parcelable Rect;

以下示例展示了 Rect 类如何实现 Parcelable 协议。

Kotlin

import android.os.Parcel
import android.os.Parcelable

class Rect() : Parcelable {
    var left: Int = 0
    var top: Int = 0
    var right: Int = 0
    var bottom: Int = 0

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

        override fun newArray(size: Int): Array<Rect?> {
            return Array(size) { null }
        }
    }

    private constructor(inParcel: Parcel) : this() {
        readFromParcel(inParcel)
    }

    override fun writeToParcel(outParcel: Parcel, flags: Int) {
        outParcel.writeInt(left)
        outParcel.writeInt(top)
        outParcel.writeInt(right)
        outParcel.writeInt(bottom)
    }

    private fun readFromParcel(inParcel: Parcel) {
        left = inParcel.readInt()
        top = inParcel.readInt()
        right = inParcel.readInt()
        bottom = inParcel.readInt()
    }

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

Java

import android.os.Parcel;
import android.os.Parcelable;

public final class Rect implements Parcelable {
    public int left;
    public int top;
    public int right;
    public int bottom;

    public static final Parcelable.Creator<Rect> CREATOR = new Parcelable.Creator<Rect>() {
        public Rect createFromParcel(Parcel in) {
            return new Rect(in);
        }

        public Rect[] newArray(int size) {
            return new Rect[size];
        }
    };

    public Rect() {
    }

    private Rect(Parcel in) {
        readFromParcel(in);
    }

    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(left);
        out.writeInt(top);
        out.writeInt(right);
        out.writeInt(bottom);
    }

    public void readFromParcel(Parcel in) {
        left = in.readInt();
        top = in.readInt();
        right = in.readInt();
        bottom = in.readInt();
    }

    public int describeContents() {
        return 0;
    }
}

Rect 类中的编组非常简单。请查看 Parcel 的其他相关方法,了解您可以向 Parcel 写入哪些其他类型的值。

警告:请注意从其他进程接收数据的安全问题。在本例中,RectParcel 读取四个数字,但您需确保:无论调用方目的为何,这些数字均在可接受的值范围内。如需详细了解如何防止应用受到恶意软件侵害,请参阅安全提示

带软件包参数(包含 Parcelable 类型)的方法

如果某个方法接受预计包含 Parcelable 的 Bundle 对象,请务必先通过调用 Bundle.setClassLoader(ClassLoader) 设置 Bundle 的类加载器,然后再尝试从 Bundle 读取。否则,即使您在应用中正确定义了 Parcelable,也会遇到 ClassNotFoundException

例如,请考虑以下示例 .aidl 文件:

// IRectInsideBundle.aidl
package com.example.android;

/** Example service interface */
interface IRectInsideBundle {
    /** Rect parcelable is stored in the bundle with key "rect". */
    void saveRect(in Bundle bundle);
}
如以下实现所示,在读取 Rect 之前,已在 Bundle 中明确设置了 ClassLoader

Kotlin

private val binder = object : IRectInsideBundle.Stub() {
    override fun saveRect(bundle: Bundle) {
      bundle.classLoader = classLoader
      val rect = bundle.getParcelable<Rect>("rect")
      process(rect) // Do more with the parcelable.
    }
}

Java

private final IRectInsideBundle.Stub binder = new IRectInsideBundle.Stub() {
    public void saveRect(Bundle bundle){
        bundle.setClassLoader(getClass().getClassLoader());
        Rect rect = bundle.getParcelable("rect");
        process(rect); // Do more with the parcelable.
    }
};

调用 IPC 方法

如需调用使用 AIDL 定义的远程接口,请在调用类中执行以下步骤:

  1. 在项目的 src/ 目录中添加 .aidl 文件。
  2. 声明一个 IBinder 接口实例,该实例基于 AIDL 生成。
  3. 实现 ServiceConnection
  4. 调用 Context.bindService(),并传入 ServiceConnection 实现。
  5. 在您的 onServiceConnected() 实现中,您会收到一个名为 serviceIBinder 实例。调用 YourInterfaceName.Stub.asInterface((IBinder)service) 以将返回的参数转换为 YourInterface 类型。
  6. 调用您在接口上定义的方法。始终捕获 DeadObjectException 异常,系统会在连接中断时抛出此异常。此外,还应捕获 SecurityException 异常,当 IPC 方法调用中两个进程的 AIDL 定义发生冲突时,系统会抛出此异常。
  7. 如要断开连接,请使用接口实例调用 Context.unbindService()

调用 IPC 服务时,请注意以下几点:

  • 对象是跨进程计数的引用。
  • 您可以将匿名对象作为方法参数发送。

如需详细了解如何绑定到服务,请参阅绑定服务概览

以下示例代码摘自 ApiDemos 项目的远程服务示例,展示了如何调用 AIDL 创建的服务。

Kotlin

private const val BUMP_MSG = 1

class Binding : Activity() {

    /** The primary interface you call on the service.  */
    private var mService: IRemoteService? = null

    /** Another interface you use on the service.  */
    internal var secondaryService: ISecondary? = null

    private lateinit var killButton: Button
    private lateinit var callbackText: TextView
    private lateinit var handler: InternalHandler

    private var isBound: Boolean = false

    /**
     * Class for interacting with the main interface of the service.
     */
    private val mConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // This is called when the connection with the service is
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service)
            killButton.isEnabled = true
            callbackText.text = "Attached."

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService?.registerCallback(mCallback)
            } catch (e: RemoteException) {
                // In this case, the service crashes before we can
                // do anything with it. We can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(
                    this@Binding,
                    R.string.remote_service_connected,
                    Toast.LENGTH_SHORT
            ).show()
        }

        override fun onServiceDisconnected(className: ComponentName) {
            // This is called when the connection with the service is
            // unexpectedly disconnected&mdash;that is, its process crashed.
            mService = null
            killButton.isEnabled = false
            callbackText.text = "Disconnected."

            // As part of the sample, tell the user what happened.
            Toast.makeText(
                    this@Binding,
                    R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT
            ).show()
        }
    }

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private val secondaryConnection = object : ServiceConnection {

        override fun onServiceConnected(className: ComponentName, service: IBinder) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            secondaryService = ISecondary.Stub.asInterface(service)
            killButton.isEnabled = true
        }

        override fun onServiceDisconnected(className: ComponentName) {
            secondaryService = null
            killButton.isEnabled = false
        }
    }

    private val mBindListener = View.OnClickListener {
        // Establish a couple connections with the service, binding
        // by interface names. This lets other applications be
        // installed that replace the remote service by implementing
        // the same interface.
        val intent = Intent(this@Binding, RemoteService::class.java)
        intent.action = IRemoteService::class.java.name
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
        intent.action = ISecondary::class.java.name
        bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE)
        isBound = true
        callbackText.text = "Binding."
    }

    private val unbindListener = View.OnClickListener {
        if (isBound) {
            // If we have received the service, and hence registered with
            // it, then now is the time to unregister.
            try {
                mService?.unregisterCallback(mCallback)
            } catch (e: RemoteException) {
                // There is nothing special we need to do if the service
                // crashes.
            }

            // Detach our existing connection.
            unbindService(mConnection)
            unbindService(secondaryConnection)
            killButton.isEnabled = false
            isBound = false
            callbackText.text = "Unbinding."
        }
    }

    private val killListener = View.OnClickListener {
        // To kill the process hosting the service, we need to know its
        // PID.  Conveniently, the service has a call that returns
        // that information.
        try {
            secondaryService?.pid?.also { pid ->
                // Note that, though this API lets us request to
                // kill any process based on its PID, the kernel
                // still imposes standard restrictions on which PIDs you
                // can actually kill. Typically this means only
                // the process running your application and any additional
                // processes created by that app, as shown here. Packages
                // sharing a common UID are also able to kill each
                // other's processes.
                Process.killProcess(pid)
                callbackText.text = "Killed service process."
            }
        } catch (ex: RemoteException) {
            // Recover gracefully from the process hosting the
            // server dying.
            // For purposes of this sample, put up a notification.
            Toast.makeText(this@Binding, R.string.remote_call_failed, Toast.LENGTH_SHORT).show()
        }
    }

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private val mCallback = object : IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here is
         * NOT running in our main thread like most other things. So,
         * to update the UI, we need to use a Handler to hop over there.
         */
        override fun valueChanged(value: Int) {
            handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0))
        }
    }

    /**
     * Standard initialization of this activity.  Set up the UI, then wait
     * for the user to interact with it before doing anything.
     */
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContentView(R.layout.remote_service_binding)

        // Watch for button taps.
        var button: Button = findViewById(R.id.bind)
        button.setOnClickListener(mBindListener)
        button = findViewById(R.id.unbind)
        button.setOnClickListener(unbindListener)
        killButton = findViewById(R.id.kill)
        killButton.setOnClickListener(killListener)
        killButton.isEnabled = false

        callbackText = findViewById(R.id.callback)
        callbackText.text = "Not attached."
        handler = InternalHandler(callbackText)
    }

    private class InternalHandler(
            textView: TextView,
            private val weakTextView: WeakReference<TextView> = WeakReference(textView)
    ) : Handler() {
        override fun handleMessage(msg: Message) {
            when (msg.what) {
                BUMP_MSG -> weakTextView.get()?.text = "Received from service: ${msg.arg1}"
                else -> super.handleMessage(msg)
            }
        }
    }
}

Java

public static class Binding extends Activity {
    /** The primary interface we are calling on the service. */
    IRemoteService mService = null;
    /** Another interface we use on the service. */
    ISecondary secondaryService = null;

    Button killButton;
    TextView callbackText;

    private InternalHandler handler;
    private boolean isBound;

    /**
     * Standard initialization of this activity. Set up the UI, then wait
     * for the user to interact with it before doing anything.
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.remote_service_binding);

        // Watch for button taps.
        Button button = (Button)findViewById(R.id.bind);
        button.setOnClickListener(mBindListener);
        button = (Button)findViewById(R.id.unbind);
        button.setOnClickListener(unbindListener);
        killButton = (Button)findViewById(R.id.kill);
        killButton.setOnClickListener(killListener);
        killButton.setEnabled(false);

        callbackText = (TextView)findViewById(R.id.callback);
        callbackText.setText("Not attached.");
        handler = new InternalHandler(callbackText);
    }

    /**
     * Class for interacting with the main interface of the service.
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // This is called when the connection with the service is
            // established, giving us the service object we can use to
            // interact with the service.  We are communicating with our
            // service through an IDL interface, so get a client-side
            // representation of that from the raw service object.
            mService = IRemoteService.Stub.asInterface(service);
            killButton.setEnabled(true);
            callbackText.setText("Attached.");

            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                mService.registerCallback(mCallback);
            } catch (RemoteException e) {
                // In this case the service crashes before we can even
                // do anything with it. We can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
            }

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_connected,
                    Toast.LENGTH_SHORT).show();
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service is
            // unexpectedly disconnected&mdash;that is, its process crashed.
            mService = null;
            killButton.setEnabled(false);
            callbackText.setText("Disconnected.");

            // As part of the sample, tell the user what happened.
            Toast.makeText(Binding.this, R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };

    /**
     * Class for interacting with the secondary interface of the service.
     */
    private ServiceConnection secondaryConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // Connecting to a secondary interface is the same as any
            // other interface.
            secondaryService = ISecondary.Stub.asInterface(service);
            killButton.setEnabled(true);
        }

        public void onServiceDisconnected(ComponentName className) {
            secondaryService = null;
            killButton.setEnabled(false);
        }
    };

    private OnClickListener mBindListener = new OnClickListener() {
        public void onClick(View v) {
            // Establish a couple connections with the service, binding
            // by interface names. This lets other applications be
            // installed that replace the remote service by implementing
            // the same interface.
            Intent intent = new Intent(Binding.this, RemoteService.class);
            intent.setAction(IRemoteService.class.getName());
            bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
            intent.setAction(ISecondary.class.getName());
            bindService(intent, secondaryConnection, Context.BIND_AUTO_CREATE);
            isBound = true;
            callbackText.setText("Binding.");
        }
    };

    private OnClickListener unbindListener = new OnClickListener() {
        public void onClick(View v) {
            if (isBound) {
                // If we have received the service, and hence registered with
                // it, then now is the time to unregister.
                if (mService != null) {
                    try {
                        mService.unregisterCallback(mCallback);
                    } catch (RemoteException e) {
                        // There is nothing special we need to do if the service
                        // crashes.
                    }
                }

                // Detach our existing connection.
                unbindService(mConnection);
                unbindService(secondaryConnection);
                killButton.setEnabled(false);
                isBound = false;
                callbackText.setText("Unbinding.");
            }
        }
    };

    private OnClickListener killListener = new OnClickListener() {
        public void onClick(View v) {
            // To kill the process hosting our service, we need to know its
            // PID.  Conveniently, our service has a call that returns
            // that information.
            if (secondaryService != null) {
                try {
                    int pid = secondaryService.getPid();
                    // Note that, though this API lets us request to
                    // kill any process based on its PID, the kernel
                    // still imposes standard restrictions on which PIDs you
                    // can actually kill.  Typically this means only
                    // the process running your application and any additional
                    // processes created by that app as shown here. Packages
                    // sharing a common UID are also able to kill each
                    // other's processes.
                    Process.killProcess(pid);
                    callbackText.setText("Killed service process.");
                } catch (RemoteException ex) {
                    // Recover gracefully from the process hosting the
                    // server dying.
                    // For purposes of this sample, put up a notification.
                    Toast.makeText(Binding.this,
                            R.string.remote_call_failed,
                            Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    // ----------------------------------------------------------------------
    // Code showing how to deal with callbacks.
    // ----------------------------------------------------------------------

    /**
     * This implementation is used to receive callbacks from the remote
     * service.
     */
    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        /**
         * This is called by the remote service regularly to tell us about
         * new values.  Note that IPC calls are dispatched through a thread
         * pool running in each process, so the code executing here is
         * NOT running in our main thread like most other things. So,
         * to update the UI, we need to use a Handler to hop over there.
         */
        public void valueChanged(int value) {
            handler.sendMessage(handler.obtainMessage(BUMP_MSG, value, 0));
        }
    };

    private static final int BUMP_MSG = 1;

    private static class InternalHandler extends Handler {
        private final WeakReference<TextView> weakTextView;

        InternalHandler(TextView textView) {
            weakTextView = new WeakReference<>(textView);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case BUMP_MSG:
                    TextView textView = weakTextView.get();
                    if (textView != null) {
                        textView.setText("Received from service: " + msg.arg1);
                    }
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    }
}