Android深入理解IPC机制(一)基础知识概要

为了加强自己对Android中的IPC机制的理解,特意重温了一遍《Android开发艺术探索》关于IPC技术的详细讲解。趁着最近手头上的事少了一些整理出这篇文章,如有错误欢迎指正。

  • Android IPC简介
  • Android中的多进程模式
  • IPC基础概念介绍
  • Android中的几种IPC方式
  • Binder连接池
  • 选择合适的IPC方式

一、Android IPC简介

IPC全称Inter-Process Communication,含义为进程间通信或跨进程通信,是指在两个进程之间进行数据交换。那么什么是进程呢?它与我们常说的线程之间的关系又是什么?按照操作系统的描述,线程是CPU调度的最小单元,而进程一般是指一个执行单元,在Android中进程一般是指一个应用程序,一个进程可以包含一个或多个线程。每个Android应用都有一个主线程,也就是我们常说的UI线程,假如我们试图在非主线程上更新UI,系统会抛出CalledFromWrongThreadException异常。

android.view.ViewRootImpl$CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views.

通常情况下我们要避免在主线程上执行耗时任务,否则的话会造成界面长时间无法响应(ANR-Application Not Response),这是非常糟糕的用户体验。这个时候我们就需要用到子线程,把这些耗时任务交给子线程去执行,任务完成之后再通知主线程更新UI。

IPC机制不是Android系统所独有的,其他系统也有相应的进程间通信机制。Android中最有特色的进程间通信方式就是Binder了,这也是我们接下来会着重介绍的内容。我们使用多进程的原因不外乎两种:

  1. 第一种情况是一个应用因为某些原因自身需要开启多进程模式,原因可能有很多,比如某些模块需要单独运行在一个独立的进程中,又或者是为了获取更多可使用内存而开启新进程。我们不用担心某个应用会占用系统过多的内存资源,因为Android系统对单个应用能使用的最大内存做了限制。
  2. 第二种情况是两个不同的应用之间需要进行数据交互,不同的应用本身就是两个不同的进程,因此就必须要使用进程间通信方式来达到数据交换的目的。

总之,我们应该谨慎地使用多进程技术,一旦我们采用了多进程的设计方法,我们就必须要妥善地处理进程间通信的各种问题。

二、Android中的多进程模式

1、开启多进程模式

常规情况下,Android中使用多进程只有一个方法,那就是在AndroidManifest.xml中为四大组件指定android:process属性。这看起来似乎很简单,但是实际上我们可能需要付出更大的努力去解决使用多进程所带来的代码层面的负面影响。下面是一个多进程示例:


当H5Activity启动时,系统会为它创建一个单独的进程,进程名称为"包名:remote"。其他没有指定android:process属性的Activity,会运行在默认进程中,默认进程名称与包名相同。我们可以在Android Studio中查看当前应用运行的进程列表。


进程列表

属性android:process=":remote"中":"的含义是指要在当前进程名前面加上应用包名,这是一种简写的方法。其次,进程名以":"开头的进程属于当前应用的私有进程,其他应用的组件不能和它运行在同一个进程中,不以这方式命名的进程属于全局进程,其他组件可以通过ShareUID与它运行在同一进程中。

还有一种非常规的多进程方法,那就是通过JNI在native层去fork一个新进程,我们暂时不讨论这种特殊的方式。

2、多进程模式的运行机制

Android系统为每个进程都分配一个独立的虚拟机,每个虚拟机在内存分配上有不同的地址空间,因此在不同的虚拟机中访问同一个类的对象会产生多个副本。因此运行在不同进程的组件,是不能通过内存共享数据的,必须要借助一些中间层来实现数据共享。
一般来说,使用多进程会造成下面的几个问题:

  1. 静态成员和单例模式完全失效
  2. 线程同步机制完全失效
  3. SharedPreference的可靠性降低
  4. Application会多次创建

1、2两个问题本质上是相同的,既然都不在同一块内存了,那么不管是锁对象还是锁全局类,都没法保证线程同步;
问题3是因为SharedPreference底层是通过读写XML文件实现的,并发写显然是会出问题的,甚至并发读写都有可能出现问题;
问题4是因为当组件需要运行在一个新进程中,系统就要创建一个新的进程并且为新进程分配独立的虚拟机,这实际上就是启动一个应用的过程,既然是启动应用,那么自然会创建新的Application。问题4很容易验证,我们只需要连续启动3个同一应用内属于不同进程的Activity,然后观察应用的Application类是否执行了三次onCreate方法。

三、IPC基础概念介绍

Serializable和Parcelable接口可以完成对象的序列化过程,当我们需要通过Intent和Binder传输数据的时候就需要对数据进行序列化处理。还有的时候我们需要将数据持久化保存到本地或者通过网络传输给其他终端,我们也需要Serializable完成对象的持久化。

1、Serializable接口

Serializable是Java提供的序列化接口,它是一个空接口,为对象提供序列化与反序列化操作。我们只需在类中指定一个"serialVersionUID"标识就可以自动实现默认的序列化过程。

Tips:AndroidStudio 安装GenerateSerialVersionUID插件可以帮助开发者快速生成serialVersionUID。

public class News implements Serializable {

    private static final long serialVersionUID = 3110845084273175296L;

    private int id;
    private String title;
    private String author;
    private String publishDate;
    private String summary;
    ……

}

通过Serializable 接口实现对象序列化非常简单,几乎所有工作都是系统自动完成的。实现对象的序列化和反序列化也非常简单,我们只需采用ObjectInputStream和ObjectOutputStream即可轻松实现。我们看一个经典的例子:


序列化和反序列化

如图所示,只需要把实现了Serializable 接口的对象写入到文件中,反序列化的时候就可以直接从文件中读取对象数据,恢复后的对象与之前的对象虽然内容相同,但是并不是同一个对象。

想知道serialVersionUID到底是干什么的吗?戳这里:java类中serialVersionUID作用 是什么?举个例子说明

2、Parcelable接口

作用与Serializable 接口类似,用法如下:

public class ParcelableTest implements Parcelable {

    private int i;
    private String s;
    private WeatherInfo w;  //WeatherInfo 类必须也实现了序列化接口

    public ParcelableTest(int i, String s, WeatherInfo w) {
        this.i = i;
        this.s = s;
        this.w = w;
    }

    /**
     * 完成反序列化,通过通过Parcel的一系列read方法完成
     */
    public static final Creator CREATOR = new Creator() {
        @Override
        public ParcelableTest createFromParcel(Parcel in) {
            return new ParcelableTest(in);
        }

        @Override
        public ParcelableTest[] newArray(int size) {
            return new ParcelableTest[size];
        }
    };

    protected ParcelableTest(Parcel in) {
        i = in.readInt();
        s = in.readString();
        // w是另一个可序列化对象,所以它的反序列化过程需要传递当前线程
        // 的类上下文加载器,否则会报无法找到该类的错误
        w = in.readParcelable(WeatherInfo.class.getClassLoader());
    }

    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * 实现序列化,通过Parcel的一系列write方法完成
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(i);
        dest.writeString(s);
        dest.writeParcelable(w, flags);
    }
}

注:写入数据的顺序和读出数据的顺序必须是相同的。

详细方法说明

Serializable 与 Parcelable对比

  1. Serializable 是Java中的序列化接口,使用起来简单;Parcelable是Android中的序列化方式,使用起来稍微复杂一些。
  2. Serializable 在序列化操作的时候会产生大量的临时变量,从而导致GC的频繁调用(原因是使用了反射机制);Parcelable是以Ibinder作为信息载体的,在内存上的开销比较小,因此在内存之间进行数据传递的时候,Android推荐使用Parcelable。
  3. 将对象序列化到存储设备上或者序列化后通过网络传输,使用Parcelable 会稍显复杂,因此这两种情况建议使用Serializable。

由于Binder是一个很深入的概念,所以这里暂时不对它进行分析。


推荐阅读

Android深入理解IPC机制(二)浅谈Binder
Android深入理解IPC机制(三) Android中的几种IPC方式
Android深入理解IPC机制(四)Binder连接池

参考书籍

《Android开发艺术探索》

你可能感兴趣的:(Android深入理解IPC机制(一)基础知识概要)