1、跨进程通信
要想了解AIDL的用途,我们先来上个图,聊聊跨进程通信。
图上有进程1、进程2两个进程,如果进程1想要和进程2通信,或者说进程1想要共享数据给进程2,那该怎么做。Android中两个进程想要直接进行通信是不能的,一个应用程序没办法和另一个应用程序进行通讯,这样就保证了数据的安全性,保证一个进程的销毁不会影响到其它进程。
所以谷歌就提供了如图的这样一种间接的方法,进程下面的矩形就是我们的Android系统,进程1进程2都是基于Android系统之上的,所以这两个进程只能让Android系统来帮忙。如果进程1想要向进程2提出一些请求,就会先把它的请求发送给系统,系统收到请求后根据请求的标识找到进程2,把进程2里的相关信息传递回进程1。应用程序间想要通信就是这样完成的。
所以通过上面的说明,跨进程通信可以总结为两点:
- 两个进程无法直接通信
- 通过Android系统底层间接通信
要实现跨进程通信,谷歌在SDK中给我们提供了四种方式,分别对应我们的四大组件,这里我们要讲的就是基于Service的方式,使用的语言就是AIDL。
2、AIDL简介
AIDL (Android Interface Definition Language), Android接口定义语言,Android提供的IPC (Inter Process Communication,进程间通信)的一种独特实现。
我们可以利用它定义客户端与服务使用进程间通信 (IPC) 进行相互通信时都认可的编程接口。 在 Android 上,一个进程通常无法访问另一个进程的内存。 既然如此,进程需要将其对象分解成操作系统能够识别的原语,并将对象编组成跨越边界的对象。 编写执行这一编组操作的代码是一项繁琐的工作,因此 Android 会使用 AIDL 来处理。
注:只有允许不同应用的客户端用 IPC 方式访问服务,并且想要在服务中处理多线程时,才有必要使用 AIDL。 如果您不需要执行跨越不同应用的并发 IPC,就应该通过实现一个 Binder 创建接口;或者,如果您想执行 IPC,但根本不需要处理多线程,则使用 Messenger 类来实现接口。
总结可为AIDL使用于IPC、多线程、多个应用程序,Binder使用于IPC、非多线程、多个应用程序,Messenger则是只要做IPC时使用。
3、AIDL文件
AIDL的语法与Java接口是类似的,但AIDL是使用.aidl文件,也就是说必须使用 Java 编程语言语法在 .aidl 文件中定义 AIDL 接口。然后将它保存在托管服务的应用以及任何其他绑定到服务的应用的源代码(src/ 目录)内。
我们开发每个包含 .aidl 文件的应用时,Android SDK 工具都会生成一个基于该 .aidl 文件的 IBinder 接口,并将其保存在项目的 gen/ 目录中。服务必须视情况实现 IBinder 接口。然后客户端应用便可绑定到该服务,并调用 IBinder 中的方法来执行 IPC。
我们来看看.aidl文件是如何定义的:
// 包名,文件必须放在指定的目录
package com.example.android;
// interface 关键字
// 文件名必须对应接口名,IRemoteService.aidl
interface IRemoteService {
// 方法
int getPid();
}
这就是大致的.aidl文件的书写格式,和Java几乎是一样的。除了在IDE中可以将.aidl编译成Java文件,我们也可以通过命令直接手动编译。
我们在SDK的build-tools下某个android版本里可以看到aidl.exe,我们把这个目录添加到环境变量中就可以使用了。
现在我在D盘下创建了这个目录,我们打开cmd:
cd D:\com\example\android
aidl IRemoteService.aidl
到达指定目录后执行这条命令即可生成Java文件。
不过在开发中我们使用的IDE会帮我们对.aidl文件做处理,这个知识只是给大家了解一下。我们在开发Android时最好使用Android Studio,它的功能真的十分强大。我们在eclipse中需要去改文件,而Android Studio已经把AIDL处理的很完备。
AIDL的文件都会放在一个aidl的文件夹下,这个文件夹是与java文件夹同级的,我们就可以把.aidl的文件都放在这里,这样就比eclipse清晰的多啦。但Android Studio在生成和修改.aidl文件的时候时不会编译产生Java文件的,所以我们需要手动编译一下。
Make Project之后,我们就可以在build中找到生成的Java文件:
这样我们就可以在Java中使用这个接口了。
4、AIDL案例
这里我们实现一个如何在项目中使用AIDL的案例。像在一个大的项目中,已经实现了许多的进程模块,完全够我们使用了。这时我们要开始一个新的项目,考虑到成本,时间等各种问题,我们是没有必要去重复开发的,所以我们就要让新项目去访问旧项目的其中一些服务。
从图中可以知道我们的这个案例,就是点击Button调用服务端的接口方法,传入两个数,然后服务端将结果返回到我们的TextView中。因为这是两个进程,所以就要用到我们的AIDL。
实现这个例子我们需要在Android Studio中创建两个项目,我们先来处理服务端。
创建好项目后,我们在aidl目录下创建一个.aidl文件,文件名就叫ICalculateAIDL。
interface ICalculateAIDL {
int add(int num1, int num2);
}
这个AIDL接口是实现好了,那我们怎么去实现它将两个数相加呢,官方文档是这么说得:
当您开发应用时,Android SDK 工具会生成一个以 .aidl 文件命名的 .java 接口文件。生成的接口包括一个名为 Stub的子类,这个子类是其父接口(例如,YourInterface.Stub)的抽象实现,用于声明 .aidl 文件中的所有方法。如需实现 .aidl 生成的接口,请扩展生成的 Binder 接口(例如,YourInterface.Stub)并实现从 .aidl 文件继承的方法。
所以我们需要创建一个继承Service的Java文件,去处理.Stub接口。
public class ICalculateService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
private IBinder iBinder = new ICalculateAIDL.Stub() {
@Override
public int add(int num1, int num2) throws RemoteException {
return num1 + num2;
}
};
}
这样我们就实现好了AIDL的接口,最后一步就是向客户端公开该接口,要知道我们的IBinder对象是不可能让外界去访问的,但我们必须让客户端知道要调用的方法是什么。
其实要实现也很简单,对Service有了解的都知道,客户的要想使用一个服务就要调用bindService()方法,绑定服务时就会执行onBind(),所以我们把这个IBinder对象在onBind()方法中返回即可。
@Override
public IBinder onBind(Intent intent) {
return iBinder;
}
这样客户端绑定服务时,它的 onServiceConnected() 回调会接收服务的 onBind() 方法返回的 IBinder 实例。
当然不要在Manifest中注册我们的服务,否则是不能被调用的:
<service
android:name=".ICalculateService"
android:exported="true"
android:process=":remote">
</service>
我们可以新建一个Module来实现客户端,首先首先我们的页面布局:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.ht.aidlsecond.MainActivity">
<EditText
android:id="@+id/et_num1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
<TextView
android:text="+"
android:textSize="30sp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<EditText
android:id="@+id/et_num2"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:text="="
android:textSize="30sp"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/result"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<Button
android:id="@+id/btn_add"
android:text="AIDL远程计算"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
客户端还必须具有对 interface 类的访问权限,因此如果客户端和服务在不同的应用内,则客户端的应用 src/ 目录内必须包含 .aidl 文件(它生成 android.os.Binder 接口 — 为客户端提供对 AIDL 方法的访问权限)的副本。
所以我们还需要把在服务端应用中的.aidl文件复制一份到客户端中,注意要包名一致:
我们要拿到AIDL的接口服务,因为是在onBind()里,所以当然要让客户端尝试去绑定这个服务。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private EditText edtNum1, edtNum2;
private TextView result;
private Button mAddBtn;
ICalculateAIDL mICalculateAIDL;
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mICalculateAIDL = ICalculateAIDL.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mICalculateAIDL = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initViews();
bindService();
}
private void bindService() {
//获取服务端
Intent intent = new Intent();
//必须以显式Intent启动,绑定服务
intent.setComponent(new ComponentName("com.ht.aidlstudy",
"com.ht.aidlstudy.ICalculateService"));
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
private void initViews() {
edtNum1 = (EditText) findViewById(R.id.et_num1);
edtNum2 = (EditText) findViewById(R.id.et_num2);
result = (TextView) findViewById(R.id.result);
mAddBtn = (Button) findViewById(R.id.btn_add);
mAddBtn.setOnClickListener(this);
}
@Override
public void onClick(View v) {
int num1 = Integer.parseInt(edtNum1.getText().toString());
int num2 = Integer.parseInt(edtNum2.getText().toString());
try {
Log.d("TAG", num1 + " " + num2);
int res = mICalculateAIDL.add(num1, num2);
result.setText("" + res);
} catch (RemoteException e) {
e.printStackTrace();
result.setText("Error");
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(mServiceConnection);
}
}
我们初始化控件之后,就开始绑定我们的服务,这里我们要用显式Intent,服务的包名和类名我们不能弄错。bindService()需要ServiceConnection,前面说过在绑定服务时,onServiceConnected就会帮我们接收onBind()返回的值,就是参数里的service,我们用asInstance()方法就可以获得我们的AIDL接口。最后在点击事件里调用接口实例的方法即可。当然不要忘了在程序结束是解绑服务。
5、支持数据类型
默认情况下,AIDL支持下列数据类型:
- Java 编程语言中的所有基本类型(如 int、long、char、boolean 等等)除了short外
- String
- CharSequence
- List:List 中的所有元素都必须是以上列表中支持的数据类型、其它 AIDL 生成的接口或我们自己声明的可打包类型。 可选择将 List 用作“通用”类(例如,
List<String>
)。另一端实际接收的具体类始终是 ArrayList,但生成的方法使用的是 List 接口。也就是在客户端用ArrayList去接收。 - Map:Map 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。 不支持通用 Map(如
Map<String, Integer>
形式的 Map)。 另一端实际接收的具体类始终是 HashMap,但生成的方法使用的是 Map 接口。
这里还需要注意,举例我们如果在AIDL中使用List,那么单单用List<String> aList
来申明是不行的。
因为我们的Android系统在底层操作的都是一些非常基本的数据,所以如果我们要传的是非常大的数据,那就必须打碎成小的数据,而在另一边在界面看到的还是大的数据,所以还有一个还原的过程。而这个转换的过程非常耗内存和资源,所以我们要在它们编译的过程中告诉它们是到底是输入的还是输出的,这样就大大减少了资源,这个就是打包。
所有非基本参数都需要指示数据走向的方向标记。可以是 in、out 或 inout。基本类型默认为 in,不能是其他方向。
所以如果参数是List,我们要给它加上走向in List<String> aList
。
6、传递对象
通过上面的介绍,我们知道了AIDL可以支持各种基本类型,但这样是不是有点太局限了,我们在实际开发中不可能只使用基本的数据类型,大部分情况我们传递的都会是一个个对象,AIDL不可能没有处理的方法,我们用得就是Parcelable序列化。
这里还是写一个例子,大家通过这个过程来看看如何实现。
既然是使用对象,我们就先创建一个实体类,就叫Person。
public class Person implements Parcelable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
protected Person(Parcel in) {
name = in.readString();
age = in.readInt();
}
public static final Creator<Person> CREATOR = new Creator<Person>() {
@Override
public Person createFromParcel(Parcel in) {
return new Person(in);
}
@Override
public Person[] newArray(int size) {
return new Person[size];
}
};
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(age);
}
}
支持 Parcelable 接口很重要,因为 Android 系统可通过它将对象分解成可编组到各进程的原语,也就是我们在上图说得将不是基本的数据打成碎片。
Android Studio的功能很强大,我们一让它实现序列化的方法,它就帮我们把序列化的操作都根据我们Person类的变量实现好了,我们来看看这些方法的作用。
writeToParcel(Parcel dest, int flags)方法就是把我们在这边的数据都封成dest这么个序列化的包,那么在另一边就会从这个包去取数据。打包我们只要用writeString(),writeInt()方法把对应的变量写入即可,接下来就是如何去取数据。
这个CREATOR的写法是固定的,我们不能改变它,它的两个方法都容易明白作用,一个是创建数组,这个将对应的size传入即可,另一个就是从序列化的包里将数据一个个的取出来,要注意的是取数据的顺序必须和writeToParcel中写入数据的顺序一致,也就是谁先被写入,谁就先被取出。
虽然我们已经有了Person这么一个实现了序列化的打包类,但是现在仍然不能使用Person这个类,AIDL依然不能识别。我们还需要去创建一个声明可打包类的 .aidl 文件。
// Person.aidl
package com.ht.aidlstudy;
parcelable Person;
// IMyAidlInterface.aidl
package com.ht.aidlstudy;
import com.ht.aidlstudy.Person;
interface IMyAidlInterface {
List<Person> add(in Person person);
}
最后用import导入Person类所在的包,我们就可以顺利编译它啦。
我们这里就不重写代码了还是用上面进行远程计算的界面,这里要实现的是每次点击Button,就传入一个Person对象到服务端,服务端将Person对象放入List集合里,并将其返回给客户端,客户端用Log打印出来。
public class IRemoteService extends Service {
private ArrayList<Person> persons;
@Override
public IBinder onBind(Intent intent) {
persons = new ArrayList<Person>();
return mIBinder;
}
private IBinder mIBinder = new IMyAidlInterface.Stub() {
@Override
public List<Person> add(Person person) throws RemoteException {
persons.add(person);
return persons;
}
};
}
因为只有在绑定的时候才会创建List对象,所以如果连按了几次Button,List集合就会不断更新,返回的List集合里的数据不断增加。
因为接口的更新所以我们在客户端项目里也要把IMyAidiInterface.aidl和Person.aidl复制过去,而Person类也是如此,大家一定要注意包名,Person.aidl和Person.java的包名必须是一致的。
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button mAddBtn;
private IMyAidlInterface myAidlInterface;
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
myAidlInterface = IMyAidlInterface.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
myAidlInterface = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mAddBtn = (Button) findViewById(R.id.btn_add);
mAddBtn.setOnClickListener(this);
bindService();
}
private void bindService() {
//获取服务端
Intent intent = new Intent();
//必须以显示Intent启动,绑定服务
intent.setComponent(new ComponentName("com.ht.aidlstudy",
"com.ht.aidlstudy.IRemoteService"));
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
@Override
public void onClick(View v) {
try {
List<Person> persons = myAidlInterface.add(new Person("李四", 43));
Log.i("TAG", persons.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(mServiceConnection);
}
}
toString()如果没有重写返回的就是地址,所以我们要给Person加上toString()方法,注意服务端和客户端的代码必须是一致,要修改就要一起修改。
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
我点击了四次Button,可以看到我们返回的List数据已经不一样啦。
7、AIDL实现原理
经过上面的分析和学习,相信大家都已经可以使用AIDL实现跨进程间通信了,大家都知道要在客户端用asInterface()方法获得远程服务,那么AIDL到底是如何处理的呢。大家可以看到自己项目中的.aidl文件编译产生的.java文件,我就不在这里展示了。
大家仔细观察,可以看到我们的IMyAidlInterface.java里除了最外层就是本身这个接口,除此之外就是几个方法和类。首先是Stub(存根)类,我们在获取服务的时候调用的就是这个类,所以我来依着这个顺序看下去。
可以看到如果这个IBinder对象不为空,asInterface()方法就会调用Proxy(代理)类,而Proxy类里就有我们的add()方法,这就是我们方法实现的地方,在里面我们看到使用了transact()方法,之后就又会调用onTransact(),在这个方法中我们能看到write等等的方法,就是在这里实现数据处理的。我们来看看这个图。
总结来说,我们在服务端new了一个接口的Stub,里面定义了我们接口的方法,但实际上我们要实现的是一个存根类的方法onTransact()。我们在客户端调用asInterface()的时候,获得的并不是Service,而是在asInterface()上拿到了Proxy这个代理,它会把我们的请求通过transact到底层,到Stub的onTransact()方法,它里面再调用了我们服务端里的方法,最终实现了整个跨进程通信。
结束语:本文仅用来学习记录,参考查阅。