Android Interface Define Language(AIDL)

1,444 阅读12分钟

AIDL和你可能使用过的其他IDL相似。它允许你定义一个客户端和服务端同时认可的编程接口以实现跨进程通信。在android中,一个进程一般不能访问另一个进程的内存,所以它需要把自己的对象分解成操作系统可以理解的元数据,然后穿过底层,把这些元数据从一个进程传递给另一个进程。完成这些工作的代码写起来又臭又长,所以android提供了AIDL来帮你处理。

首先,我们明确一个概念,如果服务端跟客户端是一个进程,我们称服务端的进程为本地进程,如果服务端跟客户端不在一个进程,那么服务端所在的进程被称为远程进程。

在你开始设计自己的AIDL接口之前,记住AIDL接口的调用是直接的功能调用。你不应该假设这些调用会发生在什么线程。这取决于接口中方法的调用来自的线程是在本地进程还是远程进程中。具体来说就是:

  • 如果服务端运行在本地进程,那么服务端中的方法与客户端中调用处的代码在同一个线程里。如果客户端在你的主线程调用,那么AIDL接口中的代码也是主线程。如果是子线程,那么你服务端的代码也是在这个子线程里运行。因此,只有本地线程(意思跟前面一样,指的是一个进程中的线程)正在访问service的情况下,你才能完全控制service中到底执行哪些线程(如果是同一个进程,那你就不应该使用AIDL了)。
  • 远程进程的方法调用会分配到一个当前进程的线程池中。收到的方法调用可能来自未知的线程,还有可能同时收到多个方法调用。换句话说,AIDL的实现必须考虑到线程安全。同一个远程进程的同一个线程的方法调用应该按顺序依次抵达接收端。
  • 「oneway」这个关键字可以影响远程进程中方法的调用。使用了它之后,远程的调用不再阻塞(换句话说服务端方法的调用变成异步的了);如果是用在本地进程的方法调用上,就不会有影响,方法调用仍然是同步的。

定义一个AIDL接口

你必须使用java语法将你的AIDL接口定义到一个.aidl文件中,然后将这个文件保存到项目存放服务端和客户端的源码(src/ 这个路径下)中。

当你够贱一个包含.aidl文件的app时,Android SDK tools会根据这个文件生成一个IBinder接口并且保存到项目的gen/路径下。service必须正确的实现这个接口。然后客户端就可以绑定到service,调用IBinder中的方法来进行IPC了。

步骤如下:

  1. 创建一个.aidl文件 这个文件定义了带有方法签名的编程接口。
  2. 实现接口 Android SDK tools根据你的.aidl文件使用java语言生成一个接口。这个接口有一个内部抽象类Stub,它继承了Binder并且实现了你的AIDL接口中的方法。你必须继承这个Stub类并且实现方法。
  3. 将接口暴露给客户端 继承Service类,重写它的onBind()方法并且返回你写的Stub的实现类。

1.创建.aidl文件

AIDL使用简单的语法来让你声明一个接口。接口中方法的参数和返回值可以是任何类型,甚至是其他AIDL生成的接口。

.aidl文件是用java语言编写的。每一个.aidl文件必须定义一个单一接口并且只需要接口声明和方法签名。

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

  • 所有的java基本数据类型(int,long,char,boolean等等)
  • String
  • CharSequence
  • List List中的元素必须是这个列表中的数据类型或者其他AIDL生成的接口或者你声明的parcelable。
  • Map 包含的元素与上面的List有同样的要求

如果要使用的类型不在上面的列表中,你必须使用「import」声明将这个类型导入进来,即便这些类型和你的接口在同一个包里。

定义AIDL接口时,要注意:

  • 所有的非基本数据类型的参数都需要一个方向标签来标明数据的去向。要么是in,out或者inout(参考下面的例子)。基本类型的参数默认是in,并且不可更改。
  • 所有的代码注释在生成的IBinder接口中都会保留(除了import和package声明之前的注释)。
  • String和int类型的常量可以定义在AIDL中。比如:const int VERSION = 1;

下面是一个例子:

// IRemoteService.aidl
//这句代码表明文件在哪个包下
package com.example.android;

// Declare any non-default types here with import statements
//在这里用import关键字声明任何没有默认支持的类型

/** Example service interface */
interface IRemoteService {
    /** 获取这个service所在进程的id,然后做一些邪恶的事情 */
    int getPid();

    /** 展示一些AIDL支持的基本数据类型
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

只需要把你的.aidl文件保存在项目的src/路径下,当你构建项目时,SDK tools会在gen/路径下生成IBinder接口。生成文件的文件名跟.aidl文件相匹配,但是扩展名是.java。

如果你使用Android Studio,它的增量构建特性可以帮你瞬间生成这个Binder类。但是如果你没有使用Android Studio,那么gradle tool在你下次构建项目时便会帮你生成——建议写好.aidl文件之后立刻构建项目(使用gradle task assembleDebug 或者 assembleRelease)这样你的代码可以链接到生成的类上。

2.实现这个接口

当你构建项目时,Android SDK tools会生成相应的.java接口文件。生成的接口包含了一个名为Stub的内部类,他是外部接口的抽象实现并且声明了所有.aidl文件中的方法。

为了实现.aidl文件生成的接口,需要继承Binder类并且实现所有从.aidl文件中读取的方法,也就是Stub这个类所做的事情。

下面是实现AIDL的接口的例子

private RemoteMusicServiceInterface.Stub binder = new RemoteMusicServiceInterface.Stub() {
        @Override
        public void start() throws RemoteException {
            playMusic();
        }

        @Override
        public void pause() throws RemoteException {
            pauseMusic();
        }

        @Override
        public void stop() throws RemoteException {
            stopMusic();
        }

        @Override
        public void seekToPosition(int position) throws RemoteException {
            mediaPlayer.seekTo((int) (position * mediaPlayer.getDuration() / 1000f));
        }

        @Override
        public int getMusicProgress() throws RemoteException {
            return (int) (1000f * mediaPlayer.getCurrentPosition() / mediaPlayer.getDuration());
        }
};

现在我们的binder其实就是一个Stub的实例,它定义了服务端的RPC接口。下一步,这个实例将被暴露给客户端这样他就可以与服务端通信了。

下面是你实现AIDL接口时需要注意的点:

  • 进入服务端的调用不保证能运行在主线程,所以你需要从头考虑一下多线程并且让你的service线程安全。
  • 默认情况下,远程过程调用是同步的。如果你知道服务端需要花一定的时间才能完成一个请求,那你就不应该在客户端的主线程调用,因为可能会阻塞主线程。
  • 服务端抛出的异常不会回到客户端。

3.将接口暴露给客户端

一旦你实现了服务端的AIDL接口,你就需要把这个接口暴露给客户端这样客户端就可以绑定到服务端了。为了暴露你的服务端的接口,你的service应该继承Service类,实现onBind()方法,返回一个Stub类的实例。下面的代码是一个简单的RemoteService:

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

现在,当一个客户端调用bindService()方法来连接到服务端时,客户端的onServiceConnected()方法接收到一个binder对象,它是service的onBind()方法返回的。

客户端也应该能访问接口的class对象,所以如果客户端和服务端在不同的app中,客户端的app必须有一份服务端.aidl文件的拷贝。

当客户端在onServiceConnected()方法中收到IBinder对象时,必须调用YourServiceInterface.Stub.asInterface(service)来把它转换成AIDL接口的对象,例子如下:

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 example above 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;
    }
};

通过IPC传递对象

如果想把一个类的对象从一个进程传递到另一个进程,你必须确保你的类对于另一个进程是可用的也就是你的类必须实现了Parcelable接口。实现这个接口非常重要因为进程之间是不共享内存的,别的进程想要识别你的类,那么这个类必须可序列化。

那如何进行Parcelable序列化呢?

  1. 首先让类实现Parcelable接口
  2. 实现writeToParcel方法,这个方法的作用是读取当前对象的状态并且把这个状态写入到一个Parcel中。
  3. 添加一个名为CREATOR的常量,它是一个实现了Parcelable.Creator接口的对象。
  4. 最后,创建一个.aidl文件,声明你要用的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, 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;
    }
}

包含了Parcelable数据的Bundle作为方法的参数

如果你的aidl接口的方法接收Bundle类型的数据作为参数,而这个bundle又包含了parcelable数据,那在从bundle中读取数据之前请务必调用Bundle.setClassLoader(ClassLoader)方法。否则,即便记得类正确实现了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);
}

那你的服务端要这么写:

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. 将.aidl文件放在项目的src/路径下。
  2. 声明一个IBinder接口的实例(根据.aidl文件生成)。
  3. 实现ServiceConnection。
  4. 调用Context.bindService()方法,将步骤3中的实现传入。
  5. 在onServiceConnected()方法中,你将接收到IBinder的实例。调用YourInterfaceName.Stub.asInterface((IBinder)service)来将这个Ibinder对象转换成你的AIDL接口对象
  6. 调用你在AIDL接口中定义的方法。在这个过程中你应该捕获DeadObjectException这种类型的异常,当连接断开的时候会有这种异常抛出。除了这个,还有SecurityException异常,这种异常会在IPC方法涉及的两个进程出现AIDL定义冲突时抛出。
  7. 要断开连接,调用Context.unbindService()方法。

下面是一些要注意的点:

  • 对象可以看作跨进程的引用

  • 你可以发送匿名类作为方法参数

      public static class Binding extends Activity {
          /** The primary interface we will be 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 poke it before doing anything.
           */
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
      
              setContentView(R.layout.remote_service_binding);
      
              // Watch for button clicks.
              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 has been
                  // 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 has crashed before we could 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 has been
                  // unexpectedly disconnected -- 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 allows other applications to 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
                              // has crashed.
                          }
                      }
      
                      // 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 will return
                  // to us that information.
                  if (secondaryService != null) {
                      try {
                          int pid = secondaryService.getPid();
                          // Note that, though this API allows us to request to
                          // kill any process based on its PID, the kernel will
                          // still impose standard restrictions on which PIDs you
                          // are actually able to 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 will also be 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.
                          // Just for purposes of the 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 will
               * NOT be 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);
                  }
          	}
      	}
      }