在我们使用 Android 手机的时候,有时我们使用的软件会需要消耗比较大的内存,也经常会需要同时打开多个软件。这些时候,我们都会需要使用到多进程技术。作为 Android 开发者,相信我们都知道如何去开启应用的单个多进程,但是开启多进程之后,如何进行「进程间通信(IPC)」呢?进程通信的方法有很多,他们适用于不同的使用场景,下面我们来逐个了解。
前置知识
- Android 四大组件
- Java 序列化
IPC简介
相信学过大学「操作系统」这门课的同学都还记得 进程间通信
信号量机制
这些名词,今天我们学习的也是操作系统的通信,不过是针对以 Linux 为内核的 Android 操作系统。我们通常会以一个软件或者程序为进程。而 Android 是可以使用多进程的,对于稍微大型一些的软件也都会使用到多进程。使用多进程的目的有如下几个:
- 进程隔离,以达到某些特殊的业务需求
- 扩大软件的内存大小。
多进程的开启很简单,其唯一方法是给注册文件 AndroidManifest.xml
中的四大组件部分添加 android:process
属性即可
<activity
android:name="com.qxy.potatos.module.mine.activity.WebViewActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="false"
android:process=":h5"
android:screenOrientation="portrait"
android:theme="@style/BlackTheme" />
上述的 android:process=":h5"
指的是该程序下的私有进程,是 com.qxy.potatos:h5
的简写。如果属性值换为全局形式的 com.qxy.potatos.h5
,则表示该进程是全局的,可共享给其他应用的。
而多进程出现后会导致如下的一些问题:
- 静态成员和单例模式完全失效
- 线程同步机制完全失效
- SharePreferences 的可靠性下降
- Application 多次创建
上述问题为何会出现呢?
由于 Android 为每个独立进程都分配了一个虚拟机,那么虚拟机的内存空间必然是不同的,所以不同的量在不同内存中都有一份副本,在不同进程中只能修改其内存下的副本。所以1和2中,无论是加何种锁,不作用在同一片内存空间中都是失效的。
而3则是由于 SharePreferences 是存储在 xml 文件中的,不同进程对该文件的并发读写时会导致数据出错的
4中则是由于 Android 的启动机制是每次都要由启动新的 Application,则每个进程都会有一个自己的 Application。我们也需要着重注意这个问题,在Application 中做好启动分类,在多进程启动阶段,防止不需要的资源多次加载
基于上述的原因和问题,我们需要深入了解 IPC 机制,让跨进程通信更好的服务于我们,解决多进程所带来的问题。
IPC基础知识
序列化与反序列化
概述
在谈论序列化与反序列化问题之前,我们需要先了解他们是什么,且作用有哪些。
序列化的意思就是将对象转化为字节序列的过程
反序列化则是将字节序列恢复为对象的过程
那么将对象序列化为字节序列有什么用呢?
将对象序列化为字节序列,可以在传递和保存对象的时候,保证对象的完整性和可传递性。使其易于保存在本地或者在网络空间中传输。
而反序列化,可以将字节流中保存的对象重建为对象
所以,其最核心的作用就是,对象状态的保存和重建
序列化优点
- 序列化后的为字节流的对象,存储在硬盘中方便JVM重启调用
- 序列化后的二进制序列能够减少存储空间,方便永久性保存对象
- 序列化成二进制字节流的对象方便进行网络传输
- 序列化后的对象可以进行进程间通信
Android中的序列化手段
基于上述的讨论,我们知道了何为序列化以及序列化的作用和优点。这其中提到序列化的一大特性就是用于进程间通信,而在后续提到的进程间通信手段中,他们共同的点都是传递信息时将对象序列化,接收信息时则是将对象反序列化。
在Android中需要学习使用到的序列化手段有两个,分别是 Serializable
和 Parcelable
-
Serializable
Serializable
是 Java 自带的序列化接口,我们使用者只需要继承Serializable
接口即可实现对该对象的序列化了。而具体去调用对其序列化和反序列化过程的也是 Java 提供的API。ObjectOutputStream
可以实现对象的序列化,ObjectInputStream
实现对象的反序列化。//序列化 User user = new User(1, "hello world", false); ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("cache.txt")); objectOutputStream.writeObject(user); objectOutputStream.close(); //反序列化 ObjectInputStream objectInputStream = new ObjectInputStream( new FileInputStream("cache.txt")); User user = (User) objectInputStream.readObject();//会在此处进行检查,是否同一 serialVersionUID objectInputStream.close();
需要注意的是,被序列化的
User
类一般情况下需要指定一个serialVersionUID
,其作用是对该类做唯一标识,在反序列化时候会进行 serialVersionUID 的比对,如果不一致则会认为版本不同出现报错。但是,如果不指定该 ID 也是可以正常实现序列化和反序列化的,因为系统会自动生成该类的 hash 值赋给 serialVersionUID 。那么为什么我们还要建议手动复制呢?因为 hash 值是根据类的变化在变化的,如果 ID 是 hash 值的话,我们在序列化对象后更改了对象的结构就会导致前后 ID 不一致,使得该对象无法被反序列化。但是手动指定的 ID 可以让被更改过的对象依旧可以被反序列化,可以最大限度地恢复其内容。
public class User implements Serializable { private static final long serialVersionUID = 519067123721295773L;//静态成员不参与序列化过程,代表类的状态 public int userId; public String userName; public boolean isMale; public transient int hhhhh;//被 transient 关键字修饰不参与序列化过程,代表对象的临时数据 public Book book;//该类必须可以被序列化,即继承了 Serializable 接口,否则会报错。每个成员变量都必须可被序列化。 }
-
Parcelable
Parcelable
是 Android 特有的序列化方法。他也是一个接口,实现该接口比 Serializable 要复杂一些。由于他是 Android 自带的序列化方法,所以对 Android 更加友好,实现该接口后的对象可以通过 Binder 来实现跨进程传递。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.userId = userId; this.userName = userName; this.isMale = isMale; } /** * 自定义构造函数,辅助反序列化过程,其中由于 book 是另一个可序列化对象 * 所以,反序列化过程需要传递当前线程的上下文类加载器 */ private User(Parcel in) { userId = in.readInt(); userName = in.readString(); isMale = in.readInt() == 1; book = in.readParcelable(Thread.currentThread().getContextClassLoader()); } /** * 内容描述,基本都返回 0 */ @Override public int describeContents() { return 0; } /** * 序列化过程 */ @Override 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>() { @Override public User createFromParcel(Parcel in) { return new User(in);//调用自写的构造函数 } @Override public User[] newArray(int size) { return new User[size]; } }; }
系统自带的实现了 Parcelable 接口的类有:Intent、Bundle、Bitmap…,其中 List 和 Map 也可以直接序列化,但是要保证其中的每个元素都是可序列化的。注意,基本数据类型是天然支持序列化的了。
优劣对比
Serializable:Java自带、操作简单,但是I/O操作多、开销大(多用于序列化到存储设备,以及网络传输中)
Parcelable:Android自带、效率高,使用稍微复杂些(Android 中首选,多用于内存序列化)
Binder_AIDL
上面提到过 Binder 可用于进程间通信。那么 Binder 是什么东西呢?我们该如何理解 Binder ?
Binder 实际上是 Android 内部的一个类,其实现了 IBinder 接口,主要作用是用于支持 Android 的跨进程通信。
如上图所示,我们可以视 Binder 为一个驱动,其连接客户端(进程1)与服务端(进程2),客户端和服务端绑定后就借助 Binder 驱动来通信和获取对应服务。而在 Android 的底层设计中,Binder 也是 ServiceManager 连接各种 Manager 的桥梁。
那标题的 AIDL 又是什么呢?AIDL 使用 Binder 进行进程间通信的较常用较典型的方法。查看一个简单的 AIDL 示例可以简单的了解它是如何使用 Binder 进行进程间通信的。
AIDL 全称为:Android Interface Definition Language,即Android接口定义语言。它是一种模板,我们根据对应的规则写好我们需要的通信接口之后, AIDL 会为我们自动生成对应的 IPC 代码。
这里做一个示例,假设我们需要进行进程间通信的相关类为 Book,需要的通信服务为获得 Book 书单,以及向 Book 书单中添加书籍。那么我么可以以如下方式编写一个 AIDL 文件。
首先需要将 Book 类序列化
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
private Book(Parcel source) {
bookId = source.readInt();
bookName = source.readString();
}
/**
* Describe the kinds of special objects contained in this Parcelable
* instance's marshaled representation. For example, if the object will
* include a file descriptor in the output of {@link #writeToParcel(Parcel, int)},
* the return value of this method must include the
* {@link #CONTENTS_FILE_DESCRIPTOR} bit.
*
* @return a bitmask indicating the set of special object types marshaled
* by this Parcelable object instance.
*/
@Override public int describeContents() {
return 0;
}
/**
* Flatten this object in to a Parcel.
*
* @param dest The Parcel in which the object should be written.
* @param flags Additional flags about how the object should be written.
* May be 0 or {@link #PARCELABLE_WRITE_RETURN_VALUE}.
*/
@Override public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
public static final Parcelable.Creator<Book> CREATOR = new Creator<Book>() {
/**
* Create a new instance of the Parcelable class, instantiating it
* from the given Parcel whose data had previously been written by
* {@link Parcelable#writeToParcel Parcelable.writeToParcel()}.
*
* @param source The Parcel to read the object's data from.
* @return Returns a new instance of the Parcelable class.
*/
@Override public Book createFromParcel(Parcel source) {
return new Book(source);
}
/**
* Create a new array of the Parcelable class.
*
* @param size Size of the array.
* @return Returns an array of the Parcelable class, with every entry
* initialized to null.
*/
@Override public Book[] newArray(int size) {
return new Book[size];
}
};
}
然后要编写两个 AIDL 文件
// Book.aidl
package com.qxy.potatos.module.test.aidl;
parcelable Book;
// IBookManager.aidl
package com.qxy.potatos.module.test.aidl;
import com.qxy.potatos.module.test.aidl.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
Book.aidl
文件中,需要对自定义序列化的 Book 类进行序列化声明。然后在 IBookManager.aidl
文件中,声明两个方法,分别对应提出的两个需求,同时记得一定要手动导入 Book 类的路径。
编写好 AIDL 文件后,将项目 rebuild 一下,就可以在 build 的 generate 文件中看到生成的 IBookManager
类了。生成的这个类就可以用于辅助我们进行进程间通信了。
由于生成的文件较长,这里就不放出生成的文件源码了。我们看其生成的模板代码,可以得知,在不同进程时候,调用的是 Stub 内部的代理类 Proxy 来执行跨进程功能。同一进程的时候,则不会走这种跨进程的 transact 功能。
其中,几类方法或者成员变量值得关注
- DESCRIPTOR:Binder 唯一标识,一般是类名
- asInterface:负责将服务端的 Binder 对象转化为该接口类型。根据是否同一进程有不同转换返回值。
- asBinder:返回当前 Binder 值
- onTransact:运行在服务端线程池中,从 data 中提出参数,执行 code 的目标,然后再 reply 中返回。该方法是 Boolean 类型,返回值为
true
表示执行成功,false
表示请求执行失败,可以控制这个返回值做权限隔离。 - Proxy#getBookList Proxy#addBook:这两个接口的实现方法运行在客户端线程池中。把参数信息写入 data 中,然后通过序列化、Binder 等手段发送给服务端去执行,然后线程挂起等待执行结果。如果有结果返回后,在 reply 中取出结果。
由上述可知,这样的 Binder 过程是耗时的,不应执行在 UI 线程中;同时,由于其运行规则,我们需要采取同步的方式来进行,即使它很耗时,都需要客户端挂起等待服务端响应。
如需完整版 Android学习笔记 请点击此处免费获取