前言
本文主要从以下几个方面来介绍IPC机制:
IPC是什么?- 为什么要学
IPC?- 怎么进行
IPC?
下面是我这篇博文的学习脑图,方便读者更快的找到自己想要了解的知识点。
一、IPC简介
1.1什么是IPC
IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
看到这里,需要先了解一下进程、线程以及它们的关系。
进程:一般指一个执行单元,在PC和移动设备上指一个程序或应用。
线程:CPU调度的最小单元。线程是一种有限的系统资源。
二者之间的关系:
一个进程可包含多个线程,即一个应用程序上可以同时执行多个任务。
注意:不可在主线程做大量耗时操作,会导致ANR(应用无响应)。
IPC不是Android中所独有的,任何一个操作系统都需要有相应的IPC机制,Binder是Android中最有特色的进程间通信方式。
1.2为什么要进行IPC
进程间通信的必要性
所有运行在不同进程的四大组件,只要它们之间需要通过内存在共享数据,都会共享失败。这是由于Android为每个应用分配了独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这会导致在不同的虚拟机中访问同一个类的对象会产生多份副本。
1.3怎么进行IPC
使用Bundle、文件共享、
Messenger、AIDL、ContentProvider、Socket。
二、Android 中的多进程模式
2.1开启进程
- (常规)在
AndroidMenifest中给四大组件指定属性android:process。- (不常规)通过
JNI在native层fork一个新的进程。
进程名的默认规则:
默认进程:
- 没有指定该属性则运行在默认进程,其进程名就是包名。
以“:”开头的进程:
- 省略包名,如
android:process=":remote",表示进程名为com.example.myapplication:remote。 - 属于当前应用的私有进程,其他进程的组件不能和他跑在同一进程中。
完整命名的进程:
- 如
android:process="com.example.myapplication.remote"。 - 属于全局进程,其他应用可以通过
**ShareUID**方式和他跑在用一个进程中。
UID: Android系统会为每个应用分配一个唯一的UID,具有相同的UID才能共享数据。两个应用通过
ShareUID跑在同一个进程中的条件:具有相同的ShareUID和签名。
- 满足上述条件的两个应用,无论是否跑在同一进程,它们可共享data目录,组件信息。
- 若跑在同一进程,它们除了可共享data目录、组件信息,还可共享内存数据。它们就像是一个应用的两个部分。
2.2多进程的运行机制
Android为每一个进程分配了一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,这也导致了不同虚拟机中访问同一个对象会产生多份副本。
一般来说使用多进程会带来以下四个方面的问题:
静态变量和单例模式失效 原因:不同虚拟机中访问同一个对象会产生多份副本。
线程同步机制失效 原因:内存不同,线程无法同步。
SharedPreference的可靠性下降 原因:底层是通过读写XML文件实现的,发生并发问题。Application多次创建- 原因:Android系统会为新的进程分配独立虚拟机,相当于应用重新启动了一次。
为了解决这些问题,可以采用跨进程通信方法,通过Intent,共享文件和SharedPreferences,Messenger、AIDL和Socket等。
三、IPC基础概念
在了解以下三种接口使用前,需要先了解一下什么是序列化和反序列化。
1.什么是序列化?
含义:序列化表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。
场景:需要通过
Intent和Binder等传输类对象就必须完成对象的序列化过程。两种方式:实现
Serializable/Parcelable接口。
2.什么是反序列化?
把字节序列恢复为对象的过程称为对象的反序列化,与序列化相反。
3.1 Serializable接口
Serializable是Java所提供的一个序列化接口,是一个空接口,为对象提供标准的序列化和反序列化操作。
用法:实体类实现
Serializable接口,声明serialVesionUID。注意:
serialVesionUID非必需,但是不声明会对反序列化有影响。
serialVesionUID与当前类的serialVesionUID相同才能被正常反序列化。
serialVesionUID可以系统配置/手动修改。
代码示例:
//User实体类实现Serializable接口;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
public int UserId;
public String userName;
public boolean isMale;
public User(int userId, String userName, boolean isMale) {
UserId = userId;
this.userName = userName;
this.isMale = isMale;
}
public static long getSerialVersionUID() {
return serialVersionUID;
}
public int getUserId() {
return UserId;
}
public void setUserId(int userId) {
UserId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public boolean isMale() {
return isMale;
}
public void setMale(boolean male) {
isMale = male;
}
}
//序列化过程
User user = new User(1,"Yuki",true);
ObjectOutputStream outputStream;
{
try {
outputStream = new ObjectOutputStream(new FileOutputStream("cache.txt"));
//反序列化过程
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User)in.readObject();
in.close();
} catch (IOException e) {
e.printStackTrace();
}catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
3.2 Parcelable接口
用法:
实体类实现
Parcelable接口内容描述
序列化
反序列化
代码示例:
//User实体类实现Parcelable;
public class User implements Parcelable {
public int UserId;
public String userName;
public boolean isMale;
// public Book book;
public User(int userId, String userName, boolean isMale) {
UserId = userId;
this.userName = userName;
this.isMale = isMale;
}
protected User(Parcel in) {
UserId = in.readInt();
userName = in.readString();
isMale = in.readByte() != 0;
// book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
//反序列化
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
public int getUserId() {
return UserId;
}
public void setUserId(int userId) {
UserId = userId;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public boolean isMale() {
return isMale;
}
public void setMale(boolean male) {
isMale = male;
}
//内容描述
@Override
public int describeContents() {
return 0;
}
//序列化
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(UserId);
dest.writeString(userName);
dest.writeByte((byte) (isMale ? 1 : 0));
// dest.writeParcelable(book,0);
}
}
Parcelable方法说明
| 方法 | 功能 | 标记位 |
|---|---|---|
createFromParcel(Parcel in) | 从序列化后的对象中创建原始对象 | |
newArray(int size) | 创建指定长度的原始对象数组 | |
writeToParcel(Parcel dest, int flags) | 将当前对象写入序列化结构中,其中flags标识有两种值:0或者1,为1时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有的情况都为0 | PARCELABLE_WRITE_RETURN_VALUE |
| User(Parcel in) | 从序列化后的对象中创建原始对象 | |
describeContents | 返回当前对象的内容描述,如果含有文件描述符,返回1,否则返回0,几乎所有的情况都返回0 | CONTENTS_FILE_DESCRIPTOR |
Serializable和Parcelable比较
Serializable | Parcelable |
|---|---|
| Java序列化接口 | Android序列化接口 |
| 使用简单 | 使用较麻烦 |
| 效率低 | 效率高 |
3.3 Binder
3.3.1 Binder是什么?
- 直观上:Android 的一个类,实现了
IBinder接口。**IPC角度:Android中的一种跨进程通信**。- 实现方式角度:一种虚拟的物理设备。
- Android
Framework角度::ServiceManager连接各种Manager(ActivityManager、WindowManager等)的桥梁。- Android应用层角度:客户端和服务端进行通信的媒介。
3.3.2 为什么是Binder?
Android系统是基于Linux内核的,Linux已经提供了管道、消息队列、内存共享和Socket等IPC机制,为什么Android还要提供Binder来实现IPC?
| 优势 | 描述 |
|---|---|
| 性能 | 只需要一次数据拷贝,性能上仅次于共享内存 |
| 稳定性 | 基于C/S架构,职责明确,架构清晰,稳定性好 |
| 安全性 | 为每个APP分配UID,进程的UID事鉴别进程身份的重要标志 |
3.3.3 Binder IPC底层通信原理
3.3.3.1.其他IPC机制完成一次进程间通信是怎么样的?
在不同的进程之间,消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用
copy*from*user()函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用copy*to*user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。
3.3.3.2 Binder IPC完成一次进程间通信又是怎么样的?
- 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
- 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
- 发送方进程通过系统调用
copy*from*user() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
各种IPC方式数据拷贝次数
IPC | 数据拷贝次数 |
|---|---|
| 共享内存 | 0 |
Binder | 1 |
消息队列/管道/Socket | 2 |
3.3.4 Binder通信过程
Binder框架定义了四个角色:Server,Client,ServiceManager以及Binder驱动。其中Server,Client,ServiceManager运行于用户空间,驱动运行于内核空间。这四个角色的关系和互联网类似:Server是服务器,Client是客户终端,ServiceManager是域名服务器(DNS),驱动是路由器。
-
首先,一个进程使用
BINDER*SET*CONTEXT_MGR命令通过Binder驱动将自己注册成为ServiceManager; -
Server通过驱动向ServiceManager中注册Binder(Server中的Binder实体),表明可以对外提供服务。驱动为这个Binder创建位于内核中的实体节点以及ServiceManager对实体的引用,将名字以及新建的引用打包传给ServiceManager,ServiceManger将其填入查找表。 -
Client通过名字,在Binder驱动的帮助下从ServiceManager中获取到对Binder实体的引用,通过这个引用就能实现和Server进程的通信。这里参考自:写给 Android 应用工程师的 Binder 原理剖析
问:当服务端进程异常终止的话,造成Binder死亡的话,怎么办?
在客户端绑定远程服务成功后,给Binder设置死亡代理,当Binder死亡的时候,我们会收到通知,从而重新发起连接请求。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
@Override
public void binderDied(){
if(mBookManager == null){
return;
}
mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
mBookManager = null;
// TODO:这里重新绑定远程Service
}
}
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
四、Android中的IPC模式
4.1 Bundle
概念:由于Bundle实现了Parcelable接口,可以方便的在不同进程间传输。
- 范围:
Activity、Service、Receiver间传递。- 使用:通过
Intent发送。
扩展使用:A进程要启动B进程并把在A进程计算完的数据传递给B进程,如何把不支持Bundle的数据由A进程传入B中?
答:将原本在A进程的计算任务转移到B进程的后台Service中去执行。通过Intent启动B进程的一个Service,让计算任务在Service完成,计算完成 后再去启动目标组件,并把数据传递给目标组件。
4.2 文件共享
概念:两个进程通过读/写同一个文件来交换数据。
- 文件范围:对文件格式没有具体要求。
- 局限性:并发读/写。
4.3 Messenger
概念:可以在不同进程中传递Message对象,把需要传递的数据放进对象中。
底层实现:轻量级的
IPC方案,它的底层实现是AIDL。实现
Message1.服务端进程
- 创建一个
Service处理客户端连接请求- 创建一个
Handle并通过它创建一个Messenger对象- 在
Service的onBind返回这个对象的底层Binder2.客户端进程
- 绑定服务端的
Service- 用服务端返回的
IBinder对象创建一个Messenger(客户端——>服务端)- 在客户端创建一个
Handler并由此创建一个Messenger,并通过Message的**replyTo字段**传递给服务器端进程。服务端通过读取Message得到Messenger对象,进而向客户端进程传递数据。(客户端 <——>服务端)
//Messenger 服务端代码
public class MessengerService extends Service {
private static final String TAG = "MessengerService";
private final Messenger messenger = new Messenger(new MessengerHandler());
//处理客户端发送的消息
private static class MessengerHandler extends Handler{
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what){
case 1:
Log.i("TAG"," "+msg.getData().getString("msg"));
break;
default:
break;
}
super.handleMessage(msg);
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return messenger.getBinder(); //返回它的Binder对象
}
}
<service android:name=".MessengerService"
android:process=":remote"/> //注册Service
//客户端代码
public class MessengerActivity extends AppCompatActivity {
private Messenger mService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//绑定服务
Intent intent = new Intent(this,MessengerService.class);
bindService(intent,mConnection, Context.BIND_AUTO_CREATE);
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mService = new Messenger(service); //用服务端返回的IBinder对象创建一个Messenger对象
Message msg = Message.obtain(null,1);
Bundle data = new Bundle();
data.putString("msg","Client");
msg.setData(data);
try {
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onDestroy() {
unbindService(mConnection);
super.onDestroy();
}
}
4.4 使用AIDL
AIDL是Android Interface Definition Language的缩写,意思是Android接口定义语言,用于让某个Service与多个应用程序组件之间进行跨进程通信,从而可以实现多个应用程序共享同一个Service的功能。其使用可以简单的概括为服务端和客户端,类似于Socket一样,服务端服务于所有客户端,支持一对多服务。
4.4.1 Messenger和AIDL比较
Messenger缺点:串行方式处理消息,无法并发处理。
AIDL:可以并发处理请求。
AIDL通信流程
- 服务端
- 创建
Service监听客户端请求- 创建
AIDL文件- 在
AIDL文件中申明暴露给客户端的接口- 在
Service实现这个AIDL接口- 客户端
- 绑定服务端的Service
- 将服务端返回的Binder对象转成
AIDL接口所属的类型- 调用
AIDL方法
4.4.2AIDL能够支持哪些数据类型?
注意:除了基本数据类型,其它类型的参数必须标上方向:in、out或inout,用于表示在跨进程通信中数据的流向。
4.4.3 关键类和关键方法
AIDL接口:继承IInterface。Stub类:Binder的实现类,服务端通过这个类来提供服务。Proxy类:服务器的本地代理,客户端通过这个类调用服务器的方法。asInterface():客户端调用,将服务端的返回的Binder对象,转换成客户端所需要的AIDL接口类型对象。
返回对象:
- 若客户端和服务端位于同一进程,则直接返回
Stub对象本身;- 否则,返回的是系统封装后的
Stub.proxy对象。
asBinder():返回代理Proxy的Binder对象。onTransact():运行服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。transact():运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。
4.4.4 产生ANR的情形
-
客户端:
- 调用服务端的方法是运行在服务端的
Binder线程池中,若主线程所调用的方法里执行了较耗时的任务,同时会导致客户端线程长时间阻塞,易导致客户端ANR。 - 在
onServiceConnected()和onServiceDisconnected()里直接调用服务端的耗时方法,易导致客户端ANR。
- 调用服务端的方法是运行在服务端的
-
服务端:
- 服务端的方法本身就运行在服务端的**
Binder线程中,可在其中执行耗时操作,而无需再开启子线程**。 - 回调客户端
Listener的方法是运行在客户端的Binder线程中,若所调用的方法里执行了较耗时的任务,易导致服务端ANR。
- 服务端的方法本身就运行在服务端的**
解决客户端频繁调用服务器方法导致性能极大损耗的办法:实现观察者模式。
即当客户端关注的数据发生变化时,再让服务端通知客户端去做相应的业务处理。
4.4.5解注册失败的问题
- 原因:
Binder进行对象传输实际是通过序列化和反序列化进行,即Binder会把客户端传递过来的对象重新转化并生成一个新的对象,虽然在注册和解注册的过程中使用的是同一个客户端传递的对象,但经过Binder传到服务端后会生成两个不同的对象。另外,多次跨进程传输的同一个客户端对象会在服务端生成不同的对象,但它们在底层的Binder对象是相同的。 - 解决办法:当客户端解注册的时候,遍历服务端所有的
Listener,找到和解注册Listener具有相同的Binder对象的服务端Listener,删掉即可。
需要用到
RemoteCallBackList:Android系统专门提供的用于删除跨进程listener的接口。其内部自动实现了线程同步的功能。
4.5 使用ContentProvider
4.5.1 什么是ContentProvider?
ContentProviderAndroid中提供的专门用于不同应用间进行数据共享的方法,它天生就适合进程间通信。
4.5.2 如何自定义一个ContentProvider?
- 用一个实体类继承
ContentProvider。 - 实现
onCreate、query、update、insert、delete和getType等六种抽象方法。、
注意:
除了onCreate()运行在UI线程中,其他的query()、update()、insert()、delete()和getType()都运行在Binder线程池中。
CRUD四大操作存在多线程并发访问,要注意在方法内部要做好线程同步。
一个SQLiteDatabase内部对数据库的操作有同步处理,但多个SQLiteDatabase之间无法同步。
4.6 使用Socket
4.6.1 什么是Socket?
Socket也称为“套接字”,是网络通信的概念。分为流式套接字和用户数据报套接字两种。
- 流套接字:基于
TCP协议,采用流的方式提供可靠的字节流服务。- 数据流套接字:基于
UDP协议,采用数据报文提供数据打包发送的服务。
4.6.2 怎么实现Socket通信?
-
服务端:
- 创建一个
Service,在线程中建立TCP服务、监听相应的端口等待客户端连接请求; - 与客户端连接时,会生成新的
Socket对象,利用它可与客户端进行数据传输; - 与客户端断开连接时,关闭相应的
Socket并结束线程。
- 创建一个
-
客户端:
-
开启一个线程、通过
Socket发出连接请求; -
连接成功后,读取服务端消息;
-
断开连接,关闭
Socket。
-
五、IPC方式
各种IPC方式的优缺点比较:
| 名称 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的数据类型 | 四大组件间的进程间通信 |
| 文件共享 | 简单易用 | 不适合高并发场景,无法做到进程间的即时通信 | 无并发访问,交换简单数据且实时性不高 |
AIDL | 支持一对多并发和实时通信 | 使用稍复杂,需要处理线程同步 | 一对多且有RPC需求 |
Messenger | 支持一对多串行通信 | 不能很好处理高并发,不支持RPC,只能传输Bundle支持的数据类型 | 低并发的一对多 |
ContentProvider | 支持一对多并发数据共享 | 可理解为受约束的AIDL | 一对多进程间数据共享 |
| Socket | 支持一对多并发数据共享 | 实现细节繁琐 | 网络数据交换 |
六、Binder连接池
6.1 典型的AIDL使用流程
- 创建一个
Service和一个AIDL接口- 创建一个类继承自
AIDL接口中的Stub类并实现Stub中的抽象方法- 在Service的
onBind方法中返回这个类的对象
6.2 当有多个不同的业务需要使用AIDL来进行通信,该怎么处理?
可以将所有的AIDL放在同一个Service中去管理,它的工作机制是这样的:
每个业务模块创建自己的AIDL接口,向服务端提供自己的唯一标识和对应的Binder对象。对服务端来说,只需一个Service,服务端提供一个queryBinder接口,可以根据业务模块特征返回相应的Binder对象。Binder连接池的主要作用就是将每个业务模块的Binder请求统一转发到远程Service去执行,避免重复创建Service。
实现方式:
为每个业务模块创建
AIDL接口并具体实现。为
Binder连接池创建AIDL接口IBinderPool.aidl并具体实现。远程服务
BinderPoolService的实现,在onBind()返回实例化的IBinderPool实现类对象。
Binder连接池的具体实现,来绑定远程服务。客户端的调用。
本文参考:
-
《Android开发艺术探索》