likes
comments
collection
share

解密 Android IPC 机制_初识篇

作者站长头像
站长
· 阅读数 45

我正在参加「掘金·启航计划」

引言

在我们使用 Android 手机的时候,有时我们使用的软件会需要消耗比较大的内存,也经常会需要同时打开多个软件。这些时候,我们都会需要使用到多进程技术。作为 Android 开发者,相信我们都知道如何去开启应用的单个多进程,但是开启多进程之后,如何进行「进程间通信(IPC)」呢?进程通信的方法有很多,他们适用于不同的使用场景,下面我们来逐个了解。

前置知识

  • Android 四大组件
  • Java 序列化

IPC简介

相信学过大学「操作系统」这门课的同学都还记得 进程间通信 信号量机制 这些名词,今天我们学习的也是操作系统的通信,不过是针对以 Linux 为内核的 Android 操作系统。我们通常会以一个软件或者程序为进程。而 Android 是可以使用多进程的,对于稍微大型一些的软件也都会使用到多进程。使用多进程的目的有如下几个:

  1. 进程隔离,以达到某些特殊的业务需求
  2. 扩大软件的内存大小。

多进程的开启很简单,其唯一方法是给注册文件 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,则表示该进程是全局的,可共享给其他应用的。

而多进程出现后会导致如下的一些问题:

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

上述问题为何会出现呢?

由于 Android 为每个独立进程都分配了一个虚拟机,那么虚拟机的内存空间必然是不同的,所以不同的量在不同内存中都有一份副本,在不同进程中只能修改其内存下的副本。所以1和2中,无论是加何种锁,不作用在同一片内存空间中都是失效的。

而3则是由于 SharePreferences 是存储在 xml 文件中的,不同进程对该文件的并发读写时会导致数据出错的

4中则是由于 Android 的启动机制是每次都要由启动新的 Application,则每个进程都会有一个自己的 Application。我们也需要着重注意这个问题,在Application 中做好启动分类,在多进程启动阶段,防止不需要的资源多次加载

基于上述的原因和问题,我们需要深入了解 IPC 机制,让跨进程通信更好的服务于我们,解决多进程所带来的问题。

IPC基础知识

序列化与反序列化

概述

在谈论序列化与反序列化问题之前,我们需要先了解他们是什么,且作用有哪些。

序列化的意思就是将对象转化为字节序列的过程

反序列化则是将字节序列恢复为对象的过程

那么将对象序列化为字节序列有什么用呢?

将对象序列化为字节序列,可以在传递和保存对象的时候,保证对象的完整性和可传递性。使其易于保存在本地或者在网络空间中传输。

而反序列化,可以将字节流中保存的对象重建为对象

所以,其最核心的作用就是,对象状态的保存和重建

序列化优点
  1. 序列化后的为字节流的对象,存储在硬盘中方便JVM重启调用
  2. 序列化后的二进制序列能够减少存储空间,方便永久性保存对象
  3. 序列化成二进制字节流的对象方便进行网络传输
  4. 序列化后的对象可以进行进程间通信
Android中的序列化手段

基于上述的讨论,我们知道了何为序列化以及序列化的作用和优点。这其中提到序列化的一大特性就是用于进程间通信,而在后续提到的进程间通信手段中,他们共同的点都是传递信息时将对象序列化,接收信息时则是将对象反序列化。

在Android中需要学习使用到的序列化手段有两个,分别是 SerializableParcelable

  • 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 的跨进程通信。

解密 Android IPC 机制_初识篇

如上图所示,我们可以视 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 线程中;同时,由于其运行规则,我们需要采取同步的方式来进行,即使它很耗时,都需要客户端挂起等待服务端响应。

参考

序列化和反序列化有什么作用?java序列化和反序列化的作用小官学长的博客-CSDN博客

关于Java中Serializable的一些问题_viclee108的博客-CSDN博客

教妹学Java(九):一文搞懂Java中的基本数据类型沉默王二的博客-CSDN博客java基本数据类型

Intent、Bundle传递数据的那些秘密_请叫我鲜鲜哥的博客-CSDN博客

写给 Android 应用工程师的 Binder 原理剖析 - 掘金 (juejin.cn)

Java中线程安全的集合java线程安全的集合Willing卡卡的博客-CSDN博客

转载自:https://juejin.cn/post/7231481633672544293
评论
请登录