本文为Android开发艺术探索的笔记,仅供学习
1 IPC 含义就是进程间通信或者跨进程通信
线程是CPU调度的最小单位,同时线程也是一种有限的系统资源。
进程一般指的是一个执行单元,在PC和移动设备上指一个程序或者是一个应用。
一个进程可以有很多个线程,但只有一个线程的时候即为主线程,在android里也称为UI线程。UI线程里才能去操作界面元素。很多时候,一个进程需要执行大量耗时任务,如果这些任务放在主线程里,就会造ANR(应用程序无响应)。解决这个问题就需要用到线程,需要建立子线程通过Handle去操作些耗时操作或更新UI。
2 多进程模式分析
2.1 android中的多进程模式
想要使用多进程,只需在四大组件的XML文件里使用android:process的属性即可。
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:label="@string/app_name"
android:launchMode="standard" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:configChanges="screenLayout"
android:label="@string/app_name"
android:process=":remote" />
<activity
android:name=".ThirdActivity"
android:configChanges="screenLayout"
android:label="@string/app_name"
android:process="com.ryg.chapter_2.remote" />
让我们来看看这三个Activity分别对应的进程是什么
可以看到有三个进程,分别表示在xml里声明的三个Activity,一次对应MainActivity,SecondActivity,ThirdActivity
SecondActivity的进程名字是com.ryg.chapter_2:remote ,:的前面部分是当前进程是在的包名。
ThirdActivity所对应的进程com.ryg.chapter_2 .remote ,这个命名方式是完整的命名方式,不会附加包名。
以:开头的进程是私有进程,其他应用组件是不会和它跑在同一个进程里的,而不是以:开头的则是全局进程其他应用可以通过ShareUID的方式,可以跑在同一个进程里。
注:Android的系统会给每一个应用分配一个唯一的UID,具有相同的UID才能共享数据。若两个相同的UID想要跑在同一个进程里是有要求的,必须要有相同签名可以。这种情况下,他们可以互相访问私有数据,还可以共享数据。
2.2 多进程模式的运行机制
首先举个例子好让大家理解,在上面的基础上,我们再添加一个类,并且声明一个全局变量如下图
public class UserManager{ public int sUserId = 1;}
那么我们需要在MainActivity中去修改sUserId ,把值改为2 并且打印出来,在SecondActivity将sUserId打印出来,结果
按照常理来说SecondActivity应该输出2,因为MainActivity已经把值改为2了,而事实却是相反的,因为Android中每一个进程,都会为其分配一个DVM(虚拟机),所以每一个进程都是相对独立的,而MainActivity修改的值,仅对同一进程有效。SecondActivity和MainActivity并不在同一个进程中,所以MainActivity修改的值SecondActivity会接收不到。
所以这就是多进程带来的主要影响有以下几点
- 静态成员和单例模式完全失效
- 线程同步机制完全失效
- SharePreferences的可靠性降低
- Application会被多次创建
第一个影响就是上面的例子,第二个影响也一样,既让都不在同一个虚拟机中,不再同一个进程中线程又怎么去同步呢。第三个影响就是Sharepreferences本生不支持两个进程同事去写,这样有一点的几率会造成数据的丢失。第四个影响每一个应用对于一个进程,对于一个虚拟机,对于一个Application,所以开启多个进程会开启多个虚拟机开启多个Application 是一样的道理。
3 IPC的基础概念
主要包括三个,serializable接口,parcelable接口以及binder
3.1 serializable的接口
Serializable 是java提供的一个序列化的接口,就是对对象进行序列化和反序列化的操作。首先想要实现序列化的操作需要让类去实现Serialezable接口并声明一个serialVersionUID即可,事实上serialVersionUID并不是必须的,不声明serialVersionUID也可以实现序列化。
public class User implements Serializable {
private static final long serialVersionUID = 519067123721295773L;
public int UserId;
public String username;
public boolean isMale;
}
就完成对象的序列化操作,几乎所有的工作都让系统去自动完成。
如何对对象进行序列化和反序列化的操作,只需采用ObjectInputStream和ObjectOutputStream
//序列化的过程
String CHAPTER_2_PATH = Environment.getExternalStorageDirectory().getPath()
+ "/singwhatiwanna/chapter_2/";
String CACHE_FILE_PATH = CHAPTER_2_PATH + "usercache";
User user = new User(1, "hello world", false);
File dir = new File(MyConstants.CHAPTER_2_PATH);
if (!dir.exists()) {
dir.mkdirs();
}
File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
ObjectOutputStream objectOutputStream = null;
try {
objectOutputStream = new ObjectOutputStream(
new FileOutputStream(cachedFile));
objectOutputStream.writeObject(user);
Log.d(TAG, "persist user:" + user);
} catch (IOException e) {
e.printStackTrace();
} finally {
MyUtils.close(objectOutputStream);
}
//反序列化过程
User user = null;
File cachedFile = new File(MyConstants.CACHE_FILE_PATH);
if (cachedFile.exists()) {
ObjectInputStream objectInputStream = null;
try {
objectInputStream = new ObjectInputStream(
new FileInputStream(cachedFile));
user = (User) objectInputStream.readObject();
Log.d(TAG, "recover user:" + user);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
MyUtils.close(objectInputStream);
}
}
这就是一个简单的使用,将对象输出到txt中就是序列化,将txt的数据读取出来就是反序列化。
android中Intent 是可以来传递一个序列化的对象
好了 之前说serialVersionUID 没有也可以序列化,但到底有什么用呢?
- serialVersionUID就像是标识符,当你序列化的时候serialVersionUID的值也会被写入,在反序列化的时候 系统会通过对比类里的serialVersionUID与文件中的serialVersionUID 是否一致来进行反序列化,但一致的时候反序列化成功不一致则不成功。
- 当你没声明serialVersionUID的时候,在反序列化的时候 修改了类那么反序列化会不成功 因为当前类与序列化时候的类不一样导致了反序列化的失败。(因为不声明serialVersionUID,系统会自动声明serialVersionUID,因为你进行类修改所以会导致 前后两次serialVersionUID的值不一样,所以反序列化失败)
- 但是如果你手动声明了serialVersionUID,当你修改了序列化对象,对其进行增删改属性之后再进行反序列化,此时反序列化会是成功的,也是无论修改这样serialVersionUID的值都是一样的,系统会最大程度的去回复数据。
3.2 Parcelable的接口
Parcelable也是序列化的一种方式,使用Parcelable只需去实现Parcelable接口即可
public class User implements Parcelable {
public int userId;
public String userName;
public boolean isMale;
public User() {
}
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);
}
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.readString();
isMale = in.readInt() == 1;
}
}
其实下列方法都会自动生成,不需要手打
既让Serializable 和Parcelable都可以序列化,那就说说两者之间的区别,Serializable 在序列化和反序列化的时候,需要进行大量 I/O操作,很耗时。Parcelable是android的序列化方式,他很高效,但是使用起来很麻烦。
Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化
3.3 Binder
Binder就是android的一个类,它实现了IBinder的接口。从IPC角度上来说,就是Binder是Android的一种跨进程通信的方式。从Android Framework的角度来分析就是Binder 是ServiceManager用来连接各种Manager的(ActivityManager WindowManager)桥梁。从Android应用层的角度来分析就是客户端和服务端进行通信的媒介,当bindService的时候,服务端会调用一个Binder对象,通过这个对象,客户端就可以获取到一些数据。
在Android的开发中,Binder主要用在Service中,包括AIDL和Messenger。普通的Service中的Binder不涉及到进程通信,所以适用AIDL来分析Binder的工作机制。
先演示一下AIDL的建立和通过AIDL生成JAVA文件去看看Binder的机制
其中Book.java是自定义类,用途就是个Bean。如果需要使用自定义类 必须要建立一个同名的aidl文件。
系统可能会找不到我们创建的类,那么就是要一下声明
sourceSets {
main {
manifest.srcFile ='src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java', 'src/main/aidl']
resources.srcDirs = ['src/main/java', 'src/main/aidl']
aidl.srcDirs = ['src/main/aidl']
res.srcDirs = ['src/main/res']
assets.srcDirs = ['src/main/assets']
}
}
接下来 我们来看看生成的JAVA文件 去看看Binder的机制,先看看生成的java文件,代码确实比较多,我就截取一个方法为例子,我直接在代码上进行解释
public interface IBookManager extends android.os.IInterface
{
/** Stub是运行在服务端中的 */
public static abstract class Stub extends android.os.Binder implements com.example.gyh.myapplication.IBookManager
{
//这个属性就是Binder的唯一标识,也就是包名
private static final java.lang.String DESCRIPTOR = "com.example.gyh.myapplication.IBookManager";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
//该方法就是为了将服务端的Binder转化为用户端的AIDL接口类型的对象,这种转化过程是区分进程的,若是同一个进程则就返回服务端本生的Stub对象,不是则转化成Stub.Proxy对象。
public static com.example.gyh.myapplication.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.example.gyh.myapplication.IBookManager))) {
return ((com.example.gyh.myapplication.IBookManager)iin);
}
return new com.example.gyh.myapplication.IBookManager.Stub.Proxy(obj);
}
//该方法就是返回Binder对象
@Override public android.os.IBinder asBinder()
{
return this;
}
//该方法是运行在服务端的Binder线程池中的,当客户端发起跨进程请求的时候,远程请求会通过系统底层的封装后交由改方法进行处理。现在来解释一下里面的参数代表着什么意思
code表示客户端请求的方式是什么,data保存着方法的参数,reply是要返回的值,flags为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_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.example.gyh.myapplication.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.example.gyh.myapplication.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.example.gyh.myapplication.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;
}
@Override public void addBook(com.example.gyh.myapplication.Book book) throws android.os.RemoteException
{
//首先要创立三个对象,_data,_reply,_result , 然后如果有参数的话把参数写到_data中,接着调用transact方法去发起远程过程调用(RPC),同时将现场挂起(就是暂停的意思);然后服务端的ontransact调用,直到RPC过程返回后,该线程继续。并从_reply中取出返回值,并且赋予_result去返回。
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);
}
//transact回去调用stub中的ontransact方法,找到对应的方法也就是ontransact里的addBook方法,取出返回的_reply,从中取出客户端需要的返回值
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
}
public void addBook(com.example.gyh.myapplication.Book book) throws android.os.RemoteException;
}
通过上面的分析,我们可以知道客户端发起远程请求时,当前线程会被挂起直到服务端进程返回结果,这是一个耗时操作,所以不能再UI线程中发起。其实服务端的Binder是运行在Binder线程池中的,所以不管Binder是否耗时都应该是同步的。下图解释
那么我再来总的概括一下,当我们和服务器建立连接之后,通过服务器返回的Binder,将Binder转化为客户端的AIDL接口对象,因为是跨进程所以返回的是Stub.Proxy对象,通过这个对象去调用相应的方法如addBook,addBook方法中则会通过tranasct方法去调用Stub中onTranasct方法中对应的addBook,并通过_reply返回相应的数据给客户端。这样一来整个跨进程通信就结束了。
Binder的死亡代理
Binder的两个很重要的方法linkToDeath和unlinkToDeath,当我们服务端的进程由于某种原因断裂,那么对导致我们远程调用失败。但更为重要的是我们不知道Binder是否断裂,那么客户端的功能就会收到影响,所以Binder给我们配置了两个方法,通过linkToDeath我们可以设置一个死亡代理,当Binder死亡的时候,可以给客户端发来消息,从而我们可以重新开启服务。然后如何去设置这个代理呢?我们先来看看demo
首先声明一个DeathRecipent对象,DeathRecipent是一个接口,其内部只有一个方法binderDied,当我们实现该方法的时候,当Binder死亡的时候系统就会回调改方法,然后我们可以移除之前绑带的Binder去重新开去新的Binder
linkToDeath方法的第二个参数是标记位,我们直接可以设置为0,这样我们就设置好了死亡代理。另外我们可以通过Binder的isBinderAlive的方法去判断Binder是否死亡。