《Android 开发艺术探索》读书笔记(二)——IPC 机制

Android 多进程开发我在平时开发中还没有遇到,但不代表不重要,仍需要了解一下基本概念,Android 的序列化机制和 Binder 是。

1 Android IPC 简介

IPC 是 Inter Process Communication 的缩写,意为进程间通信或跨进程通信,是指两个进程之间进行数据交换的过程。进程一般指一个程序或一个应用,一个进程可以包含多个线程,线程是 CPU 调度的最小单元,最简单的情况是一个进程(即一个应用)只包含一个线程,这个线程被称为主线程,在 Android 中也被称为 UI 线程,所有界面元素的相关操作必须在主线程中,如果在主线程中执行大量耗时任务,就会造成界面无法响应,严重影响用户体验,这种情况在 PC 上和移动端都有,在 Android 中这种情况被称为 ANR(Application Not Responding),即应用无响应,为了避免这种问题我们就需要把耗时任务放到子线程中去执行。

IPC 不是 Android 特有的,任何一个操作系统都有相应的 IPC 机制,Windows 上可以通过剪贴板、管道等来实现进程间通信,Linux 可以通过命名管道、共享内容、信号量来实现进程间通信,Android 是一种基于 Linux 内核的移动操作系统,它的通信方式并不能完全继承自 Linux,所以它有自己的 IPC 机制,最有特色的就是 Binder 了,通过 Binder 可以轻松地实现进程间通信,除此之外 Android 还支持 Socket,Socket 都可以实现任意两个终端之间的通信,更别说同一终端的两个进程之间通信了。

说到 IPC 就要提到多进程了,因为只有在多进程的场景下才需要考虑进程间通信,毕竟“间”是对于至少两个以上的主语来说的。多进程的情况一般分为两种,一种情况是一个应用因为某些原因自身需要采用多进程模式实现,如有些模块由于特殊原因需要运行在单独的进程中,或者为了加大一个应用可使用的内存所以需要通过多进程来获取多份内存空间(Android 对单个应用的最大内存做了限制,早期一些版本一般为 16MB)。另一种情况是当前应用需要向其他应用获取数据,由于是两个应用,所以必须采用跨进程的方式来获取需要的数据,当我们通过系统提供的 ContentProvider 去查询数据时其实也是一种进程间通信,只是通信细节被系统屏蔽了。

如果采用了多进程的设计,那么应用内就必须妥善处理进程间通信带来的各种问题。

2 Android 中的多进程模式

2.1 开启多进程模式

多个应用自然是多进程,正常情况下,Android 中的多进程指的是一个应用中存在多个进程的情况。在 Android 中使用多进程的方法就是在 AndroidManifest.xml 中给四大组件指定 android:process 属性,所以我们无法给一个线程或者实体类指定其运行时的进程。除此之外还有一种方法开启多进程,就是通过 JNI 在 native 层 fork 一个新的进程,这属于特殊情况。

下面演示开启多进程:

       
            
                

                
            
        
        
        

 

定义了三个 Activity,在 MainActivity 中启动 SecondActivity,在 SecondActivity 中启动 ThirdActivity,使用 adb shell "ps|grep com.qinshou.demo1" 查看当前工程的进程:

 

可以看到 MainActivity 没有指定进程名,所以它运行在默认进程中,默认进程名为包名,这三个进程的进程 id 分别为 8668、8698、8730,进程名也不同,说明我们成功开启了多进程。

SecondActivity 和 ThirdActivity 中指定的 process 的方式是不一样的,SecondActivity 为 ":remote",这是一种简写,它会在前面加上包名,所以 SecondActivity 的完整进程名为 "com.qinshou.demo1:remote"。ThirdActivity 为 "com.qinshou.demo1.remote",这就是完整的命名方式,不会在前面附加包名了。

使用 ":" 开头简写指定进程名的进程属于当前应用的私有进程,其他应用的组件不可以和它在同一个进程中。而使用完整命名的进程属于全局进程,其他应用可以通过 shareUID 的方式和它运行在同一进程。

两个应用若想通过 shareUID 运行在同一进程也是有要求的,需要两个应用有相同的 ShareUID 并且签名相同。这样一来它们之间可以互相访问对方的私有数据,如 data 目录、组件信息等,如果在同一进程中甚至还可以共享内存数据。

 

2.2 多进程模式的运行机制

开启多进程很简单,只需要给四大组件指定 android:process 属性即可,但是其实要处理的问题很多,一般来说,使用多进程会造成如下问题:

1)静态成员和单例模式失效;

2)线程同步机制完全失效;

3)SharedPreferences 的可靠性下降;

4)Application 会多次创建。

虽然多进程可能导致的问题很多,但是仍然要正视它,为了解决这些问题系统提供很多跨进程通信方法,虽然不能直接共享内存,但是可以跨进程传递数据,所以理解各种 IPC 方式是很重要的。

 

3 IPC 基础概念介绍

IPC 主要包含三方面内容,Serializable 接口、Parcelable 接口及 Binder,Serializable 接口和 Parcelable 接口可以完成对象的序列化过程,当我们需要通过 Intent 或 Binder 传递数据时、需要把对象持久化到设备上或者通过网络传输给其他客户端时就需要使用 Parcelable 或者 Serializable。

 

3.1 Serializable 接口

Serializable 是 Java 提供的一个序列号接口,它跟 Cloneable 一样只是一个标识接口,并没有需要实现的方法,所以使用 Serializable 来实现对象的序列化非常简单,只需要在类中声明一个标识即可自动实现默认的序列化操作,看看示例代码:

public class Person implements Serializable {
    public static final long serialVersionUID = 10000L;
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

 

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Person person1 = new Person("张三", 23);
        Log.i("MainActivity", "person1.hashCode()--->" + person1.hashCode());
        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(
                    new FileOutputStream(
                            new File(MainActivity.this.getCacheDir() + "/Person.txt")
                    )
            );
            objectOutputStream.writeObject(person1);
            objectOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(
                    new FileInputStream(
                            new File(MainActivity.this.getCacheDir() + "/Person.txt")
                    )
            );
            Person person2 = (Person) objectInputStream.readObject();
            Log.i("MainActivity", person2.toString());
            Log.i("MainActivity", "person2.hashCode()--->" + person2.hashCode());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

 

 

上面 Person 类实现了 Serializable 接口,并指定了一个 serialVersionUID 常量作为序列化的一个标识,然后使用 ObjectOutputStream 和 ObjectInputStream 即可存储和恢复 Person 对象,恢复后的内容一样,但是从 hashCode 可以看出它们俩并不是同一个对象。

事实上 serialVersionUID 并不是必须的,不声明它同样可以实现序列化,但这可能对反序列化过程产生影响,系统既然提供了这个 serialVersionUID 那么它必然是有用的,它是用来辅助序列化和反序列化过程的,只有序列化后的数据中的 serialVersionUID 与当前类中的 serialVersionUID 相同才能被正常反序列化。

序列化的时候系统会将 serialVersionUID 写入序列化的文件(或者其他中介)中,当反序列化的时候系统会去检测文件中的 serialVersionUID,看它是否和目标类的 serialVersionUID 一致,如果一致就说明序列化的类的版本与当前目标类的版本是相同的,才能成功序列化,否则就说明序列化的类和当前目标类之间发生了某些变换,如成员变量的个数、类型发生了改变,这个时候就无法正常序列化了。

一般来说我们应该手动指定 serialVersionUID 的值,如果不手动指定,它会根据当前类的结构自动生成 hash 值并赋值给 serialVersionUID。当类结构发生改变时,hash 值肯定会发生变化,所以导致反序列化失败,导致程序崩溃,所以手动指定 serialVersionUID 就能很大程度上避免反序列化过程的失败,即使类发生了一些变化,如增减成员变量,它仍然能够最大限度的恢复序列化的数据。但是如果类结构发生了非常规改变,如修改了类名、修改了成员变量的类型,尽管 serialVersionUID 验证通过了,但是反序列化仍然会失败。

需要注意的是静态成员变量属于类不属于对象所以不会参与序列化过程,还有使用 transient 关键字修饰的成员变量也不会参与反序列化过程。

序列化和反序列化的过程也是可以修改的,只需要重写两个方法即可:

    private void writeObject(java.io.ObjectOutputStream objectOutputStream) throws IOException {
        objectOutputStream.defaultWriteObject();
    }

    private void readObject(java.io.ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
        objectInputStream.defaultReadObject();
        age = 25;
    }

 

objectOutputStream.defaultWriteObject() 和 objectInputStream.defaultReadObject() 表示调用默认的序列化和反序列化过程,如果这两句都不写则序列化和反序列化的都是空对象了,在反序列化的时候又把 age 给修改了一下,设置为 25 了,打印如下:

 

可以看到自定义的修改起了作用,序列化的 age 是 23,反序列化出来变成 25 了。

 

3.2 Parcelable 接口

Parcelable 同 Serializable 一样可以实现序列化并都可以用于 Intent 之间传递数据,Serializable 是 Java 的序列化接口,而 Parcelable 是 Android 特有的序列化方式,它比起 Serializable 效果要高很多,两者性能相比 Parcelable 要高约 10 倍,但是使用也比较麻烦一点。Parcelable 主要用于内存序列化上,如果要将存储到设备中或者网络传输则过程会更复杂,所以这两种情况还是推荐使用 Serializable 接口。

使用 Parcelable 接口实现序列化时类的写法如下:

public class Person implements Parcelable {
    private String name;
    private int age;
    private int sex;    // 0 表示男,1 表示女

    public Person() {
    }

    public Person(String name, int age, int sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(name);
        dest.writeInt(age);
        dest.writeInt(sex);
    }

    //成员内部类名必须为 CREATOR
    public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {

        @Override
        public Person createFromParcel(Parcel source) {
            Person person = new Person();
            person.name = source.readString();
            person.age = source.readInt();
            person.sex = source.readInt();
            return person;
        }

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


    };

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                '}';
    }
}

 

实现 Parcelable 接口后需要重写 describeContents() 方法,该方法表示内容描述,通常都是返回 0;还需要重写 writeToParcel(Parcel dest, int flags) 方法,该方法表示序列化过程,内部由一系列 wirte 方法完成;还需要定义一个 Parcelable.Creator 成员内部类,类名必须为 CREATOR,它的内部实现反序列化过程,由一系列 read 方法完成。

Android 中有很多类已经实现了 Parcelable 接口,这表明它们都是可序列化的,还有 List 和 Map 也是可以序列化的,前提是它们中的元素也必须是可序列化的。

 

3.3 Binder

Binder 刚开始看着比较复杂就跳过了,在后面看完 AIDL 后又回过头来看它的,所以这里的记录会和后面的有些重复。

Binder 是一个很深入的话题,它的底层非常复杂。书中主要介绍了 Binder 的使用和上层原理,Binder 其实就是 Android 的一个类,它实现了 IBinder 接口。从 IPC 的角度来说,Binder 是一种跨进程通信方式。Binder 也可以理解为一种虚拟物理设备,它的设备驱动是 /dev/binder,该通信方式是 Android 特有的,在 Linux 上没有。从 Framework 角度来说,Binder 是 ServiceManager 连接各种 Manager(ActivityManager、WindowManager 等)相应 ManagerService 的桥梁。从 Android 应用层来说,Binder 是客户端和服务端通信的媒介,当调用 bindService() 方法时,服务端会返回一个包含了服务端业务调用的 Binder 对象,通过这个 Binder 对象,客户端就可以获取服务端的数据或者服务端提供的服务(包括普通服务和 AIDL 服务)。

Binder 主要用在 Service 中,包括 AIDL 和 Messenger,普通服务不涉及进程间通信所以较为简单。Messenger 底层实现其实就是 AIDL,所以使用 AIDL 来分析 Binder 的工作机制。

新建一个 Book 类:

public class Book implements Parcelable {
    private int id;
    private String bookName;

    public Book() {
    }

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

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", bookName=" + bookName +
                '}';
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(bookName);
    }

    public static final Parcelable.Creator CREATOR = new Creator() {
        @Override
        public Book createFromParcel(Parcel source) {
            Book book = new Book();
            book.id = source.readInt();
            book.bookName = source.readString();
            return book;
        }

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

 

然后新建 Book.aidl:

package com.qinshou.ipc;

parcelable Book;

 

IBookManager.adil:

package com.qinshou.ipc;

import com.qinshou.ipc.Book;

interface IBookManager{
    List getBookList();

    void addBook(in Book book);
}

 

它们的结构应该是这样的:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第1张图片

 

Book 是一个表示图书信息的类,它实现了 Parcelable 接口,Book.aidl 是表示要在 AIDL 中使用 Book 类的声明,只要 AIDL 中要使用到一个 Java 类都要创建一个与之对应的 aidl 声明文件。IBookManager.aidl 是我们定义的一个接口,里面有两个方法 getBookList() 和 addBook(),在 IBookManager.aidl 中可以使用到的实现了 Parcelable 接口的类(这里是 Book 类)必须显式 import 进来,即使它们和当前的 AIDL 文件处于同一个包内。然后系统就会根据 IBookManager.aidl 自动生成 Binder 类。在 Android Studio 中是在 app-->build-->generated-->source-->aidl-->debug-->包名下,如果没有的话可以 Make Project 一下。

IBookManager.java(自动生成的):

public interface IBookManager extends android.os.IInterface {
    /**
     * Local-side IPC implementation stub class.
     */
    public static abstract class Stub extends android.os.Binder implements com.qinshou.ipc.IBookManager {
        private static final java.lang.String DESCRIPTOR = "com.qinshou.ipc.IBookManager";

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

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

        @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 {
            java.lang.String descriptor = DESCRIPTOR;
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_getBookList: {
                    data.enforceInterface(descriptor);
                    java.util.List _result = this.getBookList();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
                case TRANSACTION_addBook: {
                    data.enforceInterface(descriptor);
                    com.qinshou.ipc.Book _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.qinshou.ipc.Book.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.addBook(_arg0);
                    reply.writeNoException();
                    return true;
                }
                default: {
                    return super.onTransact(code, data, reply, flags);
                }
            }
        }

        private static class Proxy implements com.qinshou.ipc.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 getBookList() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.qinshou.ipc.Book.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

            @Override
            public void addBook(com.qinshou.ipc.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 getBookList() throws android.os.RemoteException;

    public void addBook(com.qinshou.ipc.Book book) throws android.os.RemoteException;
}

 

该类是一个接口,继承自 IInterface,所有可以在 Binder 中传输的接口都需要继承自 IInterface。可以看到在末尾有我们在 aidl 文件中定义的两个方法,其他的东西虽然命名看起来很乱,但是其实逻辑很清晰,在末尾有两个静态常量标识我们定义的方法,表示 id,用于在 transact 过程中确定客户端请求的到底是哪个方法。然后有一个内部类 Stub,它其实就是一个 Binder 类,当客户与服务端位于同一进程时,它们之间方法的调用不会走跨进程的 transact 过程,如果客户端与服务端位于不同进程,方法的调用则需要走 transact 过程,这个逻辑由 Stub 的内部代理类 Proxy 完成。所以这个接口的核心实现就是内部类 Stub 和 Stub 的内部代理类 Proxy。下面了解一下该接口中各个方法和变量的含义:

DESCRIPTOR

Binder 的唯一标识,一般用当前 Binder 的类名表示。

 

asInterface(android.os.IBinder obj)

用于将服务端的 Binder 对象转换成客户端所需要的 AIDL 接口类型的对象,该转换过程是区分单进程和多进程的,如果客户端与服务端位于同一进程,则返回服务端的 Stub 对象本身,如果客户端与服务端位于不同进程则返回系统封装后的 Stub.Proxy 对象。

 

asBinder()

返回当前 Binder 对象。

 

onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)

该方法运行在服务端中的 Binder 线程池中,当客户端每次发起跨进程请求时,远程请求会通过系统底层封装后交给此方法处理,通过 code 来确定客户端请求的目标方法,然后从 data 中取出目标方法所需要的参数(如果目标方法需要参数的话),然后执行目标方法。当目标方法执行完成后就向 reply 中写入返回值(如果目标方法有返回值的话)。如果该方法返回 false,那么客户端的请求就会失败,利用这一特性可以来做权限验证,毕竟我们也不想随便一个进程都能调用我们的方法。

 

Proxy#getBookList()

该方法运行在客户端,当客户端远程调用该方法时,会首先创建该方法所需要的输入型 Parcel 对象 _data、输出型 Parcel 对象 _reply 和返回值对象 List,然后把参数信息写入 _data 中(该方法没有,下面的 addBook() 方法就有 Book 参数),接着调用 transact 方法来完成 RPC(远程过程调用)请求,同时将当前线程挂起,然后服务端的 onTransact() 方法会被调用,直到 RPC 过程结束返回后,才继续执行当前线程,并从 _reply 中取出 RPC 过程的返回结果,最后返回 _reply 中的数据(如果有返回值的话,该方法是有的,下面的 addBook() 方法就没有)。

 

Proxy#addBook(com.qinshou.ipc.Book book)

原理同 Proxy#getBookList() 一样,只是该方法有参数,没有返回值而已。

通过分析这些方法,可以说已经很清晰的了解了 Binder 的工作机制,再来一张图说明一下:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第2张图片

 

需要注意的两点:

1)因为客户端发起远程请求时,当前线程会被挂起直到服务端进程返回数据,所以如果一个远程方法是耗时方法,那么客户端发起该远程请求时就不能在 UI 线程中。

2)服务端的 Binder 方法会运行在 Binder 线程池中,所以 Binder 方法无论是否耗时都应该采用同步的方式实现。

通过分析 Binder 的工作机制后,我们其实可以不用写 AIDL 文件就可以自己手写 Binder,所以说 AIDL 文件其实就是用来帮助我们生成 Binder 类的,这里我就没有去研究手写 Binder 的过程了,这只是为了让我们更加理解 Binder 而已,一般情况下是不会去手写 Binder 的,

Binder 还有两个重要方法 linkToDeath() 和 unlinkToDeath()。Binder 是运行在服务端进程中的,如果服务端进程异常终止,这时客户端与服务端的 Binder 连接就会断裂,也称为 Binder 死亡,这时候再去调用服务端的远程方法就会失败,如果客户端不知道 Binder 死亡的话那么客户端的功能就会受到影响。所以 Binder 提供了两个配对的方法 linkToDeath() 和 unlinkToDeath(),用来设置和解绑 Binder 的死亡监听。如果客户端通过 linkToDeath() 方法为 Binder 设置了死亡代理,那么在 Binder 死亡时客户端就会收到通知。

具体操作如下:

定义一个 DeathRecipient 对象:

    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        /**
         * Create By:禽兽先生
         * Create On:2019/1/5 0:46
         * Description: 当 Binder 死亡时就会回调 binderDied() 方法,我们就可以在该方法中解绑
         * 之前的 Binder 死亡代理并重新绑定远程服务
         */
        @Override
        public void binderDied() {
            if (mBookManager == null) {
                return;
            }
            //取消死亡监听
            mBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
            //重置 IBookManager
            mBookManager = null;
            Log.i("Demo1", "binderDied,Thread--->" + Thread.currentThread().getName());
            //TODO 在下面重新绑定远程服务
        }
    };

 

然后在客户端绑定远程服务成功时设置死亡代理即可:

            //将服务端返回的 Binder 对象转换成 AIDL 接口所属的类型
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                //设置死亡监听
                service.linkToDeath(mDeathRecipient, 0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }

linkToDeath() 方法的第二个参数是个标记为,一般设为 0 即可。除了设置死亡代理,我们也可以调用 Binder 的 isBinderAlive() 方法主动判断 Binder 是否死亡。

Binder 看完后,IPC 的基础知识就完了,感觉 Binder 看第一眼感觉很复杂,但是其实搞清楚工作原理后逻辑还是很清晰的,毕竟代码量不多,只是变量命名方式看着不那么习惯。

 

4 Android 中的 IPC 方式

跨进程通信方式有很多,如在 Intent 中附加 extras 来传递信息,或者通过共享文件的方式来共享数据,或者使用 Binder 方式来跨进程通信,或者使用 ContentProvider(它本身就支持跨进程访问),或者通过网络通信也是可以实现数据传递的。实现 IPC 的方式很多,当时它们在使用方法和侧重点上都有区别。

 

4.1 使用 Bundle

四大组件中的三大组件(Activity、Service、BroadcastReceiver)都支持在 Intent 中传递 Bundle 数据,由于 Bundle 实现了 Parcelable 接口,所以它可以很方便的在不同进程间传输。所以当我们在一个进程中启动另一个进程的 Activity、Service、Receiver 时就可以在 Bundle 中附加需要传输的数据。当然这些数据必须是它支持的,如基本类型、实现了 Parcelable 接口的对象、实现了 Serializable 接口的对象等。

使用 Bundle 是最简单的一种进程间通信方式,下面是 Demo 演示:

Demo1,:

package com.qinshou.demo1;

import android.content.ComponentName;
import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.qinshou.demo2", "com.qinshou.demo2.MainActivity"));
        intent.putExtra("name", "张三");
        intent.putExtra("age", 23);
        startActivity(intent);
    }
}

 

Demo2:

package com.qinshou.demo2;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = getIntent();
        Log.i("Demo2MainActivity", "name--->" + intent.getStringExtra("name"));
        Log.i("Demo2MainActivity", "age--->" + intent.getIntExtra("age", 0));
    }
}

 

这里有两个工程,一个叫 Demo1,一个叫 Demo2,在 Demo1 的 MainActivity 中启动了 Demo2 的 MainActivity 并传输了一些数据给 Demo2 的 MainActivity,运行后打印如下:

 

可以看到 Demo2 中成功接收到了 Demo1 传递的数据。

 

4.2 使用文件共享

共享文件也是一种不错的进程间通信方式,两个进程通过读写同一个文件来交换数据,如 Demo1 进程将数据写到一个文件中,Demo2 进程来读取该文件获取数据,Android 对于并发读写文件没有限制,甚至多个进程同时写同一个文件都是可能的,所以这有可能出现并发问题,但是通过文件交换数据仍然是一种方式,使用很简单,除了交换一些文本信息之外也交换序列化对象的数据,下面是 Demo 演示:

Demo1 中的 Person 类:

package com.qinshou.ipc;

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

import java.io.Serializable;

/**
 * Create By:禽兽先生
 * Create On:2018-12-31 23:50
 * Description:
 */
public class Person implements Serializable {
    private static final long serialVersionUID = 10000L;
    private String name;
    private int age;
    private int sex;    // 0 表示男,1 表示女

    public Person() {
    }

    public Person(String name, int age, int sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                '}';
    }
}

 

Person 类还是那个 Person 类,实现了 Serializable 接口,但是请注意它的包名,这样 Demo1 中的 Person 类的全限定名为 com.qinshou.ipc.Person。

Demo1 中的 MainActivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        PermissionUtil.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, new OnRequestPermissionResultCallBack() {
            @Override
            public void onSuccess() {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        ObjectOutputStream objectOutputStream = null;
                        try {
                            Person person = new Person("张三", 23, 0);
                            File cacheDir = new File(Environment.getExternalStorageDirectory() + "/Cache");
                            if (!cacheDir.exists()) {
                                cacheDir.mkdir();
                            }
                            File file = new File(cacheDir + "/cache.txt");
                            objectOutputStream = new ObjectOutputStream(new FileOutputStream(file));
                            objectOutputStream.writeObject(person);
                            Log.i("Demo1MainActivity", "person--->"+person.toString());
                            Log.i("Demo1MainActivity", "hashcode--->"+person.hashCode());
                        } catch (IOException e) {
                            e.printStackTrace();
                        } finally {
                            try {
                                if (objectOutputStream != null) {
                                    objectOutputStream.close();
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }).start();
            }

            @Override
            public void onError(List deniedPermissionList) {

            }
        });
    }
}

 

MainActivity 中创建了一个 Person 对象,并存储到 SD 卡下的 Cache 目录,文件名为 cache.txt。

Demo2 中的 Person 类:

package com.qinshou.ipc;

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

import java.io.Serializable;

/**
 * Create By:禽兽先生
 * Create On:2018-12-31 23:50
 * Description:
 */
public class Person implements Serializable {
    private static final long serialVersionUID = 10000L;
    private String name;
    private int age;
    private int sex;    // 0 表示男,1 表示女

    public Person() {
    }

    public Person(String name, int age, int sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", sex=" + sex +
                '}';
    }
}

 

注意它的包名与 Demo1 中的包名一样,所以全限定名也是 com.qinshou.ipc.Person。

Demo2 中的 MainActivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        PermissionUtil.requestPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE, new OnRequestPermissionResultCallBack() {
            @Override
            public void onSuccess() {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        ObjectInputStream objectInputStream = null;
                        try {
                            File cacheDir = new File(Environment.getExternalStorageDirectory() + "/Cache");
                            if (!cacheDir.exists()) {
                                cacheDir.mkdir();
                            }
                            File file = new File(cacheDir + "/cache.txt");
                            objectInputStream = new ObjectInputStream(new FileInputStream(file));
                            Person person = (Person) objectInputStream.readObject();
                            Log.i("Demo2MainActivity", "person--->" + person.toString());
                            Log.i("Demo2MainActivity", "hashcode--->" + person.hashCode());
                        } catch (IOException e) {
                            e.printStackTrace();
                        } catch (ClassNotFoundException e) {
                            e.printStackTrace();
                        } finally {
                            try {
                                if (objectInputStream != null) {
                                    objectInputStream.close();
                                }
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }).start();
            }

            @Override
            public void onError(List deniedPermissionList) {

            }
        });
    }
}

 

MainActivity 中从 SD 卡下的 Cache 目录读取 cache.txt 文件中的数据,并转换成 Person 对象。

先运行 Demo1,然后运行 Demo2,运行打印如下:

 

可以看到在 Demo2 中成功恢复了数据,但是 hashCode 并不一样,说明这两个并不是同一个对象。通过文件共享这种方式来共享数据对文件格式是没有要求的,可以是 txt 文本文件,也可以是 xml 文件,只要读写双方约定好数据格式即可,需要注意的是,如果是存储自定义的对象的时候,两个进程的对象的全限定名必须一样,不然会导致强制转换错误。

使用文件共享也是有局限性的,比如并发性问题,如果并发读,那么我们可能读取出来的数据不是最新的,如果是并发写就严重了,会导致存储数据异常,所以我们要尽量避免并发读写的情况,或者考虑线程同步来限制多线程的写操作。所以文件共享这种进程间通信方式适用于对数据同步要求不高的进程间通信。

 

4.3 使用 Messenger

Messenger 可以理解为信使,通过它可以在不同进程中传递 Message 对象,因为 Message 中可以携带 Bundle,所以可以借助 Message 可以实现进程间的数据传递。

Messenger 是一种轻量级的 IPC 方案,它的底层是由 AIDL 实现。Message 的使用方法很简单,它对 AIDL 进行了封装,而且它一次只处理一个请求,所以在服务端不用考虑线程同步的问题,实现 Messenger 的基本实现步骤主要步骤如下:

1)服务端进程

在服务端创建一个 Service 来处理客户端的连接请求,同时创建一个 Handler 并通过它来创建一个 Messenger 对象,然后在 Service 的 onBind() 方法中返回这个 Messenger 的对象的 Binder 即可。

2)客户端进程

在客户端绑定服务端的 Service,绑定成功后用服务端返回的 IBinder 对象创建一个 Messenger,通过这个 Messenger 就可以向服务端发送消息了,发送的消息为 Message 对象。如果需要服务端回应客户端,就需要像服务端一样创建一个 Handler 接收回传消息,并通过这个 Handler 创建 Messenger 对象,然后将这个 Messenger 对象通过 Message 的 replyTo 参数传递给服务端,服务端就可以通过这个 replyTo 参数的 Messenger 发送回传消息给客户端。

用代码来说明吧,下面是 Demo 演示:

Demo2 为服务端,创建一个 MessengerService:

public class MessengerService extends Service {
    //处理客户端发送的消息
    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 0:
                    //获取客户端发送的消息
                    String message = msg.getData().getString("msg");
                    Log.i("Demo2", "message--->" + message);
                    //获取客户端接受回传消息的 Messenger
                    Messenger messenger = msg.replyTo;
                    //创建回传消息
                    Message replyMessage = Message.obtain(null, 1);
                    Bundle bundle = new Bundle();
                    bundle.putString("msg", "你好张三,我是李四,请问有什么可以帮你的。");
                    replyMessage.setData(bundle);
                    try {
                        //发送回传消息
                        messenger.send(replyMessage);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
                default:
                    super.handleMessage(msg);
                    break;
            }
        }
    }

    //创建一个与 MessengerHandler 的 Messenger
    private Messenger mMessenger = new Messenger(new MessengerHandler());

    @Override
    public IBinder onBind(Intent intent) {
        //返回 Messenger 中的 Binder
        return mMessenger.getBinder();
    }
}

 

在 AndroidManifest.xml 中注册该服务并支持其他进程调用:

        

 

Demo1 为客户端:

public class MainActivity extends AppCompatActivity {
    //处理服务端回传的消息
    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    //获取服务端回传的消息
                    String message = msg.getData().getString("msg");
                    Log.i("Demo1", "message--->" + message);
                default:
                    super.handleMessage(msg);
                    break;
            }
        }
    }

    //创建一个与 MessengerHandler 的 Messenger
    private Messenger mMessenger = new Messenger(new MessengerHandler());

    //与服务端 Service 连接的 Connection
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.i("Demo1", "onServiceConnected");
            //连接成功之后根据服务端返回的 binder 对象创建 Messenger 对象
            Messenger messenger = new Messenger(service);
            //创建发送消息
            Message message = Message.obtain();
            message.what = 0;
            Bundle bundle = new Bundle();
            bundle.putString("msg", "你好,我是张三,收到请回答。");
            message.setData(bundle);
            //设置发送消息中接收回传消息的 Messenger
            message.replyTo = mMessenger;
            try {
                //发送消息
                messenger.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.i("Demo1", "onServiceDisconnected");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.qinshou.demo2", "com.qinshou.demo2.MessengerService"));
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mServiceConnection != null) {
            unbindService(mServiceConnection);
        }
    }
}

 

先运行 Demo2 然后再运行 Demo1,打印结果如下:

 

Messenger 跨进程通信的工作原理图如下:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第3张图片

 

4.4 使用 AIDL

上面说了 Messenger 是对 AIDL 的一种封装,那么为什么还要学习使用 AIDL 进行跨进程通信呢?这是因为 Messenger 是串行的,如果有大量的消息同时发送到服务端,则服务端仍只能一个个的处理,这时候用 Messenger 就不太合适了,而且 Messenger 的主要作用是传递消息,如果需要跨进程调用服务端的方法,这时候 Messenger 也是无法做到的,所以就需要 AIDL 了。它的使用也是主要分为服务端和客户端。

1)服务端

服务端首先创建一个 AIDL 文件,将需要暴露给客户端的接口在这个 AIDL 文件中声明,然后需要创建一个 Service 用来监听客户端的连接请求,最后在 Service 中实现这个 AIDL 即可。

2)客户端

客户端需要绑定服务端的 Service,绑定成功后将服务端返回的 Binder 对象转成 AIDL 接口所属的类型,然后就可以调用 AIDL 中的方法了。

文字仍然很抽象,上代码:

仍然是以 Demo2 作为服务端,首先声明一个可序列化的 Book 对象:

public class Book implements Parcelable {
    private int id;
    private String bookName;

    public Book() {
    }

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

    @Override
    public String toString() {
        return "Book{" +
                "id=" + id +
                ", bookName=" + bookName +
                '}';
    }

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(bookName);
    }

    public static final Parcelable.Creator CREATOR = new Creator() {
        @Override
        public Book createFromParcel(Parcel source) {
            Book book = new Book();
            book.id = source.readInt();
            book.bookName = source.readString();
            return book;
        }

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

 

创建后缀为 AIDL 文件 IBookManager.aidl,在里面声明一个接口和两个接口方法:

package com.qinshou.ipc;

import com.qinshou.ipc.Book;

interface IBookManager{
    List getBookList();

    void addBook(in Book book);
}

 

由于 IBookManager 中使用到了 Book 对象,所以需要创建一个 Book.aidl 表明它是 Parcelable 类型:

package com.qinshou.ipc;

parcelable Book;

 

创建一个 BookManagerService 用于给客户端连接,并在其中实现 IBookManager 这个接口:

public class BookManagerService extends Service {
    private CopyOnWriteArrayList mBookList = new CopyOnWriteArrayList<>();

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public List getBookList() throws RemoteException {
            Log.i("Demo2", "有客户端调用 getBookList() 方法,mBookList.size()--->" + mBookList.size());
            Log.i("Demo2","mBookList--->" + mBookList.toString());
            return mBookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            Log.i("Demo2", "有客户端调用 addBook() 方法,book--->" + book);
            mBookList.add(book);
        }
    };

    public BookManagerService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("Demo2", "mBookList--->" + mBookList.getClass().getCanonicalName());
        mBookList.add(new Book(1, "Android"));
        mBookList.add(new Book(2, "Ios"));
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

 

如果不能导入 IBookManager 的话可以在写完 AIDL 后可以 Make Project 一下,最后在 AndroidManifest.xml 中注册该服务并支持其他进程调用:

        

 

在 AIDL 文件中并不是所有的数据类型都可以使用,它支持的数据类型如下:

1)基本数据类型:byte、short、int、long、float、double、boolean、char;

2)String 和 CharSequence;

3)List:只支持 ArrayList,而且里面的每个元素都必须是 AIDL 支持的类型;

4)Map:只支持 HashMap,而且里面的 key 和 value 都必须是 AIDL 支持的类型;

5)Parcelable:所有实现了 Parcelable 接口的对象;

6)AIDL:所有的 AIDL 接口本身也可以在 AIDL 文件中使用;

需要注意的几点:

1)自定义的实现了 Parcelable 接口的对象和 AIDL 对象必须显式 import 进来,即使它们和当前的 AIDL 文件处于同一个包内。

2)如果 AIDL 文件中用到了自定义的实现了 Parcelable 接口的对象,则必须新建一个和它同名的 AIDL 文件,在该文件中声明它为 Parcelable 类型,如上面的 Book.aidl。

3)AIDL 中除了基本数据类型,其他类型的参数都必须标上方向:in(数据只能由客户端流向服务端)、out(数据只能由服务端流向客户端)或者 inout(数据可在服务端与客户端之间双向流通),这要根据实际需要去选择,不能一概使用 inout,因为这在底层实现是有开销的。

4)AIDL 接口中只支持方法,不支持声明静态常量。

服务端和客户端的包结构必须保持一致,不然会导致反序列化失败。为了方便 AIDL 开发,建议把所有与 AIDL 相关的类和文件全部放入一个包中,这样当需要接入一个新的客户端时只需要将这个包复制到客户端工程下即可,但是其实 Java 类和 AIDL 接口等不能在同一个包下,所以复制到客户端时其实需要复制两个包,当然,也有办法可以让它们在一个包内,修改 gradld 中的 sourceSets 标签即可,具体操作可百度。

Demo2 包结构如下:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第4张图片

到时候只需要将 aidl 和 java 中的 ipc 复制到客户端即可,到时候 Demo1 的包结构如下:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第5张图片

 

接下来实现客户端 Demo1,客户端比较简单,只需要在 MainActivity 中绑定服务端的 Service,然后将服务端返回的 Binder 对象转成 AIDL 接口所属的类型就可以调用 AIDL 中的方法了:

public class MainActivity extends AppCompatActivity {

    //与服务端 Service 连接的 Connection
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //将服务端返回的 Binder 对象转换成 AIDL 接口所属的类型
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                //存储 IBookManager
                mBookManager = bookManager;
                //调用服务端方法
                List bookList = bookManager.getBookList();
                Log.i("Demo1", bookList.getClass().getCanonicalName());
                Log.i("Demo1", bookList.toString());
                //调用服务端方法
                bookManager.addBook(new Book(3, "Android 开发艺术探索"));
                List newBookList = bookManager.getBookList();
                Log.i("Demo1", newBookList.toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //绑定服务端服务
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.qinshou.demo2", "com.qinshou.demo2.BookManagerService"));
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //解绑服务
        unbindService(mServiceConnection);
    }
}

 

先运行 Demo2 再运行 Demo1 后打印结果如下:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第6张图片

 

打印中可以看到,服务端虽然使用了 CopyOnWriteArrayList,但是传输到客户端后为 ArrayList,这说明 AIDL 只能使用 ArrayList,也说明了客户端成功调用了服务端的方法。

 

4.4.1 扩展一——注册监听,观察者模式

假如有一种需求,用户不想时不时的查询图书列表,那样太累了,而是在希望图书馆在有新书到来时通知他,这是一种典型的观察者方式,那么应该怎么做呢?

首先需要提供一个 AIDL 接口 IOnNewBookArrivedListener.aidl,每个客户端都可以实现该接口表示注册一个监听,开启新书到来时的提醒功能,当然也可以取消该监听。这里需要 AIDL 接口是因为上面说到了 AIDL 中只能使用实现了 Parcelable 的对象,所以不能使用普通接口,但是 AIDL 接口在 AIDL 文件中都可以使用。

提醒功能用程序来说就是每当有新书添加时,就回调所有 IOnNewBookArrivedListener 中的 onNewBookArrived 方法,并将新书 book 作为参数回调。

增加 IOnNewBookArrivedListener.aidl:

package com.qinshou.ipc;

import com.qinshou.ipc.Book;

interface IOnNewBookArrivedListener{
    void onNewBookArrived(in Book newBook);
}

 

在 IBookManager.aidl 中增加注册监听和注销监听的接口:

package com.qinshou.ipc;

import com.qinshou.ipc.Book;
import com.qinshou.ipc.IOnNewBookArrivedListener;

interface IBookManager{
    List getBookList();

    void addBook(in Book book);

    void registerListener(IOnNewBookArrivedListener onNewBookArrivedListener);

    void unregisterListener(IOnNewBookArrivedListener onNewBookArrivedListener);
}

 

BookManagerService 中实现 registerListner 和 unregisterListener 接口,并开启一个线程模拟增加新书的情景,在增加新书时回调所有 IOnNewBookArrivedListener 的 onNewBookArrived 方法:

public class BookManagerService extends Service {
    private AtomicBoolean mIsServiceDestroy = new AtomicBoolean(false);

    private CopyOnWriteArrayList mBookList = new CopyOnWriteArrayList<>();
    private CopyOnWriteArrayList mOnNewBookArrivedListenerList = new CopyOnWriteArrayList<>();

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public List getBookList() throws RemoteException {
            Log.i("Demo2", "有客户端调用 getBookList() 方法,mBookList.size()--->" + mBookList.size());
            Log.i("Demo2","mBookList--->" + mBookList.toString());
            return mBookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            Log.i("Demo2", "有客户端调用 addBook() 方法,book--->" + book);
            mBookList.add(book);
        }

        @Override
        public void registerListener(IOnNewBookArrivedListener onNewBookArrivedListener) throws RemoteException {
            Log.i("Demo2", "有客户端调用 registerListener() 方法,onNewBookArrivedListener--->" + onNewBookArrivedListener);
            if (!mOnNewBookArrivedListenerList.contains(onNewBookArrivedListener)) {
                mOnNewBookArrivedListenerList.add(onNewBookArrivedListener);
                Log.i("Demo2", "注册监听成功");
            }else{
                Log.i("Demo2", "该 Listener 已存在,注册监听失败");
            }
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener onNewBookArrivedListener) throws RemoteException {
            Log.i("Demo2", "有客户端调用 unregisterListener() 方法,onNewBookArrivedListener--->" + onNewBookArrivedListener);
            if (mOnNewBookArrivedListenerList.contains(onNewBookArrivedListener)) {
                mOnNewBookArrivedListenerList.remove(onNewBookArrivedListener);
                Log.i("Demo2", "注销监听成功");
            }else{
                Log.i("Demo2", "该 Listener 不存在,注销监听失败");
            }
        }
    };

    public BookManagerService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("Demo2", "mBookList--->" + mBookList.getClass().getCanonicalName());
        mBookList.add(new Book(1, "Android"));
        mBookList.add(new Book(2, "Ios"));
        new Thread(new ServiceWorker()).start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestroy.set(true);
    }
    /**
     * Create By:禽兽先生
     * Create On:2019/1/2 23:07
     * Description: 开启一个线程模拟增加新书的场景
     */
    private class ServiceWorker implements Runnable {

        @Override
        public void run() {
            //每过 5s 增加一本书
            while (!mIsServiceDestroy.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int bookId = mBookList.size() + 1;
                Book newBook = new Book(bookId, "新书" + bookId);
                try {
                    onNewBookArrived(newBook);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void onNewBookArrived(Book book) throws RemoteException {
        mBookList.add(book);
        for (int i = 0; i < mOnNewBookArrivedListenerList.size(); i++) {
            IOnNewBookArrivedListener onNewBookArrivedListener = mOnNewBookArrivedListenerList.get(i);
            if (onNewBookArrivedListener != null) {
                onNewBookArrivedListener.onNewBookArrived(book);
            }
        }
    }
}

 

客户端 Demo1 修改一下实现:

public class MainActivity extends AppCompatActivity {
    //定义一个变量保存 IBookManager,用于程序销毁时注销观察者监听
    private IBookManager mBookManager;


    //与服务端 Service 连接的 Connection
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //将服务端返回的 Binder 对象转换成 AIDL 接口所属的类型
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                //存储 IBookManager
                mBookManager = bookManager;
                //调用服务端方法
                List bookList = bookManager.getBookList();
                Log.i("Demo1", bookList.getClass().getCanonicalName());
                Log.i("Demo1", bookList.toString());
                //调用服务端方法
                bookManager.addBook(new Book(3, "Android 开发艺术探索"));
                List newBookList = bookManager.getBookList();
                Log.i("Demo1", newBookList.toString());
                //注册监听
                Log.i("Demo1", "mOnNewBookArrivedListener--->" + mOnNewBookArrivedListener);
                bookManager.registerListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //重置 IBookManager
            if (mBookManager != null) {
                mBookManager = null;
            }
        }
    };

//    //观察者监听器收到事件后会在本地再通过 Handler 发送
//    private Handler mHandler = new Handler(new Handler.Callback() {
//        @Override
//        public boolean handleMessage(Message msg) {
//            switch (msg.what) {
//                case 1:
//                    Log.i("Demo1", "receive new Book--->" + msg.obj);
//                    break;
//                default:
//                    break;
//            }
//            return false;
//        }
//    });

    //观察者监听器
    private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {
            Log.i("Demo1", "receive new Book--->" + newBook);
//            Message message = Message.obtain();
//            message.what = 1;
//            message.obj = newBook;
//            mHandler.sendMessage(message);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //绑定服务端服务
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.qinshou.demo2", "com.qinshou.demo2.BookManagerService"));
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //注销监听
        if (mBookManager != null && mBookManager.asBinder().isBinderAlive()) {
            try {
                Log.i("Demo1", "mOnNewBookArrivedListener--->" + mOnNewBookArrivedListener);
                mBookManager.unregisterListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        //解绑服务
        unbindService(mServiceConnection);
    }
}

 

注意 Demo1 中的 aidl 相关文件也要同步修改,拷贝一份即可,重新运行 Demo2、Demo,打印结果如下:

 

可以看到每次有新书的时候客户端都能收到通知。

 

4.4.2 扩展二——注销监听

注册监听是没有问题了,但是当我们退出 Demo1,会回调 onDestroy(),在该方法里面会注销监听,这时候会惊奇的发现注销失败。注销失败说明在 List 不存在需要注销的监听器对象,可是在 Demo1 中我们明明传的是同一个对象,为什么会不存在呢?这就跟之前跨进程通信一样,虽然收到的对象内容一样,但是它们经过序列化和反序列化后其实并不是同一个对象,所以即使客户端传的是同一个对象,在服务端接收后会产生两个新的对象,之前的 List 中当然不存在这两个新对象,所以注销失败。通过打印可以看看传递的和接收的对象是不是同一个:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第7张图片

 

解决这个问题的办法是使用 RemoteCallbackList 来存储监听器。RemoteCallbackList 是系统提供的专门用来管理跨进程 listener 接口的容器。它可以存放任意的 AIDL 接口,在它内部有一个 Map 用来管理所有 AIDL 接口,key 是 IBinder 类型,value 是 Callback 类型。它的存储方法是这样的:

    public boolean register(E callback, Object cookie) {
        synchronized (mCallbacks) {
            if (mKilled) {
                return false;
            }
            // Flag unusual case that could be caused by a leak. b/36778087
            logExcessiveCallbacks();
            IBinder binder = callback.asBinder();
            try {
                Callback cb = new Callback(callback, cookie);
                binder.linkToDeath(cb, 0);
                mCallbacks.put(binder, cb);
                return true;
            } catch (RemoteException e) {
                return false;
            }
        }
    }

 

可以看到作为 key 的 binder 是通过 callback.asBinder() 方法获取的,虽然跨进程传输的对象在服务端会生成新的对象,但是这些对象的 Binder 对象其实是同一个,所以通过这个相同的 binder 我们就可以找出服务端之前创建的 listener,在注销的时候删除掉该 listener 即可。当然,RemoteCallbackList 已经将这些封装好了,我们只需要在存储的时候调用 register(E callback) 方法,删除的时候调用 unregister(E callback) 方法即可。

同时 RemoteCallbackList 还会在客户端进程终止后自动移除客户端注册的 listenr。RemoteCallbackList 内部还实现了线程同步的功能。这样看来 RemoteCallbackList 真是一个非常有用的类。

下面就用 RemoteCallbackList 修改一下 BookManagerService:

public class BookManagerService extends Service {
    private AtomicBoolean mIsServiceDestroy = new AtomicBoolean(false);

    private CopyOnWriteArrayList mBookList = new CopyOnWriteArrayList<>();
    //使用 RemoteCallbackList 存储监听器
    private RemoteCallbackList mOnNewBookArrivedListenerList = new RemoteCallbackList<>();

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public List getBookList() throws RemoteException {
            Log.i("Demo2", "有客户端调用 getBookList() 方法,mBookList.size()--->" + mBookList.size());
            Log.i("Demo2", "mBookList--->" + mBookList.toString());
            return mBookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            Log.i("Demo2", "有客户端调用 addBook() 方法,book--->" + book);
            mBookList.add(book);
        }

        @Override
        public void registerListener(IOnNewBookArrivedListener onNewBookArrivedListener) throws RemoteException {
            Log.i("Demo2", "有客户端调用 registerListener() 方法");
            mOnNewBookArrivedListenerList.register(onNewBookArrivedListener);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                mOnNewBookArrivedListenerList.beginBroadcast();
                Log.i("Demo2", "mOnNewBookArrivedListenerList.getRegisteredCallbackCount()--->" + mOnNewBookArrivedListenerList.getRegisteredCallbackCount());
                mOnNewBookArrivedListenerList.finishBroadcast();
            }
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener onNewBookArrivedListener) throws RemoteException {
            Log.i("Demo2", "有客户端调用 unregisterListener() 方法");
            mOnNewBookArrivedListenerList.unregister(onNewBookArrivedListener);
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
                mOnNewBookArrivedListenerList.beginBroadcast();
                Log.i("Demo2", "mOnNewBookArrivedListenerList.getRegisteredCallbackCount()--->" + mOnNewBookArrivedListenerList.getRegisteredCallbackCount());
                mOnNewBookArrivedListenerList.finishBroadcast();
            }
        }
    };

    public BookManagerService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.i("Demo2", "mBookList--->" + mBookList.getClass().getCanonicalName());
        mBookList.add(new Book(1, "Android"));
        mBookList.add(new Book(2, "Ios"));
        new Thread(new ServiceWorker()).start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestroy.set(true);
    }

    /**
     * Create By:禽兽先生
     * Create On:2019/1/2 23:07
     * Description: 开启一个线程模拟增加新书的场景
     */
    private class ServiceWorker implements Runnable {

        @Override
        public void run() {
            //每过 5s 增加一本书
            while (!mIsServiceDestroy.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int bookId = mBookList.size() + 1;
                Book newBook = new Book(bookId, "新书" + bookId);
                try {
                    onNewBookArrived(newBook);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void onNewBookArrived(Book book) throws RemoteException {
        mBookList.add(book);
        //虽然 RemoteCallbackList 名为中含有 List,但是它并不是 List,所以在操作它的时候比操作 List 麻烦一点,
        //其中 beginBroadcast() 和 finishBroadcast() 必须配对使用,哪怕仅仅是想获取 RemoteCallbackList 中的个数
        final int n = mOnNewBookArrivedListenerList.beginBroadcast();
        for (int i = 0; i < n; i++) {
            IOnNewBookArrivedListener onNewBookArrivedListener = mOnNewBookArrivedListenerList.getBroadcastItem(i);
            if (onNewBookArrivedListener != null) {
                onNewBookArrivedListener.onNewBookArrived(book);
            }
        }
        mOnNewBookArrivedListenerList.finishBroadcast();
    }
}

 

重新运行 Demo2、Demo1 后观察打印,发现在调用 unregisterListener() 注销方法后 RemoteCallbackList 中的元素个数确实减少了,说明注销成功。

 

4.4.3 扩展三——ANR

当客户端调用服务端的方法时,被调用的方法时运行在服务端的 Binder 线程池中,同时客户端线程会被挂起,如果服务端这个被调用的方法比较耗时的话就会导致客户端线程长期阻塞,如果这个线程是 UI 线程的话就会导致客户端 ANR。所以如果我们明确知道服务端某个方法是耗时的话就应该避免在主线程,也就是 UI 线程中调用该方法。客户端的 onServiceConnected() 和 onServiceDisconnected() 也是运行在 UI 线程中的,所以也不能在它们之中调用服务端的耗时方法。

服务端的方法本身就是运行在服务端的 Binder 线程池中,所以服务端方法本身是可以执行大量耗时操作的,所以不需要再服务端方法中开启线程去异步执行任务,切换线程的操作应该在客户端完成。

修改 BookManagerService 的 getBookList() 方法,假设它是耗时方法:

        @Override
        public List getBookList() throws RemoteException {
            Log.i("Demo2", "有客户端调用 getBookList() 方法,mBookList.size()--->" + mBookList.size());
            Log.i("Demo2", "mBookList--->" + mBookList.toString());
            try {
                Thread.sleep(5*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return mBookList;
        }

 

在客户端 Demo1 中写一个按钮来调用该方法:

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mBookManager != null) {
                    try {
                        List bookList = mBookManager.getBookList();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

 

连续点击几次该按钮后就会发现客户端卡死了,解决方法很简单,就是把这个方法的调用放到子线程中:

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        if (mBookManager != null) {
                            try {
                                List bookList = mBookManager.getBookList();
                            } catch (RemoteException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }).start();
            }
        });

 

同理,如果服务端需要调用客户端 listener 的方法时,被调用的方法也是运行在 Binder 线程池中,只不过是客户端的线程池,所以如果 onNewBookArrived() 是一个耗时方法的话,那么服务端调用该方法的代码也要放到子线程中。

还有一点,由于客户端 listener 方法是运行在 Binder 线程池中,所以不能在该方法中直接操作 UI,需要通过 Handler 或其他方法切换到主线程操作 UI,如在 Demo1 的 MainActivity 的布局中增加一个 TextView 来显示收到的新书,则 onNewBookArrived() 收到后需要通过 Handler 切换到主线程显示:

    //观察者监听器收到事件后会在本地再通过 Handler 发送
    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case 1:
                    Log.i("Demo1", "receive new Book--->" + msg.obj);
                    TextView textView = findViewById(R.id.text_view);
                    textView.setText(msg.obj.toString());
                    break;
                default:
                    break;
            }
            return false;
        }
    });

    //观察者监听器
    private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {
            Log.i("Demo1", "receive new Book--->" + newBook);
            Message message = Message.obtain();
            message.what = 1;
            message.obj = newBook;
            mHandler.sendMessage(message);
        }
    };

 

4.4.4 扩展四——服务端死亡监听

Binder 是有可能意外死亡的,一般是服务端进程意外停止导致的,这是我们需要重连服务。有两种方法:

1)给 Binder 设置 DeathRecipient 监听,当 Binder 死亡时会回调 binderDied() 方法,在该方法中就可以重连远程服务,具体做法:

定义一个死亡监听:

    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            if (mBookManager == null) {
                return;
            }
            //取消死亡监听
            mBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
            //重置 IBookManager
            mBookManager = null;
            Log.i("Demo1", "binderDied,Thread--->" + Thread.currentThread().getName());
            //TODO 在下面重新绑定远程服务
        }
    };

然后在绑定远程服务成功后设置该监听:

    //与服务端 Service 连接的 Connection
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //将服务端返回的 Binder 对象转换成 AIDL 接口所属的类型
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                //设置死亡监听
                service.linkToDeath(mDeathRecipient,0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            ...
        }
        ...
    };

 

2)在 onServiceDisconnected() 方法中重连远程服务:

    //与服务端 Service 连接的 Connection
    private ServiceConnection mServiceConnection = new ServiceConnection() {
        ...

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //重置 IBookManager
            if (mBookManager != null) {
                mBookManager = null;
            }
            //TODO 在下面重新绑定远程服务
            Log.i("Demo1", "onServiceDisconnected,Thread--->" + Thread.currentThread().getName());
        }
    };

 

这两种方法可以任选其一,区别在于 binderDied() 方法是在 Binder 线程池中被调用,而 onServiceDisconnected() 是在 UI 线程中被调用。

通过 adb 命令 adb shell kill 进程pid,这里的进程 pid 是 Demo2 的 pid 来杀死进程后打印如下:

 

4.4.5 扩展五——权限验证

如何在 AIDL 中使用权限验证功能呢?默认情况下,我们的远程服务是可以被任何人连接的,但是这并不是我们希望的,所以我们可以给服务假如权限验证功能,当验证失败时就无法调用服务中的方法。

原书中提到常用方法有两种:

第一种是在 onBind 中对 permission 进行验证,验证失败就直接返回 null,这样一来验证失败的客户端直接就无法绑定服务了,实际操作后发现只有在服务端和客户端是同一个工程的情况下才能使用该验证,因为 onBind() 不是由 binder 调用的,它运行在服务端的 UI 线程,所以在 onBind() 方法中只能验证服务端的权限。如果服务端与客户端是两个工程(我自己操作时写的 Demo 就是两个工程)则服务端无论怎么验证,验证结果都是 -1。

第二种是重写 Binder 的 onTransact() 方法,客户端每次调用服务端方法时都会先回调该方法,在该方法中对客户端权限进行验证,验证失败则 return false,表示拒绝客户端调用服务端的方法。

下面是 Demo 演示:

在服务端 Demo2 的 AndroidManifest.xml 文件中增加自定义权限:

    

 

重写 BookManagerService 中 mBinder 的 onTransact() 方法:

    private Binder mBinder = new IBookManager.Stub() {
        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            //该方法中可以检查调用方,即客户端的权限,return false 即表示拒绝客户端的调用
            int permission = checkCallingPermission("com.qinshou.demo2.permission.ACCESS_BOOK_MANAGER");
            Log.i("Demo2", "onTransact--->" + Thread.currentThread().getName() + ",permission--->" + permission);
            if (permission == PackageManager.PERMISSION_DENIED) {
                Log.i("Demo2", "沒有权限,调用服务端方法失败");
                return false;
            }
            //甚至可以对客户端的包名进行验证
//            String packageName;
//            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
//            if (packages != null && packages.length > 0){
//                packageName = packages[0];
//            }
//            //TODO 对 packageName 进行验证
            return super.onTransact(code, data, reply, flags);
        }
        ...
    };

 

这时候再运行客户端 Demo1 就会发现调用服务端的方法失败了:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第8张图片

 

所以需要在 Demo1 的 AndroidManifest.xml 中声明我们自定义的权限:

    

 

再次运行发现就没有问题了,至此 AIDL 告一段落。

 

4.5 使用 ContentProvider

ContentProvider 作为 Android 四大组件之一,是 Android 提供的一种专门用于不同应用间进行数据共享的方式,所以它天生就适合跨进程通信。ContentProvider 的底层实现也是 Binder,由此可见 Binder 是何等重要。ContentProvider 的使用比 AIDL 要简单得多,系统已经做了很好的封装,所以可以无须关注底层细节就可以轻松实现 IPC。虽然 ContentProvider 使用起来很简单,但是它仍有很多细节,如 CRUD 操作、防止 SQL 注入和权限控制等,但书中没有详细讲解这些内容,应该是放到后面专门讲 ContentProvider 的章节中了。

系统已经预置了很多 ContentProvider,我在平时也有用到过,如通讯录、短信等,要访问这些消息只需要通过 ContentResolver 的 query、update、insert、delete 方法即可。现在我们来实现一个名为 BookContentProvider 的自定义 ContentProvider:

public class BookProvider extends ContentProvider {
    private static final String TAG = "Chapter_2_4_5";
    
    private static final String AUTHORITY = "com.qinshou.chapter_2_4_5.book_provider";

    private static final int BOOK_URL_CODE = 0;
    private static final int USER_URL_CODE = 1;

    private static UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        sUriMatcher.addURI(AUTHORITY, "book", BOOK_URL_CODE);
        sUriMatcher.addURI(AUTHORITY, "user", USER_URL_CODE);
    }

    /**
     * Create By:禽兽先生
     * Create On:2019/1/5 15:36
     * Description: 表示 ContentProvider 的创建,在该方法中可以进行一些初始化操作
     */
    @Override
    public boolean onCreate() {
        Log.i(TAG, "onCreate--->" + Thread.currentThread().getName());
        return false;
    }

    /**
     * Create By:禽兽先生
     * Create On:2019/1/5 15:36
     * Description: 返回一个 Uri 请求所对应的 MIME 类型(媒体类型,如图片、视频等),如果不关注
     * 该类型,可以返回 null 或者 "/"
     */
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    /**
     * Create By:禽兽先生
     * Create On:2019/1/5 15:36
     * Description: 对数据的查询操作
     */
    @Override
    public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Log.i(TAG, "query--->" + Thread.currentThread().getName());
        Log.i(TAG, getTableName(uri));
        return null;
    }

    /**
     * Create By:禽兽先生
     * Create On:2019/1/5 15:36
     * Description: 对数据的插入操作
     */
    @Override
    public Uri insert(@NonNull Uri uri, ContentValues values) {
        Log.i(TAG, "insert--->" + Thread.currentThread().getName());
        Log.i(TAG, getTableName(uri));
        return null;
    }

    /**
     * Create By:禽兽先生
     * Create On:2019/1/5 15:36
     * Description: 对数据的删除操作
     */
    @Override
    public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
        Log.i(TAG, "delete--->" + Thread.currentThread().getName());
        Log.i(TAG, getTableName(uri));
        return 0;
    }

    /**
     * Create By:禽兽先生
     * Create On:2019/1/5 15:36
     * Description: 对数据的更新操作
     */
    @Override
    public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        Log.i(TAG, "update--->" + Thread.currentThread().getName());
        Log.i(TAG, getTableName(uri));
        return 0;
    }

    private String getTableName(Uri uri) {
        switch (sUriMatcher.match(uri)) {
            case BOOK_URL_CODE:
                return "操作 Book 表";
            case USER_URL_CODE:
                return "操作 User 表";
            default:
                return "没有对应的表";
        }
    }
}

 

可以看到自定义 ContentProvider 很简单,只需要继承 ContentProvider 然后实现它的六个抽象方法即可。这六个抽象方法的作用已经写在代码注释中了。onCreate() 表示 ContentProvider 被创建,getType() 返回的是 Uri 请求对应的文件类型,一般不关注这个类型的话返回 null 或者 "/" 即可,其他四个方法分别对应增删改查操作。ContentProvider 只会收到客户端的 Uri,所以可以通过 Uri 来判断外部应用到底想访问哪些数据,上面的服务端写了两个 path,并定义了对应的 code,通过 UrilMatcher 这个类来根据请求的 Uri 来取得对应的 code,然后根据这个 code 来决定具体操作哪个表。

自定义 BookContentProvider 后还要记得四大组件都要在 AndroidManifest.xml 中注册:

        
            
            
        

 

ContentProvider 的注册声明中有一个参数 authorities,它是 ContentProvider 的唯一标识,外部应用要调用该 ContentProvider 就是用该标识作为 Uri。ContentProvider 也可以做权限验证,就跟 AIDL 类似,在服务端自定义权限:

    

 

然后客户端需要声明该权限才能调用该 ContentProvider,它可不会像 AIDL 那样“温柔”,只是调用不了而已,如果没有权限想要访问 ContentProvider 的话客户端是会直接 Crash 的。

直接写个客户端的 Demo 吧,这里没有在服务端真实的数据库操作,只是演示跨进程调用,下面是客户端的 MainActivity:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.qinshou.chapter_2_4_5.book_provider/book");
                getContentResolver().insert(uri, new ContentValues());
            }
        });
        findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.qinshou.chapter_2_4_5.book_provider/book");
                getContentResolver().delete(uri, null, null);
            }
        });
        findViewById(R.id.button3).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.qinshou.chapter_2_4_5.book_provider/user");
                getContentResolver().update(uri, new ContentValues(), null, null);
            }
        });
        findViewById(R.id.button4).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Uri uri = Uri.parse("content://com.qinshou.chapter_2_4_5.book_provider/user");
                getContentResolver().query(uri, null, null, null, null);
            }
        });
    }
}

 

在客户端定义了四个按钮,然后演示 Book 表的增、Book 表的删、User 表的改、User 表的查,主要是调用的 Uri 不同,上面说了,服务端会根据 Uri 来决定具体操作哪个表。先运行服务端,然后再运行客户端,点击四个按钮后,打印结果如下:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第9张图片

 

可以看到客户端已经调用到了客户端的对应方法,然后也可以看到 onCreate() 是执行在主线程中,而 CRUD 的方法是执行在 Binder 线程中的。ContentProvider 的其他细节,等看到书中的具体章节后再深入研究。

 

4.6 使用 Socket

Socket 也称为套接字,其实在刚开始工作的那会儿因为工作需要,所以当时使用 Socket 还蛮多的,对它还比较熟悉,它是一种网络通信,自然也可以用于跨进程通信,而且 Socket 可以传输任意字节流。

在使用 Socket 的时候,需要注意两点:

1)记得添加网络权限:

    
    

 

2)网络通信的工作不能在主线程中。

下面上代码,先是服务端:

public class TCPServerService extends Service {
    private static final String TAG = "Chapter_2_4_6";
    private boolean mIsServiceDestroyed = false;

    /* 服务器端口 */
    private final static int SERVER_HOST_PORT = 8888;

    public TCPServerService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        new Thread(new TcpServer()).start();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestroyed = true;
    }

    private class TcpServer implements Runnable {

        @Override
        public void run() {
            ServerSocket serverSocket;
            try {
                //创建服务端 Socket
                serverSocket = new ServerSocket(SERVER_HOST_PORT);
            } catch (IOException e) {
                e.printStackTrace();
                Log.i(TAG, "建立 ServerSocket 失败,port--->" + SERVER_HOST_PORT);
                return;
            }
            Log.i(TAG, "建立 ServerSocket,等待客户端的连接");
            while (!mIsServiceDestroyed) {
                try {
                    //等待客户端的连接,该方法会阻塞线程,直到有客户端连接上
                    final Socket socket = serverSocket.accept();
                    Log.i(TAG, "有客户端连接上了");
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                //开启一个线程循环接收客户端的消息
                                responseClient(socket);
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                    }).start();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        private void responseClient(Socket socket) throws IOException {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            PrintWriter printWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
            while (!mIsServiceDestroyed) {
                //接收客户端的消息
                String string = bufferedReader.readLine();
                Log.i(TAG, "收到来自客户端的消息--->" + string);
                if (string == null || string.length() == 0) {
                    break;
                }
                //将字符串处理一下后回复给客户端,这里回复的文字可以随意发挥
                string = string
                        .replace("吗", "")
                        .replace("?", "!")
                        .replace("?", "!");
                printWriter.println(string);
                printWriter.flush();
            }

            //关闭流
            bufferedReader.close();
            printWriter.close();
            socket.close();
        }
    }
}

 

记得在 AndroidManifest.xml 中注册一下这个 Service,还有添加网络权限:

        

        

 

在服务端工程中的 MainActivity 中开启该服务;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent(this, TCPServerService.class);
        startService(intent);
    }
}

 

接下来是客户端,它做的事跟服务端差不多,创建一个 Socket,然后循环接收服务端的消息。在客户端布局中有一个文本输入框用来接收用户输入,有一个按钮点击发送输入的内容,布局过于简单就不贴了,只展示客户端 MainActivity 的代码:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Chapter_2_4_6_Client";
    private PrintWriter mPrintWriter;

    private Socket mSocket;
    private Handler mHandler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case 0:
                    Log.i(TAG, "收到服务端消息--->" + msg.obj);
                    break;
            }
            return false;
        }
    });

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                connectTCPServer();
            }
        }).start();
        final EditText editText = findViewById(R.id.edit_text);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                final String string = editText.getText().toString();
                if (string != null && string.length() > 0 && mPrintWriter != null) {
                    new Thread(new Runnable() {
                        @Override
                        public void run() {
                            mPrintWriter.println(string);
                            mPrintWriter.flush();
                            runOnUiThread(new Runnable() {
                                @Override
                                public void run() {
                                    editText.setText("");
                                }
                            });
                        }
                    }).start();
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mSocket != null) {
            try {
                mSocket.shutdownInput();
                mSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    private void connectTCPServer() {
        Socket socket = null;
        while (socket == null) {
            try {
                socket = new Socket("localhost", 8888);
                mSocket = socket;
                mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())));
                Log.i(TAG, "成功连接上服务端");
            } catch (IOException e) {
                SystemClock.sleep(1000);
                Log.i(TAG, "连接服务端失败,正尝试重新连接");
            }
        }
        try {
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            while (!MainActivity.this.isFinishing()) {
                String string = bufferedReader.readLine();
                if (string == null || string.length() == 0) {
                    continue;
                }
                Message message = Message.obtain();
                message.what = 0;
                message.obj = string;
                mHandler.sendMessage(message);
            }

            Log.i(TAG, "客户端断开连接");
            if (mPrintWriter != null) {
                mPrintWriter.close();
            }
            bufferedReader.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

 

先运行服务端,然后再运行客户端,输入一些文字后,打印结果如下:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第10张图片

 

这仅仅是 Socket 的一个演示,Socket 除了发送文字,还可以发送字节流。Socket 除了能进行进程间通信,还能进行设备间通信,只要在客户端连接的时候,把 "localhost" 改为服务端 ip 即可。

 

5 Binder 连接池

Binder 连接池是跟 AIDL 有关的,使用 AIDL 的步骤是创建一个 Service 和一个 AIDL 接口,然后创建一个类继承自 AIDL 接口中的 Stub 类并实现其中的抽象方法(或者直接创建一个成员内部类),在 Service 的 onBind() 方法中返回这个类的对象,客户端就可以通过绑定服务端的 Service 建立连接后或者该对象,然后通过该对象调用服务端的方法。

如果有 10 个模块都需要使用 AIDL 来进行进程间通信呢?是不是要创建 10 个 Service 呢?如果是 100 个呢?显然不可能创建 100 个 Service,因为这会让我们的应用看起来很重量级,而且 Service 也是占系统资源的,所以我们想办法将所有的 AIDL 放到同一个 Service 中管理。

在这种模式下,整个工作机制应该是每个业务模块分别创建自己的 AIDL 接口并实现该接口,并在里面实现自己的业务逻辑,然后向服务端提供自己的唯一标识和对应的 Binder 对象。服务端的 Service 中提供一个 findBinder() 方法,这个方法能够根据业务模块的特征来返回对应的 Binder,不同的业务模块拿到所需的 Binder 对象后就可以进行远程方法调用了。

所以 Binder 连接池的主要作用就是将每个业务模块的 Binder 请求统一转发到远程 Service 中去执行,从而避免重复创建 Service。还是用代码说话:

创建两个 aidl 接口,ISecurityCenter.aidl,ICalculator.aidl:

package com.qinshou.ipc;

interface ISecurityCenter{
    String encrypt(String content);

    String decrypt(String password);
}

 

package com.qinshou.ipc;

interface ICalculator{
    int add(int a,int b);
}

 

然后分别创建它们的实现了,并实现各接口的业务逻辑:

public class SecurityCenterImpl extends ISecurityCenter.Stub {
    private static final String TAG = "Chapter_2_4_7";

    private static final char SECRET_CODE = '^';
    @Override
    public String encrypt(String content) throws RemoteException {
        Log.i(TAG, "encrypt--->" + content);
        char[] charArray = content.toCharArray();
        for (int i = 0; i < charArray.length; i++) {
            charArray[i] ^= SECRET_CODE;
        }
        return new String(charArray);
    }

    @Override
    public String decrypt(String password) throws RemoteException {
        Log.i(TAG, "decrypt--->" + password);
        return encrypt(password);
    }
}

 

public class CalculatorImpl extends ICalculator.Stub {
    private static final String TAG = "Chapter_2_4_7";

    @Override
    public int add(int a, int b) throws RemoteException {
        Log.i(TAG, "add,a--->" + a + ",b--->" + b);
        return a + b;
    }
}

 

然后创建一个 IBinderPool.aidl,里面只定义了一个方法就是 findBinder(int binderCode):

package com.qinshou.ipc;

interface IBinderPool{
    IBinder findBinder(int binderCode);
}

 

最后就是 BinderPool 了,它内部定义了各个 Binder 对应的 Code,内部还有一个 BinderPool 的实现类。在创建这个连接池的时候也会去绑定服务,这个主要是用于绑定死亡监听,设置断线重连机制。

public class BinderPool {
    private static final String TAG = "Chapter_2_4_7";

    private static final int BINDER_NONE = -1;
    private static final int BINDER_SECURITY_CENTER = 0;
    private static final int BINDER_CALCULATOR = 1;

    private Context mContext;
    private IBinderPool mBinderPool;
    private static volatile BinderPool singleton;
    private CountDownLatch mCountDownLatch;

    private BinderPool(Context context) {
        mContext = context.getApplicationContext();
        connectBinderPoolService();
    }

    public static BinderPool getInstance(Context context) {
        if (singleton == null) {
            synchronized (BinderPool.class) {
                if (singleton == null) {
                    singleton = new BinderPool(context);
                }
            }
        }
        return singleton;
    }

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mBinderPool = IBinderPool.Stub.asInterface(service);
            try {
                mBinderPool.asBinder().linkToDeath(mDeathRecipient, 0);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            mCountDownLatch.countDown();

        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            Log.i(TAG, "binderDied");
            mBinderPool.asBinder().unlinkToDeath(mDeathRecipient, 0);
            mBinderPool = null;
            connectBinderPoolService();
        }
    };

    private synchronized void connectBinderPoolService() {
        mCountDownLatch = new CountDownLatch(1);
        Intent intent = new Intent(mContext, BinderPoolService.class);
        mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
        try {
            mCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static class BinderPoolImpl extends IBinderPool.Stub {
        @Override
        public IBinder findBinder(int binderCode) throws RemoteException {
            IBinder binder = null;
            switch (binderCode) {
                case BINDER_SECURITY_CENTER:
                    Log.i(TAG, "findBinder,需要使用 SecurityCenter");
                    binder = new SecurityCenterImpl();
                    break;
                case BINDER_CALCULATOR:
                    Log.i(TAG, "findBinder,需要使用 Calculator");
                    binder = new CalculatorImpl();
                    break;
                default:
                    break;
            }
            return binder;
        }
    }
}

 

如此一来,客户端就可以根据不同的 code 来获取不同的 Binder,从而调用各自的方法:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Chapter_2_4_7_Client";

    private ServiceConnection mServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBinderPool binderPool = IBinderPool.Stub.asInterface(service);
            try {
                ISecurityCenter securityCenter = ISecurityCenter.Stub.asInterface(binderPool.findBinder(0));
                String encrypt = securityCenter.encrypt("Hello World");
                Log.i(TAG, "encrypt--->" + encrypt);
                String decrypt = securityCenter.decrypt(encrypt);
                Log.i(TAG, "decrypt--->" + decrypt);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
            try {
                ICalculator calculator = ICalculator.Stub.asInterface(binderPool.findBinder(1));
                int result = calculator.add(123, 456);
                Log.i(TAG, "result--->" + result);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.qinshou.chapter_2_4_7", "com.qinshou.chapter_2_4_7.BinderPoolService"));
        bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
    }
}

 

打印结果如下:

《Android 开发艺术探索》读书笔记(二)——IPC 机制_第11张图片

 

需要注意的是,客户端调用方法时建议放到线程中去执行,因为在 Binder 连接池的实现中,通过 CountDownLatch 将 bindService 的这一异步操作转换成同步操作,所以它可能是耗时的,同时 Binder 方法的调用过程也可能是耗时的,因此建议客户端的调用不要放在主线程中。

有了这个 BinderPool 后就可以大大 AIDL 的开发,如果要加入一个新的 AIDL,只需要建立新的 AIDL 文件,并建立它自己的实现类,确定一个唯一的 binderCode,然后在 BinderPoolImpl 的 findBinder() 方法中增加对应 Binder 的返回即可,不需要再创建新的 Service。所以 BinderPool 能极大地提高 AIDL 的开发效率,避免大量的 Service,因此建议在 AIDL 开发工作中引入 BinderPool 机制。

 

6 选用合适的 IPC 方式

实现 IPC 的方式各种各样,所以我们在实际开发中要选择合适的 IPC 方式来应对各种开发场景,下面是各种 IPC 方式的优缺点和适用场景:

IPC 方式的优缺点和适用场景
名称 优点 缺点 适用场景
Bundle 简单易用 只能传输 Bundle 支持的数据类型 四大组件间的进程间通信
文件共享 简单易用 不适合高并发场景,并且无法做到进程间的即时通信 无并发访问情形,交换简单的数据,实时性不高的场景
AIDL 功能强大,支持一对多并发通信 使用稍复杂,需要处理好线程同步 一对多通信且有 RPC 需求
Messenger 功能一般,支持一对多串行通信,支持实时通信 不能很好地处理高并发情形,不支持 RPC,数据通过 Message 进行传输,因此只能传输 Bundle 支持的数据类型 低并发的一对多即时通信,无 RPC 需求,或者无须返回结果的 RPC 需求
ContentProvider 在数据源访问方面功能强大,支持一对多并发数据共享,可通过 Call 方法扩展其他操作 可以理解为受约束的 AIDL,主要提供数据源的 CRUD 操作 一对多的进程间的数据共享
Socket 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 实现细节稍微有些麻烦,不支持直接的 RPC 网络数据交换

 

7 总结

这一章足足看了两周,也是写的最长的一篇读书笔记了。刚开始看的时候觉得多进程开发的需求比较少,所以没有放在心上,然后在看到 Binder 的时候觉得有些复杂,所以看到并不仔细,但是后面提到 Binder 越来越多才意识到这是很重要的一个知识点,又重新开始细细看。从开始讲 Binder 机制,到 Messeneger、AIDL、ContentProvider、Binder 连接池,都提到了 Binder,可见 Binder 在进程间通信中是多么重要。希望今后自己不要看到繁琐的知识点就望而生畏,如果被提到多次,那么它一定很重要。

你可能感兴趣的:(Android书籍读书笔记)