ITEEDU

使用AIDL设计和调用远程接口

在Android中, 每个应用程序都可以有自己的进程。 在写UI应用的时候, 经常要用到Service。 在不同的进程中,怎样传递对象呢? 在Android平台中不允许跨进程内存共享。 因此传递对象, 只能把对象拆分成操作系统能理解的简单形式,所以,他们需要把对象拆分成操作系统能理解的简单形式,以便伪装成对象跨越边界访问。

写"marshall"的代码是繁琐而枯燥的工作,好在AIDL提供了比较实在的工具以便让它更有趣。

AIDL (Android Interface Definition Language )是Android接口描述语言,属于Eclipse plugin 管理)。通过Eclipse plugin ,AIDL可以自动产生java并编译,这些都工具位于tools/目录。

  • 实现AIDL接口方法 - 编译器会根据AIDL接口, 产生一个JAVA接口(并且实现一些必要的附加方法供IPC调用)。 这个接口有一个名为Stub的内部抽象类,你必须创建一个类来扩展这个Stub内部抽象类(Stub内部抽象类名称为接口名称.Stub), 并实现了远程调用需要的几个方法,这些方法定义于 .aidl文件中。
  • 向客户端开放接口 - 如果写的是Service,应该扩展该Service并重载Service.onBind(Intent)方法 来返回一个实现上述接口的类的实例。
  • 创建.aidl文件

    AIDL语法很简单,可以用来声明一个带一个或多个方法的接口,也可以传递参数和返回值。 这些参数和返回值可以是任意类型,甚至在其他AIDL生成的接口上也可以这样。 然而, 需要注意的重点 必须导入所有的non-built-in类型, 即使他们定义的package和你的interface一样也要导入。 AIDL支持的数据类型如下:

    • Java编程语言中基本类型(int, boolean, etc) — 不需要 import 再次声明。
    • 以下的类 (不需要 import 声明):
      • String
      • List - List中所有的元素都必须是被支持的同一种数据类型中的一个, 包括其他AIDL生成的Interface和Parcelables。 List可以作为泛型类来灵活使用(e.g. List<String>)。 实际上这些类将却是ArrayList,虽然调用的还是名称为List接口。
      • Map - Map中所有的元素都必须是被支持同一种数据类型中的一个, 包括其他AIDL生成的Interface和Parcelables。 泛型化的Map, (e.g. 不支持Map<String,Integer> 这种形式)。 实际操作中使用的却是HashMap,不过使用的名字依然是Map接口。
      • CharSequence - CharSequence的作用是可以被TextView
      • 和其他Widget对象使用。
    • 其他的,可以根据需要,使用import声明,所以import声明是必须的。
    • 封装Parcelable 协议 需要使用自定义的类是值传递,所以import 也是必须的。

    下面是AIDL的基本语法:

    // AIDL 文件, 文件名SomeClass.aidl
    // 文件可以有注释
    // 在import和package声明以前的注释将被忽略
    // 但是可以在接口/方法/属性之前添加注释
    
    
    // package位置声明
    package com.android.sample;
    
    // 需要声明的类
    // import引入声明
    import com.android.sample.IAtmService;
    
    // 接口定义
    interface IBankAccountService {
        
        // 可以有0到多个参数,可以有0到1个返回值
        int getAccountBalance();
        void setOwnerNames(in List<String> names);
        
        // 方法中的参数可以在其他AIDL中被定义
        BankAccount createAccount(in String name, int startingDeposit, in IAtmService atmService);
    
        // 所有非non-Java原始参数(e.g., int, bool, etc) 都必须
        // 标明参数方向. 有效的参数方向 in, out, inout. (Java原始参数默认是in形式参数,并且没有其他方式)。
        // 限制参数方向是根据实际需要来定,要注意的是不注明参数方向将会耗费大量资源来真理匹配该参数的方向。
        int getCustomerList(in String branch, out String[] customerList);
    }

    实现接口

    AIDL 生成接口的名称与.aidl文件名称是一样的。 如果使用Eclipse插件, AIDL被将会自动创建 (而用不着先运行AIDL工具然后再创建项目)。 如果你还准备通过原始方式来搞,那你只能运行AIDL工具了。

    编译器会根据AIDL接口, 产生一个名为Stub的内部抽象类,它声明的所有方法都将出现在.aidl文件中。 Stub类包含一部分有用的的方法,比如asInterface(),它执行一个IBinder(在 applicationContext.bindService()执行成功后传给客户端onServiceConnected()方法), 并且返回一个接口实例以便调用IPC方法。详情参见Calling an IPC Method

    接口的大概步骤就是扩展YourInterface.Stub和实现方法。( 你可以创建一个aidl文件并实现stub方法而不用绑定-AndRoid创建过程在java文件之前会处理aidl文件。 )

    下面是一个实现调用接口IRemoteService的例子,使用匿名实例公开一个简单的方法gerPid():

    //在同一个项目中不需要import IRemoteService。
    private final IRemoteService.Stub mBinder = new IRemoteService.Stub(){
        public int getPid(){
            return Process.myPid();
        }
    }

    关于实现接口方法的几个规则:

    • 抛出的异常不要返回给调用者。
    • IPC调用是同步的。如果你知道一个IPC服务需要超过几毫秒的时间才能完成地话, 你应该避免在Activity/View 的主线程中调用。 也就是IPC调用会挂起应用程序导致界面失去响应。 这种情况应该考虑单独一个线程来处理。
    • 只有方法才获得支持,不能在AIDL接口中声明静态属性

    向客户端开放接口

    既然你已经实现接口,现在需要对客户端开放接口的访问。 这就是所谓的 "发布服务" 发布一个服务,就是继承 Service并使用 Service.onBind(Intent)方法 返回到实例的接口实现。 下面这个代码片段是一个服务允许客户端访问的IRemoteService接口。

    public class RemoteService extends Service {
    ...
        @Override
        public IBinder onBind(Intent intent) {
            // 选择返回接口,如果只有一个接口,那么直接放回即可
            if (IRemoteService.class.getName().equals(intent.getAction())) {
                return mBinder;
            }
            if (ISecondary.class.getName().equals(intent.getAction())) {
                return mSecondaryBinder;
            }
            return null;
        }
    
        /**
         *  通过IDL定义IRemoteInterface 
         */
        private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
            public void registerCallback(IRemoteServiceCallback cb) {
                if (cb != null) mCallbacks.register(cb);
            }
            public void unregisterCallback(IRemoteServiceCallback cb) {
                if (cb != null) mCallbacks.unregister(cb);
            }
        };
    
        /**
         * secondary接口
         */
        private final ISecondary.Stub mSecondaryBinder = new ISecondary.Stub() {
            public int getPid() {
                return Process.myPid();
            }
            public void basicTypes(int anInt, long aLong, boolean aBoolean,
                    float aFloat, double aDouble, String aString) {
            }
        };
    
    }

    Parcelables的参数值传递

    如果你有类需要通过AIDL接口从一个进程发送到另一个,你可以使用Parcelables的参数值传递。 你必须确保类代码可以被IPC接收端所使用。通常这意味着一开始你就要和service进行通讯。

    要支持Parcelable协议需要注意以下五个部分:

    1. 你创建的类要实现 Parcelable接口。
    2. 通过public void writeToParcel(Parcel out) 方法把当前对象打包。
    3. 通过public void readFromParcel(Parcel in) 方法从包中读取信息到对象中。
    4. 向类中添加一个静态成员CREATOR,该象可以实现Parcelable.Creator接口
    5. 最后(这不是最重要的):
      • 如果你是使用Eclipse/ADT工具开发, 按照以下步骤来做:
        1. 进入Package Explorer视图模式,右击项目节点。
        2. 选择 Android Tools > Create Aidl preprocess file for Parcelable classes
        3. 这将在项目根目录创建一个可被调用的"project.aidl" 文件。 它可以自动编译aidl文件为parcelable类。
      • 如果是使用Ant工具或者其他编译工具,就需要创建一个aidl文件用来定义你的parcelable 类(如下所示)。 如果其他编译工具,不需要把aidl文件添加到编译过程中。类似于C语言中的头文件,aidl文件不需要编译。
    6. AIDL将使用代码中生成的这些方法和成员来伪装或解读对象。

      下面这个例子说明了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.aidl示例

      package android.graphics;
      
      // 定义Rect,AIDL才能找到并且实现parcelable接口协议。
      parcelable Rect;
      

      Rect类中的伪装是相当简单的。仔细查看Parcel类中的其他方法,,你会看到其他各种值你都可以写进Parcel。

      警告: 不要忽视从其他进程接收数据时的安全性考虑。 在本例中,rect将从parcel中读四个数字,而你的工作则是确保这些都在可接受的值 得范围内而不管调用者想要干什么。Security and Permissions in Android 中有更多关于如何 确保应用程序安全的信息。

      调用一个IPC方法

      调用类调用远程接口的步骤:

      1. 声明一个接口类型的变量,该接口类型在.aidl文件中定义。
      2. 实现ServiceConnection。
      3. 调用Context.bindService(),并在ServiceConnection实现中进行传递。
      4. 在ServiceConnection.onServiceConnected()实现中, 你会收到一些IBinder实例(被调用 service)。 调用 YourInterfaceName.Stub.asInterface((IBinder)service) 将参数转换为YourInterface 类型。
      5. 调用接口中定义的方法。你总会捕捉到 DeadObjectException异常, w该异常在连接断开时被抛出。它只会被远程方法抛出。
      6. 断开连接,调用接口实例中的Context.unbindService()方法。

      调用IPC服务需要注意几点:

      • 通过过程对对象引用的统计。
      • 匿名对象可以通过方法参数发送。

      下面的代码展示了在ApiDemos项目从远程Activity例子中调用AIDL创建Service的过程。

      public class RemoteServiceBinding extends Activity {
          /** 初始化主要接口*/
          IRemoteService mService = null;
          /** 其他接口 */
          ISecondary mSecondaryService = null;
      
          Button mKillButton;
          TextView mCallbackText;
      
          private boolean mIsBound;
      
          /**
           * 标准的activity初始化,设置UI
           */
          @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.");
          }
      
          /**
           * 类与main函数交互
           */
          private ServiceConnection mConnection = new ServiceConnection() {
              public void onServiceConnected(ComponentName className,
                      IBinder service) {
                  // 连接建立时被调用的服务,客户端可以通过IDL接口与该Service通信。
                  
                  mService = IRemoteService.Stub.asInterface(service);
                  mKillButton.setEnabled(true);
                  mCallbackText.setText("Attached.");
      
                  // 监视服务
                  try {
                      mService.registerCallback(mCallback);
                  } catch (RemoteException e) {
                      // 异常抛出,服务崩溃之前可以做的一些事情; 
                  }
      
                  // 回显信息
                  Toast.makeText(RemoteServiceBinding.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(RemoteServiceBinding.this, R.string.remote_service_disconnected,
                          Toast.LENGTH_SHORT).show();
              }
          };
      
          /**
           * 类与secondary接口的交互.
           */
          private ServiceConnection mSecondaryConnection = new ServiceConnection() {
              public void onServiceConnected(ComponentName className,
                      IBinder service) {
                  // 连接一个Secondary与连接其他接口的方法一样
                  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才能结束本地服务,
                  // 好在我们的服务可以方便的返回一些有用的信息
                  if (mSecondaryService != null) {
                      try {
                          int pid = mSecondaryService.getPid();
                          // 使用这个API可以结束任何已知PID的进程
                          // 比如只运行一个进程的应用,在运行过程中,会产生一些附加进程,这些进程会共享一个UID,
                          // 结束这个UID,那么其他的附加进程也会被一起结束
      
                          Process.killProcess(pid);
                          mCallbackText.setText("Killed service process.");
                      } catch (RemoteException ex) {
                          // 进程结束失败,显示一个失败通知
      
                          Toast.makeText(RemoteServiceBinding.this,
                                  R.string.remote_call_failed,
                                  Toast.LENGTH_SHORT).show();
                      }
                  }
              }
          };
      
          // ----------------------------------------------------------------------
          // 回调示例代码
          // ----------------------------------------------------------------------
      
          /**
           * 实现远程服务的回调
           */
          private IRemoteServiceCallback mCallback = new IRemoteServiceCallback.Stub() {
              /**
               * 被调用的远程服务定时返回新的值。
               */
              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);
                  }
              }
      
          };
      }

      树上蹭灰 2008-10-04