详解Android跨进程IPC通信AIDL机制原理

2020-10-30 00:41:14 浏览数 (1)

简介

AIDL:Android Interface Definition Language,即Android接口定义语言,用于生成Android不同进程间进行进程通信(IPC)的代码,一般情况下一个进程是无法访问另一个进程的内存的。如果某些情况下仍然需要跨进程访问内存数据,这时候Android系统就要将其对象分解成能够识别的原数据,编写这一组操作的代码是一项繁琐的工作,但是AIDL对底层进行了抽象的封装,简化了跨进程操作。

AIDL IPC机制是面向接口的,像COM或Corba一样,但是更加轻量级。它是使用代理类在客户端和实现端传递数据。

在Android中跨进程操作的方式不止一种,四大组件中ContentProvider天生就是为跨进程操作而存在的,但是ContentProvider所谓的跨进程操作数据,这些数据不一定是存放在内存中的,如通讯录数据时存放在Sqlite数据库中的。AIDL支持的跨进程操作的数据是要存放在内存中的,AIDL底层实际上也是使用的Binder进行的跨进程操作,后续另起一篇博文继续介绍Binder的跨进程机制。

使用场景

只有不同应用之间需要进行IPC,并且想要在Service中处理多线程时,这种场景才有必要使用AIDL。如果仅仅需要跨进程但是不是跨应用,这时候应该通过Binder进行数据交互;另外如果仅仅是需要跨进程IPC,但是不需要处理多线程,这时候应该通过Messenger类进行数据交互。

定义AIDL接口

在Android Studio中使用AIDL的项目的目录结构跟eclipse中有很大差异,下图是使用AIDL的项目的目录结构。

在Android Studio中只需要在某个Module中使用右键菜单中new就会显示创建AIDL文件的菜单,当新建成功后AIDL文件位于工程的同java同一级的aidl目录文件夹下面。在 .aidl 文件中存放的就是AIDL接口。

定义.aidl文件

.aidl文件名称必须同接口名称保持一致,必须使用Java语言的语法定义AIDL文件。AIDL使用简单语法,通过可带参数和返回值的一个或多个方法来声明接口。参数和返回值可以是任意类型,甚至可以是其他 AIDL 生成的接口。每个.aidl文件都必须定义单个接口,并且只需包含接口声明和方法签名,也意味着在.aidl文件中接口名称和方法名称都不可以使用权限修饰符。

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

  1. Java语言中所有的基本数据类型,字符类型char,布尔类型boolean以及数值类型byte、short、int、long、float、double;
  2. String和CharSequence类型;
  3. 集合List类型,List中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。可选择将 List用作泛型类型(例如,List )。另一端实际接收的具体类始终是 ArrayList,但生成的方法使用的是List接口, 在AIDL中不可以使用ArrayList类型进行定义,只能使用List接口定义 ,否则会报unknown type编译错误。
  4. 集合Map类型,中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。不同于集合List接口, 在AIDL中不支持泛型Map(如 Map 形式的 Map)。 另一端实际接收的具体类始终是 HashMap,但生成的方法使用的是 Map 接口。类似List接口, 在.aidl文件中不能使用HashMap,只能使用Map接口 。
  5. 自定义类型必须实现Parcelable接口,并且在aidl文件夹下有对应类型的aidl文件;
  6. 非JDK中定义的类型,类似于Java语法,必须使用import进行引入。

定义AIDL接口时需要注意如下:

  1. 方法可带零个或多个参数,返回值或空值,但是方法名称不能相同;
  2. 所有非基本数据类型参数都需要指示数据走向的方向标记。可以是 in、out 或 inout。基本数据类型默认为 in,不能是其他方向;
  3. .aidl 文件中包括的所有代码注释都包含在生成的 IBinder 接口中(import 和 package 语句之前的注释除外);
  4. 只支持方法,不应在AIDL中定义静态字段。

如下是定义是IRemoteService.aidl:

代码语言:javascript复制
package com.sunny.server;
import com.sunny.server.bean.User;
interface IRemoteService {
  void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
      double aDouble, String aString);
   String getName();
   void setListData(in List<String  inList,out List<String  outList);
   void setMapData(in Map map);
   void setUser(in User user); 
}

下面是自定义的JavaBean类型User.aidl

代码语言:javascript复制
package com.sunny.server.bean; 
parcelable User;

User类在使用的时候必须实现Parcelable接口,代码这里就不再贴出来了。

Service实现AIDL接口

在定义的AIDL接口编译后实际上会生成一个跟.aidl同名的Java类文件,里面包含了所有的AIDL文件中声明的方法,并且包含了一个默认的实现类Stub,该类是抽象类,继承了Binder类实现了AIDL接口。在Stub类中有两个方法一个是asInterface()方法,该方法返回的是AIDL文件生成的接口,另外一个方法是asBinder(),该方法返回的是一个IBinder类型的实例。

asInterface()和asBinder()方法非常有用,asInterface()方法可以用于客户端的IPC方法调用,另外一个方法可以用于在服务端返回Binder实例,并在服务端实现响应的接口方法。

上面介绍过在定义非基本数据类型的时候必须定义数据走向,声明in或out或者inout,在AIDL生成的Java文件中就可以看出来究竟了,这里可以参看setListData()方法的生成实现。

代码语言:javascript复制
@Override
public void setListData(java.util.List<java.lang.String  inList, java.util.List<java.lang.String  outList) throws android.os.RemoteException {
 android.os.Parcel _data = android.os.Parcel.obtain();
 android.os.Parcel _reply = android.os.Parcel.obtain();
 try {
 _data.writeInterfaceToken(DESCRIPTOR);
 _data.writeStringList(inList);
 mRemote.transact(Stub.TRANSACTION_setListData, _data, _reply, 0);
 _reply.readException();
 _reply.readStringList(outList);
 } finally {
 _reply.recycle();
 _data.recycle();
 }
}

如果声明数据时是in,在生成相对应的方法的时候调用的实际上是Parcel的writeXXX方法,如果声明的是out,在实现上面采用的是readXXX,所以在定义的时候一定要明确调用逻辑。

接下来看一下服务端中MyService类的实现。

代码语言:javascript复制
public class MyService extends Service {
   private static final String TAG = "AIDL_Server"; 
  @Nullable
  @Override
  public IBinder onBind(Intent intent) {
    return mBinder;
  }
 
  private IRemoteService.Stub mBinder = new IRemoteService.Stub() { 
    @Override
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
      Log.d(TAG, "anInt:"   anInt   " aLong:"   aLong   " aBoolean:"   aBoolean   " aFloat:"   aFloat   " aDouble:"   aDouble   " aString:"   aString);
    }
 
    @Override
    public String getName() throws RemoteException {
      return "admin";
    }
 
    @Override
    public void setListData(List<String  inList, List<String  outList) throws RemoteException {
      Log.d(TAG, "inList:"   inList.toString());
      setOutList(outList);
    }
 
    @Override
    public void setMapData(Map map) throws RemoteException {
      Log.d(TAG, "map:"   map.toString());
    }
 
    @Override
    public void setUser(User user) throws RemoteException {
      Log.d(TAG, "user:"   user.toString());
    }
  };
 
  private void setOutList(List<String  list) {
    list.add("out_01");
    list.add("out_02");
    list.add("out_03");
  }
}

在MyService类中,除了getName是一个有返回值的方法,其余的方法都是void类型的,另外在数据走向方面,除了setListData方法的第二个参数outList是输出类型的参数,其余的参数都是输入类型参数,所以这里将其它参数直接打印出来了。

调用IPC方法

在客户端想要调用Android的AIDL中定义的IPC方法,可以通过如下步骤实现:

  1. 首先需要定义一个相同包名相同目录的AIDL文件夹;
  2. 声明一个AIDL文件生成的接口实例;
  3. 实现ServiceConnection接口;
  4. 调用bindService绑定服务,传入生成的ServiceConnection实例;
  5. 在onServiceConnected()实现中,将收到的IBinder实例(名为 service)。调用 XXX.Stub.asInterface((IBinder)service),以将返回的参数转换为 AIDL生成的接口类型。
  6. 通过调用生成的AIDL接口实例中对应的方法就可以实现IPC调用了;
  7. 在不使用的时候解除服务的绑定Context.unbindService()。

如下是客户端Activity中代码的实现:

代码语言:javascript复制
public class MainActivity extends AppCompatActivity { 
  private static final String TAG = "AIDL_Client";
  private MyConnection conn;
  private IRemoteService service;
  private List<String  outList=new ArrayList< ();
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
  }
 
  public void startBind(View v) {
    Intent intent = new Intent();
    conn=new MyConnection();
    intent.setAction("com.sunny.server.service.MyService");
    bindService(intent, conn, Context.BIND_AUTO_CREATE);
  }
 
  public void startExecute(View v) {
    try {
      service.basicTypes(1, 10000L, true, 1.5f, 300.3, "Hello World");
 
      Log.d(TAG, "getName:"   service.getName());
      List<String  inList = new ArrayList<String ();
      inList.add("inList01");
      inList.add("inList02");
      service.setListData(inList, outList);
      Log.d(TAG, "outList:"   outList.toString()); 
      Map<String, String  map = new HashMap<String, String ();
      map.put("key01", "value01");
      map.put("key02", "value02");
      service.setMapData(map); 
      User user = new User();
      user.setId(1001);
      user.setName("admin");
      service.setUser(user);
    } catch (RemoteException e) {
      e.printStackTrace();
    }
  }
  private class MyConnection implements ServiceConnection { 
    public void onServiceConnected(ComponentName name, IBinder binder) {
      service = IRemoteService.Stub.asInterface(binder);
    } 
    public void onServiceDisconnected(ComponentName name) {
    }
  }
 
  @Override
  protected void onDestroy() {
    super.onDestroy();
    unbindService(conn);
  }
}

其它

上述示例只是为了介绍AIDL如何跨进程通信的,所以在客户端接收到的数据直接就在主线程中处理了。但是实际上客户端调用服务端的远程方法,被调用的方法运行在服务端的Binder线程池中的,同时客户端线程会被挂起,这时候如果服务端方法执行比较耗时,就会导致客户端长时间阻塞在这里,如果客户端方法位于UI线程中,可能会引起ANR。在实际开发的时候注意,客户端进行IPC通信的时候尽量放在子线程中。由于服务端的方法本身就是运行在服务端的Binder线程池中,所以即使服务端需要执行大量耗时的工作也不需要开启新的线程去执行。

另外一定要注意的就是安全性,默认情况下远程服务任何人都可以连接,这应该不是我们所需要的,所以还需要考虑一下权限验证。一般情况下有两种处理方法,第一种是通过自定义权限的方法,我们在服务端Service方法的onBinder()方法中添加权限验证,如果权限验证不通过直接返回null。另外一种就是在服务端的onTransact()方法中做验证,也是做权限验证,如果不通过直接返回false。除了上面讲的权限验证之外,可以通过getCallingPid()和getCallingUid()拿到客户端应用的Pid和Uid进行校验。

有关AIDL的介绍就先到这里了,后续继续介绍一下Binder有关内容。以上就是本文的全部内容,希望对大家的学习有所帮助。

0 人点赞