Android学习之旅-使用 AIDL 实现进程间通信01[艺术
什么是 AIDL
AIDL 全称是“Android”接口定义语言,在 Android 中进程之间是不能直接通过内存共享的方式进行数据通信,为了使其他的应用程序也可以访问本应用程序提供的服务,Android系统采用了远程过程调用(Remote Procedure Call,RPC)方式来实现。与很多其他的基于RPC的解决方案一样,Android使用一种接口定义语言(Interface Definition Language,IDL)来公开服务的接口。我们知道4个Android应用程序组件中的3个(Activity、BroadcastReceiver和ContentProvider)都可以进行跨进程访问,另外一个Android应用程序组件Service同样可以。因此,可以将这种可以跨进程访问的服务称为AIDL(Android Interface Definition Language)服务。
AIDL底层其实也是通过binder来进行进程间通信的,只要Android提供了AIDL这个工具来自动生成代码便捷了开发者而已,相关代码在 build->generated->source->aidl 文件夹下,这里只是举个例子,后面我会新建一个 aidl 文件一步一步的来,并且从源码分析 aidl 生成的java文件,顺带也复习一下相关的知识点。
如何使用 AIDL
image.png为了真实演示客户端与服务端的通信,我新建了两个module,分别为client和server,在client中创建 AIDL 文件,如图所示,创建ok后在 main 文件夹下会生成一个 aidl 的文件夹,如图所示,可以看到 aidl 文件的包名和 java 目录下的包名是一致的,这一点要值得注意。
image.png
新建 Book.java:
public class Book implements Parcelable {
private int bookId;
private String bookName;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.bookId);
dest.writeString(this.bookName);
}
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
protected Book(Parcel in) {
this.bookId = in.readInt();
this.bookName = in.readString();
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel source) {
return new Book(source);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
}
所有在进程间通信的实体类必须实现 Parcelable 接口,也就是序列化,在 IBookManager.aidl 中新建两个方法供调用。
// IBookManager.aidl
package com.example.tengfei.aidl;
import com.example.tengfei.aidl.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
你可以发现我在这里需要用到Book实体类,而这也是 aidl 的特殊之处,你还需要定义一个 Book.aidl ,尽管你已经定义了一个Book.java,在 aidl 中如果你需要用到任意一个实体类,那么此类首先必须序列化,其次必须同时定义一个此类的 aidl 文件,注意前面的 parcelable,是小写的,这是一个坑,务必要注意了。
同时需要注意在 AIDL 中支持以下数据类型:
1.基本数据类型
2.String 和 CharSequence
3.List:只支持 ArrayList 并且List的每一个数据都必须要被 AIDL 支持
4.Map:只支持 HashMap 并且 Map 里的每一个数据都必须要被 AIDL 支持
5.所有的实体类数据都被被 Parcelable 序列化
6.所有 AIDL 接口本身也可以在 AIDL 文件中使用
// Book.aidl
package com.example.tengfei.aidl;
parcelable Book;
其实到了这一步在 client 中 build 文件下已经生成了相关的代码可以供我们调用,但先不要关注这些代码,接下来新建服务端module,让我们首先实现一个简单的进程间通信的小demo,注意了,为了方便你的操作,你可以将与 aidl 相关的类全部拷贝到一个包下面,但是需要注意的是在AndroidStudio中如果将你的java文件也就是后缀是java的文件放在aidl文件夹中,但是编译的时候会报错提示你java文件找不到,解决办法是把java文件放在java文件夹下面,保持其包名不变与Book.aidl一致,第二种解决办法是修改 build.gradle 文件:在 android{} 中间加上下面的内容:
sourceSets {
main {
java.srcDirs = ['src/main/java', 'src/main/aidl']
}
}
在写服务端 aidl 文件的时候直接将这个包给copy过去就可以了,也避免了不必要的错误,接下来直接将我们在client中定义的 aidl 相关文件复制到 server 中,客户端与服务端的 aidl 相关文件的包名必须一致,但是我将Book.java放到java文件夹下,并确保它与Book.aidl包名一致
在服务端中定义一个 Service
public class MyService extends Service {
private CopyOnWriteArrayList<Book> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
private Binder binder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() {
return copyOnWriteArrayList;
}
@Override
public void addBook(Book book) {
copyOnWriteArrayList.add(book);
}
};
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onCreate() {
super.onCreate();
copyOnWriteArrayList.add(new Book(0x000,"基督山恩仇记"));
copyOnWriteArrayList.add(new Book(0x001,"海底两万里"));
}
}
之所以用到 CopyOnWriteArrayList 是因为 AIDL 方法是运行在binder线程池中的,当多个客户端同时连接的时候,会出现多个线程同时访问的情况。
在客户端中绑定服务端
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "MainActivityTAG";
private IBookManager iBookManager;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
iBookManager = IBookManager.Stub.asInterface(service);
try {
iBookManager.addBook(new Book(0x002, "神秘岛"));
iBookManager.addBook(new Book(0x003, "Android开发艺术探索"));
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.bt_client_bind).setOnClickListener(this);
findViewById(R.id.bt_client_add).setOnClickListener(this);
findViewById(R.id.bt_client_get).setOnClickListener(this);
}
@Override
protected void onDestroy() {
unbindService(connection);
super.onDestroy();
}
@Override
public void onClick(View v) {
int i = 0;
switch (v.getId()) {
case R.id.bt_client_bind:
i++;
Intent intent = new Intent("com.example.tengfei.server.MyService");
intent.setPackage("com.example.tengfei.server");
bindService(intent, connection, Context.BIND_AUTO_CREATE);
break;
case R.id.bt_client_add:
try {
iBookManager.addBook(new Book(i, "#" + i));
} catch (RemoteException e) {
e.printStackTrace();
}
break;
case R.id.bt_client_get:
List<Book> bookList = null;
try {
bookList = iBookManager.getBookList();
if ( bookList != null && !bookList.isEmpty()) {
for (Book book : bookList) {
Log.i(TAG, book.toString());
}
}
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
break;
}
}
}
代码很简单,首先定义一个 ServiceConnection ,在 onServiceConnected 回调方法中可以获取服务端返回的 IBinder 对象,也就是我们自定义Service中的 onBind 方法返回的对象,拿到这个对象后将它转换成我们所需要的 AIDL 接口类型的对象,紧接着就可以通过 IBookManager 来调用远程服务端的方法,在这里有个小插曲,我绑定服务时报了 Service Intent must be explicit ,后来我查资料发现在 Android5.0 版本以上时不允许隐式启动 Service,其实你可以在setAction之后调用 setPackage 将你要启动Service所在的应用的包名给传递进去,就可以正常绑定服务了,其实学习就是这样,你不要亲自动手实践的话很多问题你压根就发现不了,所以我选择在学习完艺术探索之后亲自实践一遍,并将所学的给记录下来增强自己的理解。
参考资料
1、百度百科 什么是 AIDL
2、Android开发艺术探索
3、如何解决Android 5.0中出现的警告:Service Intent must be explicit