在前面的博客中我已经写过了怎么在AndroidStudio中进行IPC开发设置
IPC是Intent-Process Communication 的缩写,含义为进程间通讯后者夸进程通讯是指两个进程之间进行数据交互的过程。
线程:线程是CPU调度的最小单元,同时线程是一种有限的系统资源。
进程:进程一般指一个执行单元,在PC和移动设备上一个程序后者应用。一个程序可以只有一个线程,即主线程,在Android里面主线程也叫UI线程,在UI线程里才能操作界面元素。
一、Android总的多进程模式
1.开启多进程模式
正常情况下,在Android中多进程是指一个程序中存在多个进程的情况,因此这里不讨论两个应用之间的多进程情况。Android开启多进程的方法:
常规方法:给四大组件(Activity、Service、Receiver、ContentProvider)
在AndroidMenifest中指定android:process属性。
非常规方法:通过JNI在native层去fork一个新的进程。
在下面只介绍常规的。
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:label="@string/title_activity_second"
android:theme="@style/AppTheme.NoActionBar"
android:process=":remote"
/>
<activity
android:name=".ThirdActivity"
android:label="@string/title_activity_main2"
android:theme="@style/AppTheme.NoActionBar"
android:process="com.app.song.ipc1.remote"
/>
上面的示例分别为SecondActivity和ThirdActivity指定了process属性。假设报名为com.app.song.ipc1当SecondActivity启动时,系统会为它创建一个单独的进程,进程名为“com.app.song.ipc1:remote”;当ThirdActivity启动时,系统也会为它创建一个单独的进程,进程名为“com.app.song.ipc1.remote”下面运行的结果:
:remote和com.app.song.ipc1.remote的区别 “:”的含义是指在当前线程名前附加上当前包名的一种简写的方法,对于SecondActivity来说,它完整的进程名为com.app.song.ipc1:remote,对于ThirdActivity中的声明方式,它是一种完整的命名方式,不会附加包名信息,其次,进程名以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。
Android系统会为每个应用分配一个唯一的UID,具有相同的UID应用才能共享共享数据。这可要说明的是,两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。在这种情况下,它们可以通过互相访问对方的随时有数据,比如data目录、组件信息等,不管它们是否跑在同一个进程中。当然如果跑在同一个进程中,那么除了能共享data目录、组件信息,还可以共享内存数据,或者说他们看起来就像是一个应用的两个部分。
2.多进程模式的运行机制
多进程看起很简单,但是在实际中并不简单。先创建一个类,叫做UserManager:
public class UserManager {
public static int sUserId = 1;
}
然后在MainActivity中对sUserId重新赋值,,接着在MainActivity和
SecondActivity中打印出sUserId的值,下面是运行的结果:
按照经验来说应该都是2,可是SecondActivity=2这就是多进程带来的问题,所以多进程绝对不是指定一个 android:process属性这么简单。
出现这种情况是应为SecondActivity运行在一个单独的进程中,android会为每一个进程分配一个独立的虚拟机,所以MainActivity和SecondActivity运行在两个不同的虚拟机中,不同的虚拟机在内存上有不同的地址空间,当MainActivity和SecondActivity都访问UserManager的时候会产生不同的副本,副本之间互不影响,这就是为什么SecondActivity中sUserId中值没有改变的原因。
所有运行在不同进程中的四大组件,只要他们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。正常情况下,四大组件之间不可能不通过一些中间层来共享数据,那么通过简单地指定进程名来开启多进程会无法正确运行。当特殊情况下某些组件之间不需要共享数据买那个时候直接指定android:process来开启多进程。
一般来说,使用多进程会造成如下几方面的问题
(1)静态成员和单例模式会失败。原因:上面已经分析了
(2)线程同步机制完全失效。 原因:在不同内存,锁就不起作用了
(3)SharePreferences的可靠性下降。原因:有可能多个进程同时对其修改,SharePreferences不支持
(4)Application会多次创建。原因:下面会进行分析。
问题(4)原因:当一个组件跑在一个新的进程的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以在这个进程其实就是启动一个应用的过程。因此相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然会创建新的Application。这个问题可以这么理解,运行在同一个进程中的组件是属于同一个虚拟机和同一个Application的,同理,运行在不同进程中的组件是属于两个不同的虚拟机和Application的,下面是application的设置:
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
String processName = MyUtils.getProcessName(getApplicationContext(), Process.myPid());
Log.d("MyApplication1","application start, process name:" + processName);
}
}
运行的结果是:
可以看出Application被创建了三次,并且每次id都不一样,他们的进程名和指定的android:process属性一致。这也就证实了在多进程模式中,不同进程的组件的确拥有独立的虚拟机、Application以及内存空间。或者我们也可以这么理解同一个应用间的多进程:它就相当于两个不同的应用采用了SharedUID的模式,这样能够更加直接地理解多进程模式的本质。
二、IPC基础概念介绍
下面主要介绍三个方面内容:Serializable接口、Parcelable接口以及Binder,只有熟悉这三个方面的内容后,我们才能更好地理解夸进程通讯的各种方式。Serializable和Parcelable接口可以完成对象的序列化过程,当我们需要通过Intent和Binder传递数据时就需要使用Parcelable或者Serialiable。还有的时候我们需要把对象持久化到储存设备上或者通过网络传输给其他客户端这时候也需要Serializable来完成对象的持久化
1.Serializable接口
Serializable是Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable来实现序列化很简单,只需要在类的声明中指定一个类似下面的标识即可自动实现默认的序列化过程。
private static final long serialVersionUID = 519067123721295773L;
实际上,甚至这个serialVersionUID也不是必须的,我们不声明这个serialVersionUID同样也可以实现序列化,但是这个会对反序列化过程产生影响。
User类就是一个实现了Serializable接口的类,它是可以反序列化的
public class User implements Serializable {
private static final long serialVersionUID = 519067123721295773L;
public int userId;
public String userName;
public boolean isMale;
public User(int userId, String userName, boolean isMale) {
this.isMale = isMale;
this.userId = userId;
this.userName = userName;
}
}
User user = new User(0,"ddd",true);
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.text"));
out.writeObject(user);
out.close();
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.text"));
User newUser = (User) in.readObject();
in.close();
上述代码演示了才用Seriakizable方式序列化对象的典型过程,很简单,只要把实现了Serializable接口的User对象写到文件中就可以快速恢复了,恢复后的对象newUser和user的内容完全一样但是两者并不是一个
对象。
serialVersionUID是协助系统序列化的参数,只有serialVersionUID相等的时候才能够正确的反序列化,否则会报错。serialVersionUID最好手动设置,当增加某个变量或者删除某个变量,只要serialVersionUID不变还是能够被反序列化的。但是如果结构发生了非常规性改变,比如修改了类名、修改了成员变量的类型,这时候即使serialVersionUID通过了,但是反序列化过程还是会失败,因为类结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构的对象。
2.Parcelable接口
Parcaelable也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。下面的示例是一个典型的用法。
package com.app.song.ipc1.Model;
import android.os.Parcel;
import android.os.Parcelable;
import com.app.song.ipc1.Book;
/**
* Created by song on 2016/2/21.
*/
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) {
this.isMale = isMale;
this.userId = userId;
this.userName = userName;
}
public User(Parcel source) {//从序列化后的对象中创建原始对象
userId = source.readInt();
userName = source.readString();
isMale = source.readInt() == 1;
book = source.readParcelable(Thread.currentThread().getContextClassLoader());
//由于book是另一个可序列化对象,所以他的反序列化需要传递当前的上下文加载器,否则会报无法找到类的错误
}
/**
* 将当前对象写入序列化结构中
* @param dest
* @param flags flags标识两种值:0或者1为1时标识当前对象需要作为返回值返回名单是不立即释放资源,
* 几乎所有情况都为0
*
*/
@Override
public void writeToParcel(Parcel dest, int flags) {//序列化
dest.writeInt(userId);
dest.writeString(userName);
dest.writeInt(isMale ? 1 : 0);
dest.writeParcelable(book,0);
}
/**
* 返回当前对象内容描述。如果含有文件描述符,返回1,否则返回0,几乎所有情况都返回0
* @return
*/
@Override
public int describeContents() {
return 0;
}
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>(){//反序列化
@Override
public User createFromParcel(Parcel source) {//从序列化后的对象中创建原始对象
return new User(source);
}
@Override
public User[] newArray(int size) {//创建指定长度的原始对象数组
return new User[size];
}
};
}
<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"> </span><span style="font-family: Arial, Helvetica, sans-serif; font-size: 14px; background-color: rgb(255, 255, 255);"><strong>系统已经为我们提供了许多实现了Parcelable接口的类,它们都是可以直接被序列化的,比如Intent、Bundle、Bitmap等,同时List和Map也可以序列化,前提是他们里面的每个元素都是可序列化的。</strong></span>
Parcelable和Serializable比较:
Parcelable序列化的开销比Serializable的开销小,Parcelable是android中的序列化方式因此更适合Android平台,但是使用起来比较麻烦
Serializable是Java中的序列化接口,序列化和反序列化过程中有大量的I/O操作,开销比Parcelable大,但是适合将对象序列化存储到存储设备中或者将对象序列化后通过网络传输。
3.Binder
Android开发中,Binder主要爱用在Service中,包括AIDL和Messenger,其中普通Service中的Binder不涉及进程间通信,所以比较简单,无法触及Binder的核心,而Messengerde底层其实是AIDL,所以这里选择用AIDL来分析Binder的工作机制。为了分析Binder的工作机制,这里需要新建一个AIDL示例,SDK会自动生成AIDL对应的Binder类,然后我们就可以分析Binder的工作过程。这里还用前面的例子,新建Book.java、Book.aidl、和IBookManager.aidl,在Android Studio中会自动生成aidl文件夹。代码如下:
Book.java
package com.app.song.ipc1;
import android.os.Parcel;
import android.os.Parcelable;
/**
* Created by song on 2016/2/21.
*/
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
public Book(Parcel source) {
bookId = source.readInt();
bookName = source.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>()
{
@Override
public Book[] newArray(int size) {
return new Book[size];
}
@Override
public Book createFromParcel(Parcel source) {
return new Book(source);
}
};
}
Book.aidl
// BookAIDL.aidl
package com.app.song.ipc1;
// Declare any non-default types here with import statements
parcelable Book;
IBookManager
// IBookManager.aidl
package com.app.song.ipc1;
// Declare any non-default types here with import statements
import com.app.song.ipc1.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
上面的三个文件中,Book.java是一个表示图书信息的类,她实现了Parcelable接口。Book.aidl是Book类在AIDL中的声明。IBookManager.aidl是我们定义的一个接口,里面有两个方法:gerBookList和addBook,其中getBookList用于远程服务端获取图书列表,而addBook用于往图书列表添加一本书,系统(Android Studio)在generated中为IBookManager.aidl生成了Binder类,接下来根据生成的类来分析Binder的工作原理:
/*
* This file is auto-generated. DO NOT MODIFY.
* Original file: F:\\AS_Study\\MyApplication2\\app\\src\\main\\aidl\\com\\app\\song\\ipc1\\IBookManager.aidl
*/
package com.app.song.ipc1;
public interface IBookManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.app.song.ipc1.IBookManager
{
private static final java.lang.String DESCRIPTOR = "com.app.song.ipc1.IBookManager";//binder的唯一标识,一般用当前Binder的类名来表示,比如本例中的“com.app.song.ipc1.IBookManager”;
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
*用于将服务端的Binder对象转换成客户端所需的AIDL接口的对象这种转换过程是区分进程的,
*如果客户端和服务端位于同一个进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装的Stub.Proxy对象
*
*
*/
/**
* Cast an IBinder object into an com.app.song.ipc1.IBookManager interface,
* generating a proxy if needed.
*/
public static com.app.song.ipc1.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.app.song.ipc1.IBookManager))) {
return ((com.app.song.ipc1.IBookManager)iin);
}
return new com.app.song.ipc1.IBookManager.Stub.Proxy(obj);
}
/**
*
*asBinder返回当前Binder对象
*
*
*/
@Override public android.os.IBinder asBinder()
{
return this;
}
/**
*这个方法运行在服务端中的Binder线程池中,当客户端发起进程请求时,远程请求就会通过系统底层分装后交由此方法处理。
*这个方法的原型为public Boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)。
*服务端通过code可以确定客户端所请求的目标是什么,接着从data中去除目标方法所需的参数(如果目标有参数的话),然后执行目标方法。
*当目标方法执行完毕后,就向reply中写入返回值(如果目标方法有返回值的话),onTransact方法的执行过程就是这样的。
*需要注意的是,如果此方法返回false,那么客户端的请求会失败因此我们可以利用这个特性来做权限认证
*
*
*/
@Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
{
switch (code)
{
case INTERFACE_TRANSACTION:
{
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.app.song.ipc1.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.app.song.ipc1.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.app.song.ipc1.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.app.song.ipc1.IBookManager
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
/**
*这个方法运行在客户端,当客户端远程调用此方法时,他的内部实现是这样的:创建该方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List;
*然后把该方法写入_data中(如果有参数的话);接着调用transact方法来发起RPC(远程过程调用)请求,同时当前线程挂起;
*然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply数据
*
*
*
*/
@Override public java.util.List<com.app.song.ipc1.Book> getBookList() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.app.song.ipc1.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.app.song.ipc1.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
/**
*这个方法运行在客户端,它的执行过程和getBookList是一样的,addBook没有返回值所以他不需要从_reply中取出返回值
*
*
*
*/
@Override public void addBook(com.app.song.ipc1.Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public java.util.List<com.app.song.ipc1.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.app.song.ipc1.Book book) throws android.os.RemoteException;
}
Binder有两个非常重要的方法linkToDeath 和 unlinkToDeath。Binder运行在服务端进程,如果服务端进程由于某种原因异常终止这个时候到服务端的Binder链接断裂(称之为Binder死亡),会导致远程调用失败。给为关键的是,如果我们不知道Binder连接断裂,那么客户端的功能就会受到影响。为了解决这个问题,Binder中提供了两个配对的方法linkToDeath 和 unlinkToDeath,通过linkToDeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,这个时候我们就可以重新发起链接请求从而恢复连接。下面是给Binder设置死亡代理的例子:
首先声明一个DeathRecipient对象。DeathRecipient是一接口,其内部只有一个方法binderDied,我们需要实现这个方法,当Binder死亡的时候,系统就会回调binderDied方法。然后我们就可以移出之前绑定的binder代理并重新绑定远程服务:
IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
@Override
public void binderDied() {
if(mBookManager == null)
{
return;
}
mBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
mBookManager = null;
//TODO 这里重新绑定远程Service
}
}
其次,在客户端绑定远程服务成功后,给binder设置死亡代理:
mService = IMessageBoxManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
其中linkToDeath的第二个参数是标记位,我们直接设置为0即可。通过上面两个步骤,就给我们的Binder设置了死亡代理,当Binder死亡的时候我们就可以收到通知了。另外,通过Binder的方法isBinderAlive也可以判断Binder是否死亡。
上面是IPC的基础