0%

IPC 机制(三):IPC 基础概念介绍

1. Serializable 接口介绍

  • Serializable 是 Java 提供的一个序列化接口,它是一个空接口(标记接口),为对象提供标准的序列化和反序列化操作
  • 一个对象要实现序列化,只需要这个类实现 Serializable 接口并声明一个 serialVersionUID 常量即可,系统会自动完成序列化操作
  • 实际上,serialVersionUID 不是必须的,不声明 serialVersionUID 同样可以实现序列化,但是这会对反序列化过程产生影响(类结构没有发生改变或即使改变但能兼容时就没有影响)

2. serialVersionUID 的作用及工作机制

  • 作用

    • serialVersionUID 是用来辅助序列化和反序列过程的,原则上序列化后的数据中的 serialVersionUID 只有和当前类的 serialVersionUID 相同才能够正常地被反序列化
  • 工作机制

    • 序列化的时候系统会把当前类的 serialVersionUID 写入序列化的文件中(也可能是其他中介)

    • 反序列化的时候系统会去检测文件中的 serialVersionUID 是否和当前类的 serialVersionUID 一致

      • 如果一致就说明序列化的类的版本和当前类的版本是相同的,此时可以成功进行反序列化操作
      • 如果不一致就说明当前类和之前序列化的类相比发生了某些改变,此时反序列化会失败报错:java.io.InvalidClassException 发生 carsh
    • 一般来说,我们应该手动指定 serialVersionUID 的值,比如 1L,也可通过 IDE 根据当前类的结构自动生成它的 hash 值,两者效果一样。也就是说,当我们手动指定之后,就可以在很大程度上避免反序列化的失败

  • 特殊情况

    • 如果类的结构发生了非常规性改变,比如修改了类名、修改了成员变量的类型,这个时候尽管 serialVersionUID 验证通过了,但是反序列化过程还是会失败
    • 因为类的结构发生了根本性的改变,系统无法从老版本的数据中还原出一个新的类结构的对象
  • 需要注意的两点

    • 静态成员变量属于类不属于对象,所以不会参与序列化过程
    • 使用 transient 关键字标记的成员变量不会参与序列化过程
  • 默认序列化机制

    • 系统的默认序列化过程是可以改变的,通过重写 writeObject()readObject() 两个方法即可实现自定义的序列化过程
    • 大部分情况下我们不需要重写这两个方法,看具体需求实际情况决定是否自定义序列化过程

3. Serializable 接口使用 Demo

1
2
3
4
5
6
7
public class User implements Serializable {
private static final long serialVersionUID = 1L;

public int userId;
public String userName;
public boolean isMale;
}
1
2
3
4
5
// 序列化
User user = new User(0, "david", true);
ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream("cache.text"));
out.writeObject(user);
out.close();
1
2
3
4
// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.text"));
User newUser = (User) in.readObject();
in.close();

4. Parcelable 接口介绍

  • 在序列化过程中需要实现的功能有序列化反序列化内容描述
  • 序列化功能:由 writeToParcel() 方法完成,最终是通过 Parcel 中的一系列 write() 方法来完成
  • 反序列化功能:由 CREATOR 完成,其内部标明了如何创建序列化对象和数组,并通过 Parcel 的一系列 read() 方法来完成反序列化过程
  • 内容描述功能:由 describeContents() 方法完成,几乎在所有情况下这个方法都应该返回 0,仅当当前对象中存在文件描述符时返回 1
  • 系统已经为我们提供了许多实现了 Parcelable 接口的类,它们都是可以直接序列化的。比如 IntentBundleBitmap 等,同时 ListMap 也可以序列化,前提是它们里面的每个元素都是可序列化的

5. Parcelable 中的方法说明

方法 功能 标记位
createFromParcel(Parcel in) 从序列化后的对象中创建原始对象
newArray(int size) 创建指定长度的原始对象数组
User(Parcel in) 从序列化后的对象中创建原始对象
writeToParcel(Parcel out, int flags) 将当前对象写入序列化结构中,其中 flags 标识有两种值:0 或 1(参见右侧标记位)。为 1 时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为 0 PARCELABLE_WRITE_RETURN_VALUE
describeContents() 返回当前对象的内容描述。如果含有文件描述符,返回 1(参见右侧标记位),否则返回 0,几乎所有情况都返回 0 CONTENTS_FILE_DESCRIPTOR

6. Parcelable 接口使用 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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;
}

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.readString();
isMale = in.readInt() == 1;
// 需要注意的是,由于 book 是另一个可序列化对象,所以它的反序列化过程需要传递当前线程的上下文类加载器,否则会报无法找到类的错误
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
}

7. Serializable 和 Parcelable 两种序列化方式的对比

  • Serializable 是 Java 中的序列化接口,其使用起来简单但是开销很大,因为序列化和反序列化过程需要大量 IO 操作
  • Parcelable 是 Android 中的序列化方式,因此更适合用在 Android 平台上,缺点是使用起来稍微麻烦,但效率很高,是 Android 推荐的序列化方式

8. Binder 简介

  • 直观来说:Binder 是 Android 中的一个,它实现了 IBinder 接口
  • 从 IPC 角度:Binder 是 Android 中的一种跨进程通信方式,还可以把 Binder 理解为一种虚拟的物理设备,它的设备驱动是 /dev/binder,该通信方式在 Linux 中没有
  • 从 Android Framework 角度:Binder 是 ServiceManager 连接各种 Manager(ActivityManager、WindowManager 等)和相应 ManagerService 的桥梁
  • 从 Android 应用层角度:Binder 是客户端和服务端进行通信的媒介,当 bindService 的时候,服务端会返回一个包含了服务端业务调用的 Binder 对象,通过这个 Binder 对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于 AIDL 的服务

9. Binder 的工作机制简介

  • 首先,当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在 UI 线程中发起此远程请求
  • 其次,由于服务端的 Binder 运行在 Binder 线程池中,所以 Binder 方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了
  • 流程图
    Binder 的工作机制

10. 手动实现一个 Binder 简介

  • 声明一个 AIDL 性质的接口,只需要继承 IInterface 接口即可
  • 实现内部类 Stub 和 Stub 类中的 Proxy 代理类,这段代码我们可以自己写,但是写出来后会发现和系统自动生成的代码是一样的,因此这个 Stub 类我们只需参考系统生成的代码即可,只是结构上需要做一下调整
  • 实际开发中完全可以通过 AIDL 文件让系统去自动生成 Binder,AIDL 文件的本质是系统为我们提供了一种快速实现 Binder 的工具,仅此而已

11. Binder 中的 方法 linkToDeath() 和 unlinkToDeath() 介绍

  • 原因

    • Binder 运行在服务端进程,如果服务端进程由于某种原因异常终止,这个时候我们到服务端的 Binder 连接断裂(称之为 Binder 死亡),会导致我们的远程调用失败
    • 更为关键的是,如果我们不知道 Binder 连接已经断裂,那么客户端的功能就会受到影响
  • 解决

    • 为了解决 Binder 死亡的问题,Binder 中提供了两个配对的方法 linkToDeath()unlinkToDeath()
    • 通过方法 linkToDeath(),我们可以给 Binder 设置一个死亡代理,当 Binder 死亡时,我们就会收到通知,这个时候就可以重新发起连接请求从而恢复连接
  • 给 Binder 设置死亡代理的步骤

    • 首先,声明一个 DeathRecipient 对象。DeathRecipient 是一个接口,其内部只有一个方法 binderDied(),我们需要实现这个方法,当 Binder 死亡的时候,系统就会回调 binderDied() 方法,然后就可以移出之前绑定的 binder 代理并重新绑定远程服务:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient () {
      @Override
      public void binderDied () {
      if (mBookManager == null) {
      return;
      }
      mBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
      mBookManager = null;
      // TODO: 这里重新绑定远程 service
      }
      }
    • 其次,在客户端绑定远程服务成功后,给 binder 设置死亡代理:

      1
      2
      3
      mService = IMessageBoxManager.Stub.asInterface(binder);
      // 第二个参数是标记位,直接设为 0 即可
      binder.linkToDeath(mDeathRecipient, 0);
    • 最后,也可以通过 Binder 的 isBinderAlive() 方法判断 Binder 是否死亡

-------------------- 本文结束感谢您的阅读 --------------------