读书笔记-艺术探索- IPC机制(基础)

3. IPC机制(基础)

3.0 前言

本文总结自任玉刚老师的《Android开发艺术探索》,文章中的【示例】在这里

3.1 Android IPC简介

IPC (Inter-Process Communication,含义为进程间通信或跨进程通信),是指两个进程之间进行数据交换的过程。Bundle、文件共享、AIDL(Android Interface definition language一种android内部进程通信接口的描述语言,通过它我们可以定义进程间的通信接口,涉及Binder连接池的概念)、Messenger、ContentProvider、Socket都是进程间通信的方式。

进程:和线程是截然不同的概念。线程CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程(是包含的关系)。最简单的情况下,一个进程中可以只有一个线程,即主线程。Android中主线程也叫UI线程,在UI线程里面才能操作页面元素(耗时的任务在主线程中执行会造成界面无法响应:ANR,所以要把耗时的线程放在其他线程中)。

任何一个操作系统都需要有相应的IPC机制,Android中最有特色的进程间通信方式就是Binder了,通过Binder可以轻松地实现进程间通信。除了Binder还有Socket,也可以实现任意两个终端之间的通信。

3.2 Android中的多进程模式

说到IPC的使用场景就必须提到多线程,只有面对多进程的场景下,才需要考虑进程间通信。多进程的情况分为2种:

  • 应用因为某些原因自身需要采用多进程模式来实现(有些模块由于特殊原因需要运行在单独的进程中、 为了加大一个应用可使用的内存所以需要通过多线程来获取多份内存空间等)。
  • 当前应用需要向其他应用获取数据,由于是两个应用,所以必须采用跨进程的方式来获取所需的数据(用ContentProvider去查询数据等)

不管由于何种原因,我们采用了多进程的设计方法,应用中必须妥善处理进程间通信的各种问题。

3.2.1 开启多进程模式

正常情况下,Android中多进程是指一个应用中存在多个进程的情况(暂不讨论两个应用之间的多进程情况,因为这样操作起来比较方便,如果是不同进程间通信,那么它们分别是否属于两个应用没有区别,原理一样)。

Android中使用多进程只有一种方法,那就是给四大组件(Activity、Service、Recevier、ContentProvider)在AndroidMenifest中指定android:process属性,除此之外没有其他方法,也就是说我们无法给一个线程或者一个实体类指定其运行时所在的进程(其实还有另外一种非常规的多进程方法,那就是通过JNI在native层去fork一个新的进程)。下面示例如何在Android中创建多进程:

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity
    android:name=".SecondActivity"
    android:configChanges="screenLayout"
    android:label="@string/app_name"
    android:process=":remote"/>
<activity
    android:name=".ThirdActivity"
    android:configChanges="screenLayout"
    android:label="@string/app_name"
    android:process="com.example.learn_002_ipc.remote"/>

上例分别为SecondActivity和ThirdActivity指定了process属性,并且属性值不同,这意味着当前应用又增加了两个新进程。SecondActivity启动时,系统会为它创建一个单独的进程,进程名为“com.example.learn_002_ipc:remote”(当前包名为“com.example.learn_002_ipc”);当ThirdActivity启动时,系统也会为它创建一个单独的进程,进程名为“com.example.learn_002_ipc.remote”同时入口Activity是MainActivity,没有为它指定process属性,那么它运行在默认进程中,默认的进程名是包名,如图:

image

进程列表末尾存在3个进程,进程id分别是6612、6680、6704,这说明应用成功使用了多进程技术。关于上面的":remote"和"com.example.learn_002_ipc.remote"其实是有区别的:“:”的含义是指要在当前的进程名前面附加上当前的包名(简写),对于SecondActivity来说,它完整的进程名为com.example.learn_002_ipc:remote;对于ThirdActivity中的声明方式来说,它是一种完整的命名方式。并且,以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程为全局进程,其他应用可以通过ShareUID方式可以和它跑在同一进程中。

Android系统会为每个应用分配一个唯一的UID,具有相同的UID的应用才能共享数据,而两个应用通过ShareUID跑在同一个进程中要求要有相同的ShareUID并且签名相同才可以。在这种情况下,它们可以互相访问对方的私有数据,比如data目录、组件信息,共享内存数据...或者说它们看起来像是一个应用的两个部分。

3.2.2 多进程模式的运行机制

“当应用开启了多线程后,各种奇怪的现象都出现了",这是用来形容多线程的。

这里举一个例子,在上例中新建UserManager类,这个类中有个public的静态成员变量:public static int sUserId = 1;在MainActivity的onCreate中把这个sUserId重新赋值为2,打印出这个静态变量的值后再启动SecondActivity,在其中再打印一下sUserId的值,在Android Device Monitor中可以看出在MainActivity中输出的sUserId是2,在SecondActivity中输出的是1。

出现上述问题的原因是SecondActivity运行在一个单独的进程中,Android为每一个应用分配了一个独立的虚拟机(即每一个进程都分配一个独立的虚拟机),不同的虚拟机在内存分配上会有不同的地址空间,导致不同的虚拟机中访问同一个对象会产生多份副本。该例中进程com.example.learn_002_ipc和进程com.example.learn_002_ipc:remote都存在一个UserManager类,并且这两个类互不干扰,在一个进程中修改sUserId的值只会影响当前进程,对其他进程不会造成任何影响。

所有运行在不同进程中的四大组件,只要它们之间需要同内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。正常情况下,四大组件中间不可能不通过一个中间层来共享数据,那么通过简单地指定进程名来开启多线程都会无法正确运行。当然特殊情况下,某些组件之间不需要共享数据,这时可以直接指定android:process属性来开启多线程,但是这种场景不常见,几乎所有情况都需要共享数据。

一般来说多进程会造成如下几方面问题

  • 静态成员和单例模式完全失效。(上例)
  • 线程同步机制完全失效。(不是同一块内存了,不管锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象)
  • SharedPreferences可靠性下降。(SharedPreferences不支持两个进程同时执行写操作否则会导致一定机率的数据丢失,这是因为SharedPreferences是通过读/写XML文件实现的,并发读/写都有可能出问题)
  • Applicaion会多次创建。(新建一个MyApplication类,注意在Manifest内设置android:name,然后在MyApplication的onCreate方法中每次启动都打印一次当前线程的名字,再在上例中分别启动3个Activity,locat中可以看出的Application创建了3次,而且每次的进程名称和进程id都不一样)

总结:多进程模式中,不同进程的组件拥有独立的虚拟机、Application、内存空间。这样理解同一个应用间的多进程:相当于两个不同的应用采用了SharedUID的模式。为了解决这个问题,系统提供了很多跨进程通信方法(虽然不能直接地共享内存,但是还是可以实现数据交互):使用Intent来传递数据、共享文件SharedPreferences、基于Binder的MessengerAIDLSocket

3.3 IPC基础概念介绍

以下若干名词都是IPC的基础概念,先大概了解一下:Serializable和Parcelable接口可以完成对象的序列化过程,当我们需要通过Intent和Binder传输数据时就需要使用Parcelable或者Serilizable。还有的时候我们需要把对象持久化到存储设备上或者通过网络传输给其他客户端,也需要Serializable来完成对象的持久化。

序列化 (Serialization):将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。

3.3.1 Serializable接口

java.io.Serializable/public interface : Marks classes that can be serialized by ObjectOutputStream and deserialized by ObjectInputStream.

标记可以由ObjectOutputStream序列化并由ObjectInputStream反序列化的类。

该接口是Java提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable来实现序列化相当简单,只需要令该类实现Serializable接口,并在其声明中指定一个标识(serialVersionUID,但这一步甚至不是必须的)即可自动实现默认的序列化过程:

public class User implements Serializable {
    private static final long serialVersionUID = 909090L;
    public int userId;
    public String userName;
    public boolean isMale;
}

如果不实现这个接口那么序列化的时候会遇到一个很耿直的异常:

java.io.WriteAbortedException: writing aborted.io.NotSerializableException

通过Serializable方式来实现对象的序列化,实现起来非常简单,几乎所有工作都被系统自动完成了。进行对象的序列化和反序列化也非常简单,只需采用ObjectOutputStream和ObjectInputStream即可轻松实现:

//序列化过程(The serialization process)
User user = new User(0, "jake", true);
try {
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
    out.writeObject(user);
    out.close();
} catch (IOException e) {
    e.printStackTrace();
}

//反序列化过程(The deserialization process)
try {
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
    User newUser = (User)in.readObject();
    in.close();
} catch (Exception e) {
    e.printStackTrace();
}

上述演示了Serializable方式序列化对象的典型过程,只需把实现了Serializable接口的User对象写到文件中就可以快速恢复了,恢复后的对象newUser和user的内容完全一样,但是两者不是同一个对象。

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

序列化的时候系统把当前类的serialVersionUID写入序列化的文件中(也有可能是其他中介),当反序列化的时候就回去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这时候就可以成功反序列化;否则说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的(报java.io.InvalidClassException)。

一般来说,应该手动指定serialVersionUID的值,比如1L,也可以让AS根据当前类的结构自动生成它的hash值(本质一样),这样序列化和反序列化时两者的serialVersionUID是相同的,因此可以正常进行反序列化。

如果不手动指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或删除了某些成员变量,系统会重新计算当前类的hash值并把它赋值给serialVersionUID,这时候类的serialVersionUID就和序列化数据中的serialVersionUID不一致,于是反序列化失败,程序就会出现crash,所以手动指定serialVersionUID可以很大程度避免反序列化过程的失败。

另外,系统的默认序列化过程也是可以改变的(重写writeObject和readObject方法)。

3.3.2 Parcelable接口

android.os.Parcelable/public interface : Interface for classes whose instances can be written to and restored from a Parcel. Classes implementing the Parcelable interface must also have a static field called CREATOR, which is an object implementing the Parcelable.Creator interface.

其实例可以写入Parcel并从中恢复的类的接口。实现Parcelable接口的类还必须有一个名为CREATOR的静态字段,该字段是实现Parcelable.Creator接口的对象。

java.lang.Object ↳ android.os.Parcel* / public final class*: Container for a message (data and object references) that can be sent through an IBinder. A Parcel can contain both flattened data that will be unflattened on the other side of the IPC (using the various methods here for writing specific types, or the general Parcelable interface), and references to live IBinder objects that will result in the other side receiving a proxy IBinder connected with the original IBinder in the Parcel.

可以通过IBinder发送的消息(数据和对象引用)的容器。一个Parcel可以包含在IPC的另一侧(使用这里的各种方法来编写特定类型,或者通用的Parcelable接口)的平坦数据,以及引用另一方接收的活IBinder对象的引用一个代理IBinder与Parcel中的原始IBinder连接。

在Android中也提供了新的序列化方式,就是Parcelable接口,使用Parcelabel接口来实现对象的序列化,其过程要稍微复杂一些,只要实现这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递,下例是一个典型用法:

public class User2 implements Parcelable {

    public int userId;
    public String userName;
    public boolean isMale;

    public User user;

    public User2(int userId, String userName, boolean isMale) {
        this.userId = userId;
        this.userName = userName;
        this.isMale = isMale;
    }

    //序列化过程(The serialization process)
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(userId);
        dest.writeString(userName);
        dest.writeByte((byte) (isMale ? 1 : 0));
    }

    //反序列化过程(The deserialization process)
    public static final Creator<User2> CREATOR = new Creator<User2>() {
        @Override
        public User2 createFromParcel(Parcel in) {
            return new User2(in);
        }

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

    protected User2(Parcel in) {
        userId = in.readInt();
        userName = in.readString();
        isMale = in.readByte() != 0;
    }

    //内容描述过程(content description)
    @Override
    public int describeContents() {
        return 0;
    }
}

Parcel内部包装了可序列化的数据,可以在Binder中自由传输,从上述代码中可看出,序列化过程中需要实现的功能有序列化、反序列化和内容描述。

  • 序列化功能由writeToParcel方法来完成,最终是通过Parcel中的一系列write方法**来完成的。
  • 反序列化功能由CREATOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel的一系列read方法**来完成反序列化过程。
  • 内容描述功能由describeContents方法来完成**,几乎所有情况下该方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1。

需要注意的是,在User2(Parcel in) 方法中,由于user是另一个可序列化对象。以它的反序列化过程需要传递当前线程的上下文类加载器,否则会无法找到类的错误。下面是Parcelable的方法说明:

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

系统已经为我们提供了很多实现了Parcelable接口的类,他们都是可以直接序列化的,比如Intent、Bundle、Bitmap等,同时List和Map也可以序列化,前提是它们里面的每个元素都是可序列化的。

关于Parcelable和Serializable的区别:

  • Serializable是Java中的序列化接口,使用简单但是开销很大(序列化的时候会产生大量的临时变量,从而引起频繁的GC),序列化和反序列化过程都需要大量I/O操作,主要用于将对象序列化到存储设备(磁盘)中或者将对象序列化后通过网络传输,因为Parcelable的过程较复杂。
  • Parcelable是Android中的序列化方式,因此更适合用在Android平台上,缺点是操作稍微麻烦,但是效率高,更适合用在Android平台上。主要用在内存序列化上。

3.3.3 Binder

java.lang.Object ↳ android.os.Binder* / public class / extends Object / implements IBinder* : Base class for a remotable object, the core part of a lightweight remote procedure call mechanism defined by IBinder. This class is an implementation of IBinder that provides the standard support creating a local implementation of such an object.Most developers will not implement this class directly, instead using the aidl tool to describe the desired interface, having it generate the appropriate Binder subclass. You can, however, derive directly from Binder to implement your own custom RPC protocol or simply instantiate a raw Binder object directly to use as a token that can be shared across processes.

Binder : 可远程对象的基类,是由IBinder定义的轻量级远程过程调用机制的核心部分。该类是IBinder的一个实现,它提供了创建此类对象的本地实现的标准支持。大多数开发人员不会直接实现这个类,而是使用aidl工具来描述所需的接口,让它生成适当的Binder子类。但是,您可以直接从Binder派生实现您自己的自定义RPC协议,或直接实例化原始Binder对象以用作可跨进程共享的令牌(token)。

android.os.IBinder* / public interface* : Base interface for a remotable object, the core part of a lightweight remote procedure call mechanism designed for high performance when performing in-process and cross-process calls. This interface describes the abstract protocol for interacting with a remotable object. Do not implement this interface directly, instead extend from Binder.

IBinder : 可远程对象的基础接口,轻量级远程过程调用机制的核心部分,专为执行进程内和跨进程调用时的高性能而设计。该接口描述了与可远程对象交互的抽象协议。不要直接实现这个接口,而是从Binder扩展。

从IPC角度来说,Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有;

从Android Framework角度来说,Binder是ServiceManager链接各种Manager(ActivityManager、WindowManager等等)和相应ManagerService的桥梁

从Android应用层来说,Binder是客户端和服务端进行同行的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL(Android Interface Definition Language )的服务。

Android开发中,Binder主要用在Service中,包括AIDLMessenger,而Messenger的底层其实是AIDL(其中普通Service中的Binder不涉及进程间通信,较为简单不涉及Binder的核心)。

所以这里选择用AIDL来分析Binder的工作机制。新建Java包com.example.learn_002_ipc.aidl,创建以下3个文件:
package com.example.learn_002_ipc.aidl;

import android.os.Parcel;
import android.os.Parcelable;

//Book.java
public class Book implements Parcelable {

    public int bookId;
    public String bookName;

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    @Override
    public int describeContents() {     //内容描述
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {      //序列化操作
        dest.writeInt(bookId);
        dest.writeString(bookName);
    }

    public static final Creator<Book> CREATOR = new Creator<Book>() {   //反序列化操作
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

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

    protected Book(Parcel in) {
        bookId = in.readInt();
        bookName = in.readString();
    }
}

`

//Book.aidl
package com.example.learn_002_ipc.aidl;

parcelable Book;

`

//IBookManager.aidl
package com.example.learn_002_ipc.aidl;
import com.example.learn_002_ipc.aidl.Book;

interface IBookManager {       //图书管理员
    List<Book> getBookList();
    void addBook(in Book book);
}

上面3个文件中,Book.java是一个表示图书信息的类,它实现了Parcelable接口。Book.aidl是Book类在AIDL中的声明。IBookManager.aidl是我们定义的一个接口,里面有2个方法:getBookList和addBook,其中getBookList用于从远程服务端获取图书列表,而addBook用于往图书列表中添加一本书。可以看到,尽管Book类和IBookManager位于相同的包中,但是再IBookManager中仍然要导入Book类,这就是AIDL的特别之处(如果忘记导入了或者输入的包地址错误,会导致意料之外的错误)。

下面是系统为IBookManager.aidl生成的Binder类(在gen目录下的com.example.learn_002_ipc.aidl包中的IBookManager.java):

/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: F:\\AndroidStudioProjects\\Learn_002_ipc\\app\\src\\main\\aidl\\com\\example\\learn_002_ipc\\aidl\\IBookManager.aidl
 */
package com.example.learn_002_ipc.aidl;

public interface IBookManager extends android.os.IInterface {

    /** Local-side IPC implementation stub class. */

    public static abstract class Stub extends android.os.Binder implements com.example.learn_002_ipc.aidl.IBookManager {

        private static final java.lang.String DESCRIPTOR = "com.example.learn_002_ipc.aidl.IBookManager";
        /** Construct the stub at attach it to the interface. */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }
        /**
         * Cast an IBinder object into an com.example.learn_002_ipc.aidl.IBookManager interface,
         * generating a proxy if needed.
         *
         * 用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象。
         */
        public static com.example.learn_002_ipc.aidl.IBookManager asInterface(android.os.IBinder obj) {
            if ((obj**null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin!=null)&&(iin instanceof com.example.learn_002_ipc.aidl.IBookManager))) {  //如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身。
                return ((com.example.learn_002_ipc.aidl.IBookManager)iin);
            }
            return new com.example.learn_002_ipc.aidl.IBookManager.Stub.Proxy(obj);  //否则返回的是系统封装后的Stub.proxy对象。
        }
        @Override
        public android.os.IBinder asBinder() {
            return this;
        }
        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_getBookList: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<com.example.learn_002_ipc.aidl.Book> _result = this.getBookList();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addBook: {
                    data.enforceInterface(DESCRIPTOR);
                    com.example.learn_002_ipc.aidl.Book _arg0;
                    if ((0!=data.readInt())) {
                        _arg0 = com.example.learn_002_ipc.aidl.Book.CREATOR.createFromParcel(data);
                    }
                    else {
                        _arg0 = null;
                    }
                    this.addBook(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }
        private static class Proxy implements com.example.learn_002_ipc.aidl.IBookManager {
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }
            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }
            @Override
            public java.util.List<com.example.learn_002_ipc.aidl.Book> getBookList() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.example.learn_002_ipc.aidl.Book> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.example.learn_002_ipc.aidl.Book.CREATOR);
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
            @Override
            public void addBook(com.example.learn_002_ipc.aidl.Book book) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if ((book!=null)) {
                        _data.writeInt(1);
                        book.writeToParcel(_data, 0);
                    }
                    else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
                    _reply.readException();
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }
        static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }
    public java.util.List<com.example.learn_002_ipc.aidl.Book> getBookList() throws android.os.RemoteException;
    public void addBook(com.example.learn_002_ipc.aidl.Book book) throws android.os.RemoteException;
}

对于这个类可以看到它继承了IInterface这个接口(Binder接口的基类。只有一个抽象方法asBinder(),定义新接口时,必须从IInterface派生它),同时它自己也还是个接口,所有可以在Binder中传输的接口都需要继承IInterface接口。

android.os.IInterface / public interface :Base class for Binder interfaces. When defining a new interface, you must derive it from IInterface.

这个类刚开始看起来逻辑混乱,但是实际上还是很清晰的通过它我们可以清楚地了解到Binder的工作机制:

这个类结构其实很简单,首先它++声明了两个方法getBookListaddBook,显然这是我们在IBookManager.aidl中所声明的方法++,同时它还声明了两个整型的id分别用于标识这两个方法,这两个id用于标识在transact(办理)过程中客户端所请求的到底是哪个方法。接着,它声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,而当两者位于不同进程时,方法调用需要走transcat过程,这个逻辑由Stub的内部代理类Proxy来完成

可以认识到,这个接口的核心实现就是它的内部类Stub和Stub的内部实现类Proxy,下面介绍这两个类的每个方法的含义:

  • DESCRIPTOR:Binder的唯一标识,一般用当前Binder的类名,如本例中的"com.example.learn_002_ipc.aidl.IBookManager"。

  • asInterface(android.os.IBinder obj)用于将服务端Binder对象转换成客户端所需的AIDL接口类型的对象,这种转换是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy对象**。

  • asBinder :用于返回当前Binder对象

  • onTranscat该方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需的参数(如果目标方法有返回值的话)。

The key IBinder API is transact() matched by Binder.onTransact(). These methods allow you to send a call to an IBinder object and receive a call coming in to a Binder object, respectively. This transaction API is synchronous, such that a call to transact() does not return until the target has returned from Binder.onTransact(); this is the expected behavior when calling an object that exists in the local process, and the underlying inter-process communication (IPC) mechanism ensures that these same semantics apply when going across processes.

IBinder API的关键是通过Binder.onTransact()进行transact()匹配。这些方法允许您将调用发送到IBinder对象,并分别接收进入Binder对象的调用。这个事务API是同步的,因此,直到目标从Binder.onTransact()返回后,对transact()的调用才会返回。这是调用本地进程中存在的对象时的预期行为,并且基础进程间通信(IPC)机制可确保在跨进程时应用这些相同的语义。

(IBinder) public abstract boolean transact (int code, Parcel data, Parcel reply, int flags) : Perform a generic operation with the object.

使用对象执行通用操作。

(Binder) protected boolean onTransact (int code, Parcel data, Parcel reply, int flags) : Default implementation is a stub that returns false. You will want to override this to do the appropriate unmarshalling of transactions.If you want to call this, call transact().

默认实现是一个返回false的存根。您需要重写此操作以执行相应的事务解组。如果你想调用这个,请调用transact()

  • Proxy#getBookList:该方法运行在客户端**,当客户端远程调用此方法时,它的内部实现是这样的:
    • 首先创建该方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply、返回值对象List(_result);
    • 然后把该方法的参数信息写入_data中(如果有参数的话);
    • 接着调用transcat方法(android.os.IBinder.transact(...))来发起RPC(远程过程调用)请求,同时当前线程挂起;
    • 然后服务端的onTranscat方法会被调用直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果;
    • 最后返回_reply中的数据。
  • Proxy#addBook:该方法运行在客户端,执行过程个getBookList一样,addBook没有返回值所以不需要从_reply中取出返回值。

说明一下:

  1. 当客户端发起远程请求时,由于++当前线程会被挂起++直至服务器进程返回数据,所以如果一个远程方法是很耗时的,那么不能在UI线程中发起此远程请求;
  2. 由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现。因为它已经运行在一个线程中了。

[图片上传失败...(image-7114d0-1525872465108)]
【Binder的工作机制图】

从上述分析过程来看,我们完全不可以提供AIDL文件即可实现Binder,之所以提供AIDL文件,是为了方便系统为我们生成代码。现在参考IBookManager.java这个类的代码写一个一模一样的类出来:

首先写一个Binder的服务端

//写一个Binder的服务端(write a binder's server)
    private final IBookManager.Stub mBinder = new IBookManager.Stub() {
        @Override
        public List<Book> getBookList() throws RemoteException {
            synchronized (mBookList) {
                return mBookList;
            }
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            synchronized (mBookList) {
                if (!mBookList.contains(book)) {
                    mBookList.add(book);
                }
            }
        }
    };

首先我们会实现一个创建了一个Stub对象并在内部实现IBookManager的接口方法,然后在Service的onBind中返回这个Stub对象。因此,从这一点来看,我们完全可以把Stub类提取出来直接作为一个独立的Binder类来实现,这样IBookManager中就只剩接口本身了,这种分离的方式可以让它的接口变得清晰点。

根据上面的思想,手动实现一个Binder可以通过以下步骤来完成:

  • 声明一个AIDL性质的可口,只需要继承IInterface接口即可,IInterface接口中有一个asBinder方法。这个接口的实现如下:
    public interface IBookManager extends IInterface {

        static final String DESCRIPTOR = "com.example.learn_002_ipc.manualbinder.IBookManager";

        static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

        public List<Book> getBookList() throws RemoteException;
        public void addBook(Book book) throws RemoteException;
    }
  • 实现Stub类和Stub类中的Proxy代理类,这段代码我们可以自己写,但是写出来后会发现和系统自动生成的代码是一样的,因此这个Stub类只需要参考系统生成的代码即可:
    package com.example.learn_002_ipc.manualbinder;
    import android.os.Binder;
    import android.os.IBinder;
    import android.os.IInterface;
    import android.os.RemoteException;
    import java.util.List;

    /**
     * Created by WaxBerry on 2018/3/22.
     */

    public class BookManagerImpl extends Binder implements IBookManager {

        /** Construct the stub at attach it to the interface. */
        public BookManagerImpl() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into IBookManager interface,
         * generating a proxy if needed.
         */
        public static IBookManager asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin!=null)&&(iin instanceof IBookManager))) {
                return ((IBookManager)iin);
            }
            return new BookManagerImpl.Proxy(obj);
        }

        @Override
        public IBinder asBinder() {
            return this;
        }

        @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_getBookList: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<Book> _result = this.getBookList();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addBook: {
                    data.enforceInterface(DESCRIPTOR);
                    Book _arg0;
                    if ((0!=data.readInt())) {
                        _arg0 = Book.CREATOR.createFromParcel(data);
                    }
                    else {
                        _arg0 = null;
                    }
                    this.addBook(_arg0);
                    reply.writeNoException();
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        @Override
        public List<Book> getBookList() throws RemoteException {
            //TODO 待实现
            return null;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            //TODO 待实现
        }

        private static class Proxy implements IBookManager {
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            @Override public android.os.IBinder asBinder() {
                return mRemote;
            }
            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }
            @Override public java.util.List<Book> getBookList() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<Book> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(TRANSACTION_getBookList, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(Book.CREATOR);
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
            @Override public void addBook(Book book) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if ((book!=null)) {
                        _data.writeInt(1);
                        book.writeToParcel(_data, 0);
                    }
                    else {
                        _data.writeInt(0);
                    }
                    mRemote.transact(TRANSACTION_addBook, _data, _reply, 0);
                    _reply.readException();
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
        }
    }

实际开发中完全可以通过AIDL文件让系统自动生成,手动去写的意义在于可以让我们更加理解Binder的工作原理,同时也提供了一种不通过AIDL文件来实现Binder的新方式。也就是说,AIDL并不是实现Binder的必须品。如果使我们手写的Binder,那么服务端只需要创建一个BookManagerImpl的对象并在Service的onBindd方法中返回即可。最后,是否手动实现Binder没有本质区别,二者的工作原理完全一样,AIDL文件的本质是系统为我们提供了一种快速实现Binder的工具仅此而已

接下来是Binder的两个很重要的方法:linkToDeathunlinkToDeath。Binder运行在服务端进程,如果服务端进程由于某种原因被异常终止,这个时候我们到服务端的Binder连接断裂(称之为Binder死亡),会导致我们的远程调用失败。更为关键的是,如果我们不知道Binder连接已经断裂,那么客户端的功能就会收到影响。为了解决这个问题,Binder中提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath我们可以为Binder设置一个死亡代理,当Binder死亡时,我们会收到通知,这个时候我们就可以重新发起连接请求从而恢复连接

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

//给Binder设置死亡代理(set a Death Recipient for a binder)
    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设置死亡代理:

mService = IMessageBoxManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient, 0);

其中linkToDeath的第二个参数为是个标记位,我们直接设为0即可。经过上面两个步骤,就给我们的Binder设置了死亡代理,当Binder死亡的时候既可以收到通知了。另外,通过binder的方法isBinderAlive也可以判断Binder是否死亡。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269