上一篇的service涉及到进程通信问题,主要解决办法是通过 messenger来发送消息,这也是Google推荐的进程通信方式,比较简单易懂嘛~~,messenger底层也是通过binder来实现的,对于binder,这里就不做介绍了。但是如果允许不同应用的客户端用 IPC 方式访问服务、在服务中处理多线程就不太适合了,还是得乖乖用AIDL,AIDL这玩意,估计不少人都会有点陌生吧,接来下就通过跨应用对书本进行增删查,来讲解如何通过AIDL实现 跨进程通信。
一、定义AIDL接口
在开始设计 AIDL 接口之前,要注意 AIDL 接口的调用是直接函数调用。不应该假设发生调用的线程。视调用来自本地进程还是远程进程中的线程,实际情况会有所差异。具体而言:
- 来自本地进程的调用在发起调用的同一线程内执行。如果该线程是主 UI 线程,则该线程继续在 AIDL 接口中执行。如果该线程是其他线程,则其便是在服务中执行代码的线程。因此,只有在本地线程访问服务时,我们才能完全控制哪些线程在服务中执行(但如果真是这种情况,根本不应该使用 AIDL,而是应该通过实现 Binder 类创建接口)。
- 来自远程进程的调用分派自平台在自有进程内部维护的线程池。必须为来自未知线程的多次并发传入调用做好准备。换言之,AIDL 接口的实现必须是完全线程安全实现。
oneway
关键字用于修改远程调用的行为。使用该关键字时,远程调用不会阻塞;它只是发送事务数据并立即返回。接口的实现最终接收此调用时,是以正常远程调用形式将其作为来自Binder 线程池的常规调用进行接收。如果oneway
用于本地调用,则不会有任何影响,调用仍是同步调用。
我们一般会定义一个AIDL文件(当然,也可以通过实现binder类来自己创建接口,不过暂时没那必要)。首先创建两个应用client端和service端,其中client端主要用于查询,增加数据,service端则负责实现数据存储。
先看service端,我们在src\main目录下创建aidl 包,在aidl下再创建一个目录来存放接口,同时在java目录下创建相同的目录,总体目录如下:
我们先来看aidl路径下的IBookManager.aidl 接口,很简单,就定义一个接口供client调用
package com.example.aidl;
import com.example.aidl.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
boolean removeBook(in Book book);
}
在AIDl文件中可以使用的数据类型有:
- Java 编程语言中的所有原语类型(如
int
、long
、char
、boolean
等等) String
CharSequence
List
List
中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。可选择将List 用作“通用”类(例如,List<String>
)。另一端实际接收的具体类始终是ArrayList
,但生成的方法使用的是List
接口。Map
Map
中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。不支持通用 Map(如Map<String,Integer>
形式的 Map)。另一端实际接收的具体类始终是HashMap
,但生成的方法使用的是Map
接口。
我们注意到这里的接口方法比一般的Java接口多了一个“in” ,在AIDl文件中规定除了基本数据类型外,其他数据必须表上方向:in ,out或者inout。
1)in代表输入型参数(服务端将会收到客户端对象的
完整数据,但是客户端对象
不会因为服务端对传参的修改而发生变动)
2)out代表输出型参数(服务端将会收到
客户端对象,该
对象不为空,但是它里面的
字段为空,但是在服务端对该对象作任何修改之后客户端的传参对象都会
同步改动)
3)inout 代表输入输出型参数(服务端将会接收到客户端传来
对象的完整信息,并且客户端将会
同步服务端对该对象的任何变动)。
AIDL接口只支持方法,不支持声明静态常量。
由于我们使用到book这个对象,因此,需要声明该对象的AIDL文件,并在AIDL接口里面手动导入该文件,该文件声明如下
package com.example.aidl;
parcelable Book;
转到Java目录下,在相同的包下创建一个book对象,并使其实现Parcelable接口(AIDL要求接口中的类必须要实现Parcelable接口,并在AIDL目录下声明该类为parcelable,如上所示)
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName ;
}
private Book(Parcel parcel){
bookId = parcel.readInt();
bookName = parcel.readString() ;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
public static final Creator<Book> CREATOR = new Creator<Book>(){
@Override
public Book createFromParcel(Parcel parcel) {
return new Book(parcel);
}
@Override
public Book[] newArray(int i) {
return new Book[i];
}
};
}
关键来了!!(敲黑板) 准备工作做完后,我们就可以生成相应的binder类了,选择build->make project即可,gradle会自动帮我们创建一个IBookManager的Java接口,生成的接口包括一个名为 Stub 的子类,这个子类是其父接口(例如,IBookManager.Stub)的抽象实现,用于声明 .aidl 文件中的所有方法。
二、实现AIDL接口
我们创建一个BookManagerService 并继承service,创建一个binder对象
private Binder binder = new IBookManager.Stub(){
@Override
public List<Book> getBookList() throws RemoteException {
return null;
}
@Override
public void addBook(Book book) throws RemoteException {
}
@Override
public boolean removeBook(Book book) throws RemoteException {
return false ;
}
};
现在,binder 是 Stub
类的一个实例,用于定义服务的 RPC 接口。在下一步中,将向客户端公开该实例,以便客户端能与服务进行交互。
在实现 AIDL 接口时应注意遵守以下这几个规则:
- 由于不能保证在主线程上执行传入调用,因此一开始就需要做好多线程处理准备,并将服务正确地编译为线程安全服务。
- 默认情况下,RPC 调用是同步调用。如果明知服务完成请求的时间不止几毫秒,就不应该从Activity的主线程调用服务,因为这样做可能会使应用挂起(Android可能会显示“Application is Not Responding”对话框)— 我们通常应该从客户端内的单独线程调用服务。
- 在跨进程通信引发的任何异常都不会回传给调用方。
三、向client端公开该接口
我们通过重写onBind 方法来返回该bind接口
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "onBind: ");
return binder;
}
现在,当客户端(如 Activity)调用 bindService() 以连接此服务时,客户端的 onServiceConnected() 回调会接收服务的 onBind() 方法返回的 binder实例。
最后我们在配置文件中声明该service,并设置其可以被其他应用调用
<service android:name="com.example.aidl.BookManagerService"
android:enabled="true"
android:exported="true" />
至此,service端的准备工作就基本完成了,再来看看client端的
四、client端的实现
同service端一样,我们需要在client端创建一样的路径(因为底层采用对象的反序列化,路径必须一致),直接从service端copy一份过来即可,点击make project 创建对应的Java接口类,client端的准备工作就好了
public void bindService(View view){
Intent intent = new Intent() ;
intent.setComponent(new
ComponentName("com.ujs.service"
,"com.example.aidl.BookManagerService"));
bindService(intent,serviceConnection, Context.BIND_AUTO_CREATE);
}
这里我们用到intent.setComponent()方法,这是Android为了方便启动其他应用的组件而提供的,ComponentName有两个参数,第一个是目标应用的包名,第二个参数是目标组件的详细路径,然后我们传入一个serviceconnection,在onServiceConnected方法中获取service端的接口,这样我们就可以愉快的通过这个接口与service通信了
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(TAG, "onServiceConnected: - ");
iBookManager = IBookManager.Stub.asInterface(service);
try {
List<Book> list = iBookManager.getBookList();
for(Book b:list){
Log.i(TAG, "onServiceConnected: "+b.bookName);
Log.i(TAG, "onServiceConnected: "+b.bookId);
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.i(TAG, "onServiceDisconnected: ");
}
};
我们可以通知service添加一本书
public void addBook(View view){
try {
Book web = new Book(1,"web App");
iBookManager.addBook(web);
Log.i(TAG, "addBook: "+web.toString());
Log.i(TAG, "addBook: 添加数据了");
} catch (RemoteException e) {
e.printStackTrace();
}
}
从service端获取所有书本信息
public void upDate(View view){
try {
List<Book> list = iBookManager.getBookList();
for(Book book : list){
Log.i(TAG, "\nbook name : "+book.bookName+" book id :"+book.bookId);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
通知service端删除一本书,并获取返回值
public void remove(View view){
try {
boolean b = iBookManager.removeBook(web);
if(b)
Log.i(TAG, "remove: service 删除数据了");
else
Log.i(TAG, "remove: 删除失败了");
} catch (RemoteException e) {
e.printStackTrace();
}
}
至此,基于AIDL进程通信就打通了,掌握这些就满足基本的进程通信需求了~~