AIDL叫Android接口定义语言,是用于辅助开发者完成Android跨进程编程的工具。
从某种意义上说AIDL其实是一个模板,因为在使用过程中,实际起作用的并不是AIDL文件,而是据此而生成的一个IInterface的实例代码,AIDL其实是为了避免我们重复编写代码而出现的一个模板。
设计AIDL这门语言的目的就是为了实现进程间通信。在Android系统中,每个进程都运行在一块独立的内存中,在其中完成自己的各项活动,与其他进程都分隔开来。可是有时候我们又有应用间进行互动的需求,比如传递数据或者任务委托等,AIDL就是为了满足这种需求而诞生的。通过AIDL,可以在一个进程中获取另一个进程的数据或调用其暴露出来的方法,从而满足进程间通信的需求
通常,暴露方法给其他应用进行调用的应用称为服务端,调用其他应用的方法的应用称为客户端,客户端通过绑定服务端的Service来进行交互
App开发中,webview的优化是个绕不开的,而且与native的相互通信经常会碰到crash情况,webview写到一个单独的进程中,通过aidl通信,就很大程度上避免了这种情况;
现在车机开发需求比较大,车机上的app相互通信的情况是比较多的,尤其是与语音的交互,只要需要让语音操作的App都是需要暴露接口给语音模块的;一般的暴露接口都是需要AIDL的方式来暴露;
AIDL的语法十分简单,与Java语言基本保持一致,但是需要记住以下几点规则:
AIDL文件以 .aidl 为后缀名
AIDL支持的数据类型分为如下几种:
AIDL文件可以分为两类。一类用来声明实现了Parcelable接口的数据类型,以供其他AIDL文件使用那些非默认支持的数据类型。还有一类是用来定义接口方法,声明要暴露哪些接口给客户端调用,定向Tag就是用来标注这些方法的参数值
定向Tag。定向Tag表示在跨进程通信中数据的流向,用于标注方法的参数值,分为 in、out、inout 三种。其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。此外,如果AIDL方法接口的参数值类型是:基本数据类型、String、CharSequence或者其他AIDL文件定义的方法接口,那么这些参数值的定向 Tag 默认是且只能是 in,所以除了这些类型外,其他参数值都需要明确标注使用哪种定向Tag。
明确导包。在AIDL文件中需要明确标明引用到的数据类型所在的包名,即使两个文件处在同个包名下
纸上得来终觉浅,绝知此事要躬行,show your the code;客户端通过绑定服务端的Service的方式来调用服务端的方法,实现模拟登录,获取书籍详情的的功能,实现应用间的数据共享。
先把 service 创建出来
public class AIDLService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
新建一个工程,包名就定义为 com.ze.aidlservice
首先,在应用中需要用到一个 Book 类,而 Book 类是两个应用间都需要使用到的,所以也需要在AIDL文件中声明Book类,为了避免出现类名重复导致无法创建文件的错误,这里需要先建立 BookManager AIDL 文件,之后再创建 Book 类
右键点击app新建一个AIDL文件,命名为 IMyBookManager。
会自动在main目录下生成IMyBookManager aidl文件;
然后我们来定义Book类,包含两个属性,name和price;
public class Book implements Parcelable {
private int price;
private String name = "";
public Book(){}
protected Book(Parcel in) {
price = in.readInt();
name = in.readString();
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public int describeContents() {
return 0;
}
public static final Creator CREATOR = new Creator() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
@Override
public String toString() {
return "\n图书名称:" + name + "\n图书价格:" + price;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(price);
dest.writeString(name);
}
}
然后修改IMyBookManager.aidl文件,增加了三个方法,和一个Book的声明,包括服务端需要暴露给客户端一个登录接口,和获取书籍详情,获取书籍价格的方法。
上面说过,在进程间通信中真正起作用的并不是 AIDL 文件,而是系统据此而生成的文件,可以在以下目录中查看系统生成的文件。之后需要使用到当中的内部静态抽象类 Stub
创建或修改过AIDL文件后需要重新makeproject下工程,使系统及时生成我们需要的文件;
现在修改 AIDLService供客户端远程绑定了。
public class AIDLService extends Service {
MyBookManager bookManager = new MyBookManager();
public AIDLService() {
Log.d("jackie","service,AIDLService");
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.d("jackie","service,onbind");
return bookManager;
}
public class MyBookManager extends IMyBookManager.Stub{
@Override
public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {
}
@Override
public String login(String userName, String pwd) throws RemoteException {
Log.d("jackie","service,login");
if(userName.equalsIgnoreCase("admin") && pwd.equalsIgnoreCase("123456")){
return "success";
}
return "error";
}
@Override
public Book queryByName(String bookName) throws RemoteException {
Log.d("jackie","service,queryByName");
Book book = new Book();
book.setName(bookName);
book.setPrice(100);
return book;
}
@Override
public String getBookPrice(String name) throws RemoteException {
return name+" :30";
}
}
}
可以看到, onBind 方法返回的就是 IMyBookManager.Stub 对象,实现当中定义的三个方法。
客户端要能够找到这个Service,可以通过先指定包名,之后再配置Action值或者直接指定Service类名的方式来绑定Service
如果是通过指定Action值的方式来绑定Service,那还需要将Service的声明改为如下所示:
标签名 | 含义 |
---|---|
android:enabled | 该服务能否被实例化,默认为true |
android:exported | 其他应用的组件能否和这个服务交互 |
导android:process | 设置独立进程,例如 remote 则进程名为 包名:remote |
我们采用配置 Action 的方案。
还有个需要特别需要注意的点:从Android 11 开始 系统对app的可见性进行了保护,只有部分应用可见所有app。如果系统是11以上,需要在AndroidManifest里面声明
创建新的工程,包名:com.ze.aidlclient;
首先,需要把服务端的AIDL文件以及Book类复制过来,将 aidl 文件夹整个复制到和Java文件夹同个层级下,不需要改动任何代码。
注意:Book类所在的包名要与服务的的包名相同。
布局文件:
核心代码:
public class FirstFragment extends Fragment {
private FragmentFirstBinding binding;
private boolean islogin=false;
private IMyBookManager bookManager = null;
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d("jackie","client,onServiceConnected");
bookManager = IMyBookManager.Stub.asInterface(service);
Toast.makeText(getContext(), "服务连接成功!", Toast.LENGTH_SHORT).show();
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d("jackie","client,onServiceDisconnected");
bookManager = null;
}
};
@Override
public View onCreateView(
LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState
) {
binding = FragmentFirstBinding.inflate(inflater, container, false);
return binding.getRoot();
}
private void connect() {
binding.btnConnection.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("jackie","client,btnConnection click");
if (bookManager != null) {
Log.d("jackie","client,btnConnection click bookManager != null");
Toast.makeText(getContext(), "服务连接成功!", Toast.LENGTH_SHORT).show();
} else {
Log.d("jackie","client,btnConnection click bookManager == null");
Intent intent = new Intent();
intent.setAction("com.ze.aidlservice.bookService");
intent.setPackage("com.ze.aidlservice");
// intent.setComponent(new ComponentName("com.yunzhou.aidlserver", "com.yunzhou.aidlserver.MyAidlService"));
// intent.setComponent(new ComponentName("com.ze.aidlservice","com.ze.aidlservice.Service.AIDLService"));
// getContext().bindService(intent, conn, Context.BIND_AUTO_CREATE);
boolean isConnet = getContext().bindService(intent, conn, Context.BIND_AUTO_CREATE);
Log.d("jackie","isConnet:::"+isConnet);
}
}
});
binding.btnLogin.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(bookManager == null)
{
Toast.makeText(getContext(), "还没有绑定服务!", Toast.LENGTH_SHORT).show();
return;
}
if(binding.etName.getText().toString().isEmpty() || binding.etPwd.getText().toString().isEmpty()){
Toast.makeText(getContext(), "用户名或密码不能为空!", Toast.LENGTH_SHORT).show();
return;
}
try {
String resStr = bookManager.login(binding.etName.getText().toString(),
binding.etPwd.getText().toString());
if(resStr.compareToIgnoreCase("success") == 0){
islogin=true;
Toast.makeText(getContext(), "登录成功!", Toast.LENGTH_SHORT).show();
} else{
Toast.makeText(getContext(), "登录失败!", Toast.LENGTH_SHORT).show();
throw new RemoteException("登录失败");
}
} catch (RemoteException e) {
e.printStackTrace();
Toast.makeText(getContext(), "登录失败!", Toast.LENGTH_SHORT).show();
}
}
});
binding.btnQuery.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(bookManager == null && islogin )
{
Toast.makeText(getContext(), "请先绑定服务并登录!", Toast.LENGTH_LONG).show();
return;
}
if(binding.etBookName.getText().toString().isEmpty()){
Toast.makeText(getContext(), "请输入您要查找的图书!", Toast.LENGTH_LONG).show();
return;
}
try {
Book book = bookManager.queryByName(binding.etBookName.getText().toString());
String price = bookManager.getBookPrice("jackie");
if(book != null){
binding.tvBookInfo.append(book.toString());
// binding.tvBookInfo.append(price);
} else{
throw new RemoteException("查询失败!");
}
} catch (RemoteException e) {
e.printStackTrace();
binding.tvBookInfo.append("查询失败!");
}
}
});
}
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
connect();
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
}
}
测试结果:
我们说一下定向tag:
AIDL 定义的接口文件,使用 in、out、inout 方向标签表明数据流向:
原始类型、String、IBinder 参数以及 AIDL 生成的接口参数 只能 用 in 修饰,可以不显式地写出来。
其他类型参数,主要是 Parcel 类型和 List、Map 类型,支持 out、inout,必须显式写出来。
大家只要自己试一下这些参数,并 Build 生成下 Java 文件,就可以发现它们的区别:
最后在强调一下:
从Android 11 开始 系统对app的可见性进行了保护,只有部分应用可见所有app。如果系统是11以上,需要在AndroidManifest里面声明queries,要不然客户端会拉不起服务。
讲到这里基本使用已经讲完,喜欢的点赞支持!