AIDL(Android接口定义语言)

Android 接口定义语言 AIDL(Android Interface Definition Language)与其他已有的 IDL 很类似。 客户端和服务端可以通过由它定义的编程接口来达成共识,以便通过进程间通讯(IPC)完成相互通讯。 在 Android 系统中,通常一个进程不允许直接访问另一个进程的内存。 因此为了能够实现对话,进程需要把对象分解为操作系统可以识别的原生数据,在跨越进程边界后再组装起来。 实现组装的代码非常枯燥无趣,因此 Android 通过 AIDL 可有助于完成这一过程。

注意: 仅当允许其他应用程序通过 IPC 方式访问服务,并且服务需要多线程运行时,才必须用到 AIDL。 如果不需要进行跨越多个应用的并发 IPC,就应该用 实现绑定 实现接口。或者要进行 IPC 但不需要多线程运行,则可 使用 Messenger 来实现接口。 无论如何,在实现 AIDL 之前,请确认已经理解了 Bound 服务 中的内容。

在开始设计 AIDL 接口之前,请务必注意,对 AIDL 接口的调用是直接函数调用(direct function call)。 发起调用的线程根本无法预料。 根据发起调用的线程是在本地进程还是在远程进程,处理方式也将不同。 具体来说:

  • 如果从本地进程发起,则调用将在发起线程中运行。 如果是主 UI 线程发起的,则其将继续运行 AIDL 接口。 如果是其他线程发起的,则为运行服务代码的线程。 因此,如果只有本地线程需要访问服务,则可以完全控制在哪个线程运行(但如果是这样,就根本不应该使用 AIDL 了,而应该通过 实现 Binder 来创建接口)。

  • 从远程进程发起的调用,将会由系统维护的本地进程内部的线程池分发。 必须准备好迎接来自未知线程的调用,并且还可能同时收到多个调用。 换句话说, AIDL接口的实现必须完全是线程安全的。

  • 关键字oneway会改变远程调用的处理方式。 使用该关键字时,远程调用将不会阻塞,在发送完交易数据它就会立即返回。 接口的实现代码最终将这种远程调用视同由 Binder 线程池发起的调用一样接收。 如果在本地调用中使用了oneway,则不起作用,调用仍旧是同步执行的。

定义 AIDL 接口

只能在.aidl文件中使用 Java 语法定义 AIDL 接口, 并且在服务所在应用和其他需要绑定服务的应用中,均保存为源代码文件(在src/目录下)。

在编译包含.aidl文件的应用时, Android SDK 工具会根据 .aidl文件生成一个 IBinder 接口,并将它保存在项目的gen/目录下。 服务端必须根据需要实现 IBinder。 然后客户端应用就能与服务绑定,并利用 IBinder 调用方法实现 IPC 。

要利用 AIDL 创建一个可被绑定的服务,请按照以下步骤进行:

  • 创建 .aidl 文件
    该文件定义了包含方法声明(Method Signature)的编程接口

  • 实现接口
    Android SDK 工具包会根据.aidl 文件生成 Java 语言格式的接口。 这个接口有一个名为Stub的内部抽象类,它扩展自 Binder ,并用于实现 AIDL 接口所要求的方法。 必须扩展这个Stub类并实现这些方法。

  • 向客户端公布接口
    实现 Service 并重写 onBind() 方法,以便返回已实现的Stub类。

警告: 为了避免其他应用程序与服务的使用中断,在应用程序发布之后,对 AIDL 接口 所做的所有修改都必须保证与之前的定义兼容。 也就是说,因为.aidl必须复制给其他应用程序以便其访问服务接口,所以必须维持对以前接口的支持。

创建 .aidl 文件

AIDL 的语法很简单,声明一个带有若干方法的接口即可,这些方法都可带有参数和返回值。 参数和返回值可以为任意类型,甚至可以是另一个由 AIDL 生成的接口。

.aidl 文件必须用 Java 语言编写。 每个.aidl文件中必须也只能定义一个接口,且只能包含接口的定义和方法声明。

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

  • Java 语言的所有简单类型(如int、long、char、boolean等等)

  • String

  • CharSequence

  • List
    该 List 中的所有数据只能是这里列出的类型、其他某个基于 AIDL 生成的接口、已声明的自定义 Parcelable 类。 List 还可被用作“泛型”(generic)类(如 List)。 虽然方法是通过 List 接口生成的,但是实际收到的实体类其实会是一个 ArrayList 。

  • Map
    该 Map 中的所有数据只能是这里列出的类型、其他某个基于 AIDL 生成的接口、已声明的自定义 Parcelable 类。 这里不支持 Map 泛型(比如 Map<String,Integer> 的形式)。 虽然方法是通过 Map 接口生成的,但实际收到的实体类其实会是一个 HashMap。

对于未列入上述列表的类型,即便是定义于接口所在的包中,也必须包含 import 语句。

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

  • 方法可以有多个参数,也可以不带参数;可有一个返回值,或无返回值(void)。

  • 所有非简单类型的参数都需要带有一个指明数据方向的标志。 可以是in、out或inout(参阅下述例子)。

简单类型的参数默认是 in ,且不能是其他方向值。

注意: 请按照实际需求来限定参数的方向,因为参数的组装过程开销很大。

  • .aidl 文件中的代码注释都会一并放入最终生成的 IBinder 接口中( import 和 package 语句之前的注释除外)。

  • AIDL 中只支持方法,不能声明静态成员。

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

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

// 在这里用 import 语句对所有非默认类型进行声明

/** Example service interface */
interface IRemoteService {
    /** 获取服务的进程 ID 并完成一些任务(do evil things)*/
    int getPid();

    /** 这里给出一些可供 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)。
如果使用 Eclipse 开发,那么增量编译器(Incremental Build)几乎是即时生成 Binder 类。 如果不是使用 Eclipse 开发的,则 Ant 工具会在下次编译时生成 Binder 类—— 请在 .aidl 文件的编写完成后及时用 ant debug(或 ant release )编译项目, 以便其他代码可以链接编译完毕的类。

实现接口

在编译应用程序时, Android SDK 工具会以 .aidl 文件名生成一个 .java 接口文件。 生成完毕的接口包含一个名为 Stub 的子类,它是其父接口类的抽象实现(例如 YourInterface.Stub ), 它声明了 .aidl 文件定义的所有方法。

注意: Stub 还定义了一些助手方法,最值得一题的是 asInterface(), 它的参数是 IBinder (通常就是传给客户端 onServiceConnected() 方法的那个),返回值是一个 stub 接口的实例。 关于这一转换的细节,请参阅调用 IPC 方法一节。

要实现由 .aidl 文件生成的接口,请扩展已生成的 Binder 接口(比如YourInterface.Stub),并实现由 .aidl 文件继承而来的方法。

下面给出了一个名为 IRemoteService 的代码示例,它由上面的示例 IRemoteService.aidl 所定义。 这里通过一个匿名实例来实现:

private final IRemoteService.Stub mBinder = 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
    }
};

这里的 mBinder 是一个 Stub 类的实例( Binder ),用于定义 RPC 服务接口。 下一步,将向客户端公布该实例,以便它们与服务进行交互。

实现 AIDL 接口时,请注意以下几点:

  • 调用不一定是在主线程中运行,因此从一开始就要考虑多线程运行,并保证服务是按照线程安全的模式编写的。

  • 默认情况下, RPC 调用是以同步方式运行的。 如果事先知道服务处理完一次请求需要若干毫秒的时间,就不应该在 Activity 的主线程中发起调用。 因为这可能会导致应用程序挂起( Android 可能会显示“应用程序停止响应”的对话框)。 ——通常,这时应该在客户端的单独线程中发起调用。

  • 所有异常都不会(Exception)发还给调用者。

向客户端公布接口

在实现了服务的接口之后,就需要将它公布给客户端以供绑定。 要为服务发布接口,请扩展 Service 类并实现 onBind() 方法,以便返回自定义 Stub 类的实例(如上所述)。 下面是将接口 IRemoteService 公布给客户端的示例。

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

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

    private final IRemoteService.Stub mBinder = 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() 方法返回的 mBinder 实例。

客户端还必须有权限访问接口类。 因此,假如客户端和服务端位于不同的应用,客户端的应用必须在其 src/ 目录下拥有一份 .aidl 文件的拷贝。 用于生成供客户端访问 AIDL 方法的 android.os.Binder 接口。

在接收到回调方法 onServiceConnected() 中的 IBinder 时,客户端必须调用 YourServiceInterface.Stub.asInterface(service) 来把返回的参数转换为 YourServiceInterface 类型。 例如:

IRemoteService mIRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
    // Called when the connection with the service is established
    public void onServiceConnected(ComponentName className, IBinder service) {
        // Following the example above for an AIDL interface,
        // this gets an instance of the IRemoteInterface, which we can use to call on the service
        mIRemoteService = 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");
        mIRemoteService = null;
    }
};

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

通过 IPC 传递对象

如果要跨进程传递某个类,可以通过 IPC 接口来实现。 不过,请务必确保在 IPC 通道的对端可以识别该类的代码,该类必须支持 Parcelable 接口。支持 Parcelable 接口非常重要,因为这使得 Android 系统可将对象分解为能够跨进程组装的原生数据。

可按以下步骤创建支持 Parcelable 协议的类:

  1. 必须实现Parcelable 接口。

  2. 实现 writeToParcel 方法,参数为当前对象的状态,并写入一个 Parcel中。

  3. 在类中添加一个名为 CREATOR 的静态成员变量,即为一个实现了 Parcelable.Creator 接口的对象。

  4. 最后,创建 .aidl 文件,声明该 Parcelable 类(如下述 Rect.aidl 文件所示)。

如果采用自定义编译方式,请不要把 .aidl 文件加入编译项目。 与 C 语言的头文件类似, .aidl 文件不会被编译。

AIDL 利用上述方法和成员变量来分解和组装对象。

以下是创建 Rect 类的 Rect.aidl 文件示例,该类支持打包操作(Parcelable):

 package android.graphics;

// 声明 Rect,以便 AIDL 识别并知道它已实现了 Parcelable 协议。
parcelable Rect;

下面是 Rect 类如何实现 Parcelable 协议的代码示例:

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) {
        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();
    }
}

Rect 类的分解过程非常简单。 请查看一下 Parcel 的其他方法,即可了解还有哪些可写入 Parcel 的数据类型。

警告: 请不要忘记从其他进程读取数据所导致的安全风险。 比如这里的 Rect 从 Parcel 中读取了4个数字,但必须考虑是否要限制调用者只能读到这些值。 关于如何防止恶意代码对应用程序的破坏,详情请参阅 安全与权限

调用 IPC 方法

要调用 AIDL 定义的远程接口,必须按照以下步骤进行:

  1. 在项目的 src/ 目录下包含 .aidl 文件。

  2. 声明 IBinder 接口的实例(基于 AIDL 生成的)

  3. 实现 ServiceConnection。

  4. 调用 Context.bindService() ,传入上面已实现的 ServiceConnection。

  5. 在 onServiceConnected() 代码中,接收到 IBinder 实例(供调用的 service)。 调用
    YourInterfaceName.Stub.asInterface((IBinder)service) 将返回的参数转换为
    YourInterface 类型。

  6. 调用自定义接口中的方法。请确保捕获 DeadObjectException
    异常,当连接中断时会抛出该异常,而且是远程方法唯一可能会抛出的异常。

  7. 调用自定义接口实例的 Context.unbindService() 方法断开连接。

调用 IPC 服务需要注意:

  • 对象引用是跨进程计数的。

  • 可以将匿名对象作为方法的参数发送。

关于绑定服务的更多信息,请参阅文档 Bound 类型的服务。

下面给出了部分示例代码,演示了如何调用由 AIDL 创建的服务,摘自 ApiDemos 项目的 Remote Service 例程。

public static class Binding extends Activity {
    /** 调用服务的主接口 */
    IRemoteService mService = null;
    /** 使用服务的另一个接口 */
    ISecondary mSecondaryService = null;

    Button mKillButton;
    TextView mCallbackText;

    private boolean mIsBound;

    /**
     * Activity 的标准初始化方法。Standard initialization of this activity.  Set up the UI, then wait
     * 建立界面,等待用户点击后再执行动作。
     */
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.remote_service_binding);

        // 监视按钮的点击事件
        Button button = (Button)findViewById(R.id.bind);
        button.setOnClickListener(mBindListener);
        button = (Button)findViewById(R.id.unbind);
        button.setOnClickListener(mUnbindListener);
        mKillButton = (Button)findViewById(R.id.kill);
        mKillButton.setOnClickListener(mKillListener);
        mKillButton.setEnabled(false);

        mCallbackText = (TextView)findViewById(R.id.callback);
        mCallbackText.setText("Not attached.");
    }

    /**
     * 与服务的主接口进行交互的类
     */
    private ServiceConnection mConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // 当与服务建立连接后将会被调用,获取可与之交互的服务对象。
            // 与服务的通讯是通过 IDL 接口来完成的,因此需要在客户端获得一个代表原始服务的对象。
            mService = IRemoteService.Stub.asInterface(service);
            mKillButton.setEnabled(true);
            mCallbackText.setText("Attached.");

            // 在保持连接时需要一直对服务进行监视
            try {
                mService.registerCallback(mCallback);
            } catch (RemoteException e) {
                // 此时服务已崩溃,也就无法使用了,
                // 很快连接将会断开(如果服务可被重启则会重新建立连接),
                // 所以,这里不需要进行任何处理。
            }

            // 作为示例,下面把当前状态通知用户。
            Toast.makeText(Binding.this, R.string.remote_service_connected,
                    Toast.LENGTH_SHORT).show();
        }

        public void onServiceDisconnected(ComponentName className) {
            // 当与服务的连接意外终止时将会被调用,
            // 也就是说,服务的进程崩溃了。
            mService = null;
            mKillButton.setEnabled(false);
            mCallbackText.setText("Disconnected.");

            // 作为示例,下面把当前状态通知用户。
            Toast.makeText(Binding.this, R.string.remote_service_disconnected,
                    Toast.LENGTH_SHORT).show();
        }
    };

    /**
     * 与服务的第二个接口进行交互的类
     */
    private ServiceConnection mSecondaryConnection = new ServiceConnection() {
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            // 与第二个接口的连接方法类似于其他接口
            mSecondaryService = ISecondary.Stub.asInterface(service);
            mKillButton.setEnabled(true);
        }

        public void onServiceDisconnected(ComponentName className) {
            mSecondaryService = null;
            mKillButton.setEnabled(false);
        }
    };

    private OnClickListener mBindListener = new OnClickListener() {
        public void onClick(View v) {
            // 通过与接口名称绑定,与服务建立起两个连接。
            // 这样,如果某个应用程序实现了相同接口,就可以在安装后替换掉相应的远程服务。
            bindService(new Intent(IRemoteService.class.getName()),
                    mConnection, Context.BIND_AUTO_CREATE);
            bindService(new Intent(ISecondary.class.getName()),
                    mSecondaryConnection, Context.BIND_AUTO_CREATE);
            mIsBound = true;
            mCallbackText.setText("Binding.");
        }
    };

    private OnClickListener mUnbindListener = new OnClickListener() {
        public void onClick(View v) {
            if (mIsBound) {
                // 如果已接收到服务并进行了注册,
                // 就在这里进行注销。
                if (mService != null) {
                    try {
                        mService.unregisterCallback(mCallback);
                    } catch (RemoteException e) {
                        // 如果服务已经崩溃了,就不需要再做什么处理了。
                    }
                }

                // 断开已有连接
                unbindService(mConnection);
                unbindService(mSecondaryConnection);
                mKillButton.setEnabled(false);
                mIsBound = false;
                mCallbackText.setText("Unbinding.");
            }
        }
    };

    private OnClickListener mKillListener = new OnClickListener() {
        public void onClick(View v) {
            // 为了杀死服务所在的进程,需要知道其 PID。
            // 好在该服务中现成就有一个返回 PID 的方法了。
            if (mSecondaryService != null) {
                try {
                    int pid = mSecondaryService.getPid();
                    // 请记住,虽然用下面的 API 可以请求杀死指定 PID 的进程,
                    // 但系统核心仍然会按照标准的权限标准来核查指定的 PID 是否可被杀死。
                    // 通常,只有运行本应用的进程及应用创建的进程才能被杀死,
                    // UID 相同的多个包也可以互相杀死各自的进程。
                    Process.killProcess(pid);
                    mCallbackText.setText("Killed service process.");
                } catch (RemoteException ex) {
                    // 体面地处理服务进程已被杀死的情况
                    // 作为示例,这里弹出一个通知。
                    Toast.makeText(Binding.this,
                            R.string.remote_call_failed,
                            Toast.LENGTH_SHORT).show();
                }
            }
        }
    };

    // ----------------------------------------------------------------------
    // 处理回调方法的代码
    // ----------------------------------------------------------------------

    /**
     * 接收远程服务的回调方法
     */
    private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
        /**
         * 本方法将由远程服务定期调用,改变值。
         * 请注意, IPC 调用是通过每一个进程内部的线程池发起的,所以这里的代码将不会运行在主线程中。
         * 因此,如果要更新 UI,需要使用 Handler 跳出去运行。
         */
        public void valueChanged(int value) {
            mHandler.sendMessage(mHandler.obtainMessage(BUMP_MSG, value, 0));
        }
    };

    private static final int BUMP_MSG = 1;

    private Handler mHandler = new Handler() {
        @Override public void handleMessage(Message msg) {
            switch (msg.what) {
                case BUMP_MSG:
                    mCallbackText.setText("Received from service: " + msg.arg1);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }

    };
}
Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1