Android进阶(2)| IPC机制
一.IPC简介
IPC:简单来说就是进程之间数据的交换过程。
线程的概念:线程是CPU调度的最小单元,同时也是一种有限的系统资源。在Android中主线程也叫做UI线程,在UI线程里面才能操作界面元素。
进程的概念:一般指一个执行单元,在PC和移动设备上指一个程序或者是一个应用。
线程&进程的关系:一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。
二.Android中的多进程模式
1.使用多进程的情况
(1)一个应用因为自身的设计的原因需要采用多进程模式来实现。例如为了扩充该应用使用的内存需要通过多进程来获取多份的内存空间。
(2)当前应用需要向其他的应用获取数据。对于这种情况我们一般会使用四大组件中的Content Provider来进行相关的操作,其实ContentProvider也是一种进程间的通信。
2.开启多进程模式
在Android中使用多线程的方法只有一个,就是在四大组件的AndroidMenifest中指定android:progress
属性。但是这里我们在设置属性的时候是有两种意义不同的设置方式,下面来逐一介绍:
1. : + 进程名
: + 进程名是一种简写方式,它的具体含义是要在当前的进程名前面加上当前的包名。除此以外,进程名以 : 开头的进程是属于当前应用的私有进程,其它的应用组件不可以和它跑在同一个进程当中。
2. 完整的包名 + 进程名
这种方式就是进程的完整命名方式,不过以该种方式设置的进程是属于全局进程,其它的应用通过shareUID方式可以和它跑在同一个进程当中。
补充:Android系统会为每一个应用分配一个唯一的UID,具有相同的UID的应用才能共享数据。但是两个应用通过shareUID跑在同一个进程中是有要求的,需要这两个应用具有相同的ShareUID并且签名相同才可以。
3.多进程模式的运行机制
1.运行机制
Android为每一个应用分配了一个独立的虚拟机,更进一步来说是为每一个进程都分配了一个独立的虚拟机,但是不同的虚拟机在内存分配上有不同的地址空间,这就导致了在不同的进程中访问同一个对象会产生多种副本,也就是说所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败。例如全局变量static int num = 1
,在Activity1中对它加1,在Activity2中对它减1,然后我们使用Log在每个Activity中进行输出查看,发现在Activity1中num = 2
,在Activity2中num = 0
。
2.使用多进程造成的问题
(1)静态成员和单例模式完全失效
(2)线程同步机制完全失效
(3)SharedPreferences的可靠性下降
(4)Application会多次创建
三.IPC基础
1.Serializable接口
概念:Serializable接口是java所提供的一个序列化接口,它是一个空接口。使用它可以很简单的实现对象的序列化和反序列化,举例来说:
public class User implements Serializable{
private static final long serialVersionUID = 51906712387724L;
public int userId;
public String userName;
public boolean isMale;
......
}
//系列化过程
User user = new User(0,"jake",true);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();
//反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User)in.readObject();
in.close();
serialVersionUID:它是一个类似于类的标识的数据,它的工作机制是这样的:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中,在反序列化的时候系统首先会验证文件中的serialVersionUID是否和类中的一致,如果一致,则可以继续后面的反序列化,否则反序列化失败。所以一般来说我们都会手动设定或者让系统自动设定serialVersionUID的值。
注意:在序列化时静态成员和用transient关键字标记的成员变量不参与序列化的过程。
2.Parcelable接口
概念:Parcelable是Android提供的一种新的序列化方式。同样,Parcelable也是一个接口,只要实现了这个接口,一个类的对象就可以实现序列化并且可以通过Intent和Binder传递。
使用Parcelable进行主要是有三个步骤,首先是在writeToParcel()方法中完成序列化的功能,接着是在CREATOR中来是实现反序列化,最后就是在describeContents方法中来完成内容描述功能,不过这里几乎所有情况下都是返回0。
public class User implements Parcelable{
public int userId;
public String userName;
public boolean isMale;
public Book book; //Book类
public User(int userId,String userName,boolean isMale){
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
public int describeContents(){
return 0;
}
public void writeToParcel(Parcel out,int flags){ //序列化
out.writeInt(userId);
out.writeString(userName);
out.writeInt(isMale ? 1:0);
out.writeParcelable(book,0);
}
public static final Parcelable.Creator<User> Creator = new Parcelable.Creator<User>(){
public User createFromParcel(Parcel in){
return new User(in);
}
public User[] newArray(int size){
return new User[size];
}
};
private User(Parcel in){
userId = in.readInt();
userName = in.readStrint();
isMale = in.readInt == 1;
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
}
3.Binder
概念:Binder是Android中的一个类,由它来实现IBinder接口。在IPC机制中,Binder是一种跨进程的通信方式并且主要是用在Service当中,包括AIDL和Messager。
Binder的组成:Binder由四部分组成:Binder客户端、Binder服务端、Binder驱动、Service Manager(服务登记查询模块)。
-
Binder客户端
Binder客户端是想要使用服务的进程。 -
Binder服务端
Binder服务端是实际提供服务的进程。 -
Binder驱动
我们在客户端先通过Binder拿到一个服务端进程中的一个对象的引用,通过这个引用,直接调用对象的方法获取结果。在这个引用对象执行方法时,它是先将方法调用的请求传给Binder驱动;然后Binder驱动再将请求传给服务端进程;服务端进程收到请求后,调用服务端“真正”的对象来执行所调用的方法;得出结果后,将结果发给Binder驱动;Binder驱动再将结果发给我们的客户端;最终,我们在客户端进程的调用就有了返回值。Binder驱动,相当于一个中转者的角色。通过这个中转者的帮忙,我们就可以调用其它进程中的对象。 -
Service Manager
我们调用其它进程里面的对象时,首先要获取这个对象。这个对象其实代表了另外一个进程能给我们提供什么样的服务(再直接一点,就是:对象中有哪些方法可以让客户端进程调用)。首先服务端进程要在某个地方注册登记一下,告诉系统我有个对象可以公开给其它进程来提供服务。当客户端进程需要这个服务时,就去这个登记的地方通过查询来找到这个对象。
工作机制:我们可以用一张图来进行概括:
注意:Binder运行在服务端,如果当服务端由于某些原因造成远程请求失败时,我们就称这种情况为Binder死亡。所以为了让我们能够知道Binder是否是处于死亡状态,我们可以调用linkToDeath()和unlinkToDeath()方法。通过linkToDeath()来给Binder设置一个死亡代理,当Binder处于死亡状态时我们就会收到一个通知,之后我们就可以采取想应的措施来恢复与服务端之间的连接。
四.Android中的IPC方式
1.使用Bundle
概念:Bundle是实现了Parcelable接口的类,它可以在Activity,Service和Receiver中通过Intent来进行数据传输。
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putString("xxx");
intent.putExtra("key",bundle);
注意:Bundle中不支持的数据类型我们是无法通过Bundle进行进程间传输的。
2.使用文件共享
概念:由于Android系统是基于Linux内核的,因此它能够并发读写文件,所以我们可以使用同一个文件来进行不同进程间的数据交换。即在A进程中我们将数据写入文件,在B进程中我们再将文件的中数据读出来。
局限性:由于在使用文件共享时我们可以并发读写文件,所以造成的后果可能是我们读取的内容不是最新的。
适用条件:文件共享方式适合在数据同步要求不高的进程之间进行通信。
3.使用AIDL
<先设一个坑,后面再来填 2333>
4.使用ContentProvider
概念:内容提供器是Android官方建议的跨进程通信方式,它的底层实现同样也是Binder。内容提供器的具体使用方法可以参看Android入门(9)| 四大组件之内容提供器。在这里就不在具体的展开介绍。
android:authorities:这个是内容提供器的唯一标识,也就是说当外部的应用或者是其他的进程想要访问该内容提供器时必须要先在Uri中声明该标识。
onCreate()、query()、getType()、insert()、delete()和update():这6个方法是我们自己的内容提供器继承自ContentPrvider的方法,需要注意的是这6个方法均是运行在ContentProvider的进程中,并且除了onCreate()由系统回调并运行在主线程中,其他的5个方法均由外界回调并运行在Binder线程池中。
5.使用Socket
概念:Socket也称为套接字,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议,对于TCP协议和UDP协议这里就不多讲。而Socket就是依靠着这两个协议来进行跨进程同通信的。
工作过程:
-
客户端
首先客户端需要开启一个线程,并通过Socket发送连接请求,当与服务端连接成功之后就可以与服务端之间进行通信。如果想要断开连接,则直接关闭Socket即可。 -
服务端
当服务端中的Service启动时,会在线程中建立TCP/UDP服务,并且监听相应的端口来等待客户端的连接。当有客户端连接时,就会生成一个新的Socket,通过每次创建的Socket就可以分别和不同的客户端通信了。当客户端断开连接时,服务端也会关闭相应的Socket来结束通信。
注意:在Android中如果想要使用Socket来进行通信,有两点需要注意:
- (1)先声明权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
- (2)不能在主线程中访问网络
如果在主线程中使用网络,会导致我们的程序抛出以下异常:android.os.NetWorkOnMainThreadException
。并且网络操作是耗时的,如果放在主线程中可能会影响程序的执行效率。
6.方式的比较
当我们在进行IPC时,我们可以有以上6种方式完成操作。在实际进行操作时,我们可以按照项目的特点来选择最合适的方式。各个方式的特点如下图所示:
方式的比较
五.Binder连接池
背景:假设我们的一个项目中有许多的模块需要AIDL来进行进程间的通信,根据前面的介绍,按照正常的方式我们应该为每一个AIDL创建Service,但是如果一个项目中有多个Service在运行是很不好的情况,因此我们可以使用Binder连接池来优化我们的项目。
工作机制:首先是在每个业务模块创建自己的AIDL接口并且实现此接口,接着向服务端提供自己的唯一标识和其对应的Binder,之后是服务端创建一个Service并且提供一个queryBinder的接口,这个接口的功能是根据业务模块的特征来返回相应的Binder对象给它们,在业务模块拿到各自的Binder对象之后就可以进行远程方法的调用了。
Binder连接池工作机制