1.1 Android IPC简介
IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
按照操作系统中的描述,线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。
一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。最简单的情况下,一个进程中可以只有一个线程,即主线程,在Android里面主线程也叫UI线程,在UI线程里才能操作界面元素。很多时候,一个进程中需要执行大量耗时的任务,如果这些任务放在主线程中去执行就会造成界面无法响应,严重影响用户体验,这种情况在PC系统和移动系统中都存在,在Android中有一个特殊的名字叫做ANR(Application Not Responding),即应用无响应。解决这个问题就需要用到线程,把一些耗时的任务放在线程中即可。
1.2 Android中的多进程模式
1.2.1 开启多进程模式
正常情况下,在Android中多进程是指一个应用中存在多个进程的情况,因此这里不讨论两个应用之间的多进程情况。
首先,在Android中使用多进程只有一种方法,那就是给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidMenifest中指定android:process属性,除此之外没有其他办法,也就是说我们无法给一个线程或者一个实体类指定其运行时所在的进程。
其实还有另一种非常规的多进程方法,那就是通过JNI在native层去fork一个新的进程,但是这种方法属于特殊情况,也不是常用的创建多进程的方式,因此我们暂时不考虑这种方式。
假如当前App的包名为com.dream.shoppe,SecondActivity和ThirdActivity的android:process属性分别为“:remote”、“com.dream.shoppe2.remote”,那么这两种方式有区别吗?
其实是有区别的,区别有两方面:
首先,“:”的含义是指要在当前的进程名前面附加上当前的包名,这是一种简写的方法,对于SecondActivity来说,它完整的进程名为com.dream.shoppe:remote,而对于ThirdActivity中的声明方式,它是一种完整的命名方式,不会附加包名信息,即是属性值本身”com.dream.shoppe2.remote“;
其次,进程名以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。
Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。这里要说明的是,两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。在这种情况下,它们可以互相访问对方的私有数据,比如data目录、组件信息等,不管它们是否跑在同一个进程中。当然如果它们跑在同一个进程中,那么除了能共享data目录、组件信息,还可以共享内存数据,或者说它们看起来就像是一个应用的两个部分。
1.2.2 多进程模式的运行机制
一般来说,使用多进程会造成如下几方面的问题:
- (1)静态成员和单例模式完全失效。 Android为每一个应用分配了一个独立的虚拟机,或者说为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本。
- (2)线程同步机制完全失效。 既然都不是一块内存了,那么不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象。
- (3)SharedPreferences的可靠性下降。 因为SharedPreferences不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,这是因为SharedPreferences底层是通过读/写XML文件来实现的,并发写显然是可能出问题的,甚至并发读/写都有可能出问题。(题外话,SP是一个xml文件对应一个SP对象,初次读取SP时会加载一次对应xm文件,后续增删改查无论异步操作还是同步操作都只是对内存中的SP对象的操作,并不会立刻同步更新到对应xml文件中。)
- (4)Application会多次创建。 当一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然会创建新的Application。
1.3 IPC基础概念介绍
1.3.1 Serializable序列化
/**
* @author Huadao
* @date Created in 2022/10/20
* @desc
*/
public class User implements Serializable {
/**
* 是否声明序列化版本皆可,但是反序列化必需版本号相同。
* 可以手动指定,也可以让IDE根据当前类的结构自动去生成它的hash值。
*/
private static final long serialVersionUID = 1L;
/**
* 静态变量属于类不属于对象,所以不会参与序列化过程。
*/
public static boolean IS_SERIAL = true;
/**
* transient关键字标记的成员变量不参与序列化过程
*/
private transient String mName;
public String getName(){
return mName;
}
public void setName(String name){
mName = name;
}
}
String FILE_PATH = "xxx/cache.txt";
// 序列化过程
User user = new User("张三");
ObjectOutputStream outputStream = null;
try {
outputStream = new ObjectOutputStream(new FileOutputStream(FILE_PATH));
outputStream.writeObject(user);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 反序列化
ObjectInputStream inputStream = null;
try {
inputStream = new ObjectInputStream(new FileInputStream(FILE_PATH));
User newUser = (User) inputStream.readObject();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Serializable是Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable来实现序列化相当简单,只需要在类的声明中指定一个标识即可自动实现默认的序列化过程。
即使不指定serialVersionUID也可以实现序列化,那到底要不要指定呢?
如果指定的话,serialVersionUID后面那一长串数字又是什么含义呢?
我们要明白,系统既然提供了这个serialVersionUID,那么它必须是有用的。这个serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同才能够正常地被反序列化。
serialVersionUID的详细工作机制是这样的:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),当反序列化的时候系统会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化;否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的,会报错误。
一般来说,我们应该手动指定serialVersionUID的值,比如1L,也可以让IDE根据当前类的结构自动去生成它的hash值,这样序列化和反序列化时两者的serialVersionUID是相同的,因此可以正常进行反序列化。
如果不手动指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类的hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化的数据中的serialVersionUID不一致,于是反序列化失败,程序就会出现crash。
所以,我们可以明显感觉到serialVersionUID的作用,当我们手动指定了它以后,就可以在很大程度上避免反序列化过程的失败。比如当版本升级后,我们可能删除了某个成员变量也可能增加了一些新的成员变量,这个时候我们的反向序列化过程仍然能够成功,程序仍然能够最大限度地恢复数据,相反,如果不指定serialVersionUID的话,程序则会挂掉。当然我们还要考虑另外一种情况,如果类结构发生了非常规性改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID验证通过了,但是反序列化过程还是会失败,因为类结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构的对象。
根据上面的分析,我们可以知道,给serialVersionUID指定为1L或者采用IDE根据当前类结构去生成的hash值,这两者并没有本质区别,效果完全一样。
以下两点需要特别提一下:
首先,静态成员变量属于类不属于对象,所以不会参与序列化过程;
其次,用transient关键字标记的成员变量不参与序列化过程。
另外,系统的默认序列化过程也是可以改变的,通过重写readObject()和writeObject()这两个方法即可。
网友评论