目录
一、IPC的基本概念
所谓IPC,是Inter-Process Communication的缩写,即跨进程通讯。说道进程,就要区别于线程:
- 线程是cpu调度的最小单元,同时线程是一种有限的系统资源
- 进程一般指一个执行单元,在PC和移动设备上指一个程序或一个应用,一个进程可以包含多的线程
二、IPC的几种方式
1.使用Bundle传输
这个在之前的文章《Intent传递数据》中有讲过,所以不再赘述了,思路就是将数据(对象)绑定在Bundle上,然后再将Bundle绑定在Intent上,最后通过Intent传递数据。
2.通过共享文件读写数据
这种IPC方式的思路是,多个进程通过共享同一个文件来交换数据。比如A进程把数据写入文件,然后B进程通过读取这个文件来获取数据。这个听起来就像SharedPreferences,因为SharedPreferences的底层实现就是采用XML文件来存储键值对,而这个文件就在/data/data/package name/shared_prefs目录下。因此,就像SharedPreferences在多进程模式下,面对高并发的读写访问,有很大几率丢失数据那样,文件共享方式的IPC仅适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写的问题。
3.使用Messenger传输
Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的跨进程传递了。
先来看一个客户端向服务端发送请求的样例:
因为样例是在一个应用程序中开启了多进程模式,因此服务端通过指定process属性来指定其运行所在进程:
<service
android:name=".messenger.MessengerService"
android:enabled="true"
android:exported="true"
android:process=":remote" />
1.服务端:创建一个服务来处理客户端请求,然后在其内部创建一个Handler并通过它创建一个Messenger对象,最后在onBind()方法中返回Messenger的底层Binder
public class MessengerService extends Service {
private static class MessengerHandler extends Handler{
}
private final Messenger mMessenger = new Messenger(new MessengerHandler());
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
}
2.客户端:bindService方式启动服务的基本流程。在ServiceConnection的onServiceConnected()方法中获取到服务端返回的Binder对象,并通过它构造Messenger对象,再借由Messenger发送Message传输数据。
public class MessengerActivity extends AppCompatActivity {
private static final String TAG = "MessengerActivity";
private Messenger mService;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = new Messenger(service);
//第二个参数即为msg.what的值
Message msg = Message.obtain(null,1);
Bundle data = new Bundle();
data.putString("msg","hello,this is client.");
msg.setData(data);
try {
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.i(TAG, "onCreate: ");
setContentView(R.layout.activity_messenger);
Intent intent = new Intent(this,MessengerService.class);
bindService(intent,connection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(connection);
super.onDestroy();
}
}
3.服务端:在客户端发出数据后,我们就可以接着来写服务端的数据接收工作了,重写Handler的handleMessage()方法
private static class MessengerHandler extends Handler{
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 1:
Log.i(TAG, "Receive msg from client: "+msg.getData().getString("msg"));
break;
default:
super.handleMessage(msg);
}
}
}
这么一来,我们就完成了客户端向服务端的单向跨进程通讯。这里有一个小细节,就是Message是可以通过obj来传递对象的,可为什么还要通过Bundle呢? 这是因为非系统的Parcelable对象无法通过obj字段来传输。因此,跨进程通讯时,Message中能使用的载体只有what、arg1、arg2、Bundle以及replyTo。
4.想要让服务端返回数据给客户端,其实操作上与上述类似。首先让我们处理一下服务端的返回数据:
private static class MessengerHandler extends Handler{
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 1:
Log.i(TAG, "Receive msg from client: "+msg.getData().getString("msg"));
Messenger client = msg.replyTo;
Message replyMessage = Message.obtain(null,2);
Bundle bundle = new Bundle();
bundle.putString("reply","I got,I'll reply soon.");
replyMessage.setData(bundle);
try {
client.send(replyMessage);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
}
可以看到,我们先通过msg.replyTo从客户端那里拿到一个Messenger对象client,然后通过它去发送一个回复用的Message对象replyMessage,需要回复的数据就通过它来传递。
5.上面用到了客户端传递的replyTo,目前还没有添加进去,now,let's do it。在客户端中编辑:
private Messenger mGetReplyMessenger = new Messenger(new MessengerHandler());
private static class MessengerHandler extends Handler{
}
首先得为客户端声明一个Messenger的成员变量,和服务端一样,先创建一个Handler然后通过它创建一个Messenger。
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = new Messenger(service);
Message msg = Message.obtain(null,1);
Bundle data = new Bundle();
data.putString("msg","hello,this is client.");
msg.setData(data);
//变更在这里
msg.replyTo = mGetReplyMessenger;
try {
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
然后在发送请求的同时,将上述声明的Messenger成员变量mGetReplyMessenger通过replyTo字段传递给服务端。
6.客户端:最后编辑一下接收服务端返回数据的工作,重写Handler的handleMessage()方法
private static class MessengerHandler extends Handler{
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case 2:
Log.i(TAG, "Receive msg from service: "+msg.getData().getString("reply"));
break;
default:
super.handleMessage(msg);
}
}
}
4.使用AIDL传输
4.1.什么是AIDL
AIDL是android interface description language的缩写,即Android接口描述语言。使用AIDL是为了方便系统为我们生成代码从而实现跨进程通讯。
那么如何理解使用AIDL实现的IPC呢?我个人理解为:假如现在有一家无良公司,把所有员工都关在各自的房间里专心工作,不允许他们私下交流。那么员工A(进程A)想和员工B(进程B)商量一下工作相关的事情(通讯)该怎么办呢,这个时候公司就派出一个管理员(Binder驱动),让他做了一张表(ServiceManager)用来记录所有员工的名字和门牌号,每个员工都只能联系这个管理员。这么一来,当员工A想联系员工B的时候,就先呼叫管理员,问他B的门牌号,然后等管理员查表告诉他门牌号之后,A再写封信让管理员送到指定门牌号的地方。然而有一天B终于忍受不了A每天向他炫耀按摩椅有多舒服,他就去问管理员能不能让A把按摩椅送过来。管理员告诉他,不行,只能寄信。于是A就把按摩椅的购买地址写在纸上(序列化)寄给B,B再去那个地方给自己买了一个按摩椅(反序列化)。
4.2.使用AIDL文件的注意事项
1.AIDL文件(*.aidl的文件)仅支持以下六种数据类型
- 基本数据类型(int,long,char,boolean,double等)
- String和CharSequence
- List:只支持ArrayList,里面的每个元素都必须能够被AIDL支持
- Map:只支持HashMap,里面的每个元素都必须被AIDL支持,包括key和value
- Parcelable:所有实现了Parcelable接口的对象
- AIDL:所有的AIDL接口本身也可以在AIDL文件中使用
2.如果AIDL文件中使用到了自定义的Parcelable对象,那么需要新建一个与它同名的AIDL文件,并在其中声明它为parcelable。比如在IBookManager.aidl文件中要使用序列化的Book对象,那么需要额外新建Book.aidl,如下
//Book.aidl
package com.xxxx.xxxxx.ipcdemo.aidl;
parcelable Book;
3.如果AIDL文件中使用到了自定义的Parcelable对象或者AIDL对象,不管它们是否和当前的AIDL文件位于同一个包内,都必须显示import进来。比如在IBookManager.aidl文件中要使用序列化的Book对象,那么需要在最开始将 Book import 进来,如下
//IBookManager.aidl
package com.xxxx.xxxxx.ipcdemo.aidl;
import com.xxxx.xxxxx.ipcdemo.aidl.Book;
interface IBookManager {
}
4.AIDL中除了基本数据类型,其他类型的参数必须标上方向:in、out、inout。其中in表示输入型参数,out表示输出型参数,inout表示输入输出型参数。比如在IBookManager接口中定义两个方法,其中addBook方法的参数指定为输入型
//IBookManager.aidl
package com.xxxx.xxxxx.ipcdemo.aidl;
import com.xxxx.xxxxx.ipcdemo.aidl.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
5.AIDL接口中只支持声明方法,不支持声明静态常量。(区别于传统的接口)
6.建议将所有和AIDL相关的类和文件全部放入同一个包中,以便将整个包复制到客户端中。因为AIDL的包结构在服务端和客户端要保持一致,否则反序列化会出错。
4.3.使用AIDL的注意事项
在使用AIDL传输的时候,由于数据的载体是Binder,因此要注意以下两点:
- 客户端发起远程请求时,当前线程会被挂起直到服务端进程返回数据,因此不能在UI线程中发起远程请求
- 服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了
但是Messenger方式传输的底层实现不也是AIDL么?为什么它就没有在服务端采用同步锁呢?这是因为Messenger传输一次只处理一个请求,服务端中不存在并发执行的情形,因此在服务端 我们不用考虑线程同步的问题。
4.4.使用AIDL实现IPC
Demo场景:
服务端是一个图书馆,客户端可以查询服务端的所有书籍。然后简单起见,只再额外添加一个方法允许客户端请求服务端添加书籍(addBook方法)。
1.创建AIDL文件包(此步骤中所有新建文件都在该包内)
在app/src/main目录下新建一个AIDL文件夹(与java目录同级,具体新建方式可以右键main,New→Folder→AIDL Folder)
然后编辑app/build.gradle文件,在android闭包中添加如下代码使上述文件夹生效
sourceSets {
main {
java.srcDirs = ['src/main/java', 'src/main/aidl']
}
}
再来新建Book实体类,让其实现Parcelable接口
package ein.aidlserver;
import android.os.Parcel;
import android.os.Parcelable;
public class Book implements Parcelable{
private String bookName;
private int bookId;
public Book(String bookName, int bookId) {
this.bookName = bookName;
this.bookId = bookId;
}
protected Book(Parcel in) {
bookName = in.readString();
bookId = in.readInt();
}
public static final Creator<Book> CREATOR = new Creator<Book>() {
@Override
public Book createFromParcel(Parcel in) {
return new Book(in);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(bookName);
dest.writeInt(bookId);
}
//get,set方法
......
}
参考使用AIDL文件注意事项的第2条,新建Book.aidl文件
package ein.aidlserver;
parcelable Book;
最后新建AIDL的接口文件IBookManager.aidl。注意即使和Book.java文件同在ein.aidlserver包下,也需要手动import
package ein.aidlserver;
import ein.aidlserver.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
PS:这个时候我们手动Rebuild Project就可以发现系统为我们生成的接口文件IBookManager.java了。具体位置如下
分析这个文件我们就可以知道AIDL是如何实现IPC的。在这里我推荐这位前辈的博文深入浅出AIDL(二)。
2.通讯桥梁搭好了之后,我们再来创建服务端(此时的代码已经不在上述的AIDL包中了哟)
首先,新建一个服务BookManagerService用来接收客户端的请求。
public class BookManagerService extends Service {
private static final String TAG = "BookManagerService";
//这里的CopyOnWriteArrayList支持并发读写,用它来处理线程同步问题
private CopyOnWriteArrayList<Book> mBooklist = new CopyOnWriteArrayList<>();
private Binder mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return mBooklist;
}
@Override
public void addBook(Book book) throws RemoteException {
mBooklist.add(book);
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onCreate() {
super.onCreate();
mBooklist.add(new Book("The Da Vinci Code",1));
mBooklist.add(new Book("Thinking in Java",2));
}
}
可以看到,在onBind方式启动服务的时候,我们是需要返回一个IBinder对象的。然后我们发现系统生成的IBookManager有个内部类Stub继承了Binder,所以直接拿过来用
public static abstract class Stub extends android.os.Binder implements ein.aidlserver.IBookManager {
同时Stub又实现了IBookManager接口,因此要重写我们在AIDL文件中定义的那两个方法。最后在onCreate方法里面暂时添加两本书。
然后为了客户端能简单的调用该服务,我们在AndroidManifest中为该服务指定一下action属性(这里采用了包名加类名)
<service
android:name=".BookManagerService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.xxxx.xxxxx.aidldemoserver.BookManagerService"/>
</intent-filter>
</service>
3.客户端的创建
首先要做的就是把AIDL文件包(第一步创建的那个)整个拷贝到客户端的工程里面来(同样放在与java同级的位置)。
然后,客户端直接通过onBind的方式绑定远程服务,再通过服务端返回的Binder对象转换成AIDL接口IBookManager,这样就可以通过它调用服务端的远程方法了。
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
//由于目前方法体是执行在UI线程中的,假设getBookList方法耗时,需要再开子线程去执行
List<Book> list = bookManager.getBookList();
for(Book book : list)
Log.d(TAG, "query book list: "+"book id is "+book.getBookId()+" book name is "+book.getBookName());
} 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);
//绑定服务
Intent intent = new Intent();
intent.setAction("com.xxxx.xxxxx.aidldemoserver.BookManagerService");
intent.setPackage("com.xxxx.xxxxx.aidldemoserver");
bindService(intent,connection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
unbindService(connection);
super.onDestroy();
}
}
这样一来,在手机装上上面的两个应用,打开客户端便能看到日志中输出了服务端的图书列表。接着我们再来尝试添加书籍
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
//由于目前方法体是执行在UI线程中的假设getBookList方法耗时,需要再开子线程去执行
List<Book> list = bookManager.getBookList();
for(Book book : list)
Log.d(TAG, "query book list: "+"book id is "+book.getBookId()+" book name is "+book.getBookName());
//添加书籍
bookManager.addBook(new Book("In the Cases",3));
list = bookManager.getBookList();
for(Book book : list)
Log.d(TAG, "query book list: "+"book id is "+book.getBookId()+" book name is "+book.getBookName());
} catch (RemoteException e) {
e.printStackTrace();
}
}
重新运行可见如下日志
5.使用ContentProvider
可参考我之前的博文ContentProvider