Android开发艺术探索知识回顾——第2章 IPC机制:1、IPC 的基础知识

 

第2章 IPC机制

本章主要讲解 Android 中的 IPC 机制。首先介绍 Android 中的多进程概念以及多进程开发模式中常见的注意事项,接着介绍Android 中的序列化机制和 Binder,然后详细介绍 Bundle文件共享、AIDLMessengerContentProvider Socket 等进程间通信的方式。

为了更好地使用 AIDL 来进行进程间通信,本章还引入了 Binder 连接池的概念。最后,本章讲解各种进程间通信方式的优缺点和适用场景。通过本章,可以让读者对 Android 中的 IPC 机制和多进程开发模式有深入的理解。

 

2.1 Android IPC 简介

什么是IPC?

IPC 是 Inter-Process Communication 的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。

进程、线程、ANR

说起进程间通信,我们首先要理解什么是进程,什么是线程?

进程和线程是截然不同的概念。按照操作系统中的描述,线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。最简单的情况下,一个进程中可以只有一个线程,即主线程,在Android里面主线程也叫UI线程,在UI线程里才能操作界面元素。

很多时候,一个进程中需要执行大量耗时的任务,如果这些任务放在主线程中去执行就会造成界面无法响应,严重影响用户体验这种情况在 PC 系统和移动系统中都存在,在Android中有一个特殊的名字叫做ANR (Application Not Responding) 即应用无响应。解决这个问题就需要用到线程,把一些耗时的任务放在线程中即可

IPC 不是 Android 中所独有的,任何一个操作系统都需要有相应的 IPC 机制

IPC 不是 Android 中所独有的,任何一个操作系统都需要有相应的 IPC 机制,比如 Windows 上可以通过剪贴板、管道和邮槽等来进行进程间通信;Linux 上可以通过命名管道、共享内容、信号量等来进行进程间通信。可以看到不同的操作系统平台有着不同的进程间通信方式,对于 Android 来说,它是一种基于 Linux 内核的移动操作系统,它的进程间通信方式并不能完全继承自 Linux,相反,它有自己的进程间通信方式。

Android进程间通信方式:Binder 和 Socket

在 Android 中最有特色的进程间通信方式就是 Binder 了,通过 Binder 可以轻松地实现进程间通信。除了 Binder,Android 还支持 Socket,通过 Socket 也可以实现任意两个终端之间的通信,当然同一个设备上的两个进程通过 Socket 通信自然也是可以的。

多进程的情况分为两种

说到 IPC 的使用场景就必须提到多进程,只有面对多进程这种场景下,才需要考虑进程间通信。这个是很好理解的,如果只有一个进程在运行,又何谈多进程呢?多进程的情况分为两种

第一种情况是:一个应用因为某些原因自身需要釆用多进程模式来实现,至于原因,可能有很多,比如有些模块由于特殊原因需要运行在单独的进程中,又或者为了加大一个应用可使用的内存,所以需要通过多进程来获取多份内存空间。Android对单个应用所使用的最大内存做了限制,早期的一些版本可能是16MB,不同设备有不同的大小。

另一种情况是:当前应用需要向其他应用获取数据,由于是两个应用,所以必须采用跨进程的方式,来获取所需的数据,甚至我们通过系统提供的 Contentprovider 去査询数据的时候,其实也是一种进程间通信,只不过通信细节被系统内部屏蔽了,我们无法感知而已。

后续章节会详细介绍 Contentprovider 的底层实现,这里就先不做详细介绍了。总之,不管由于何种原因,我们采用了多进程的设计方法,那么应用中就必须妥善地处理进程间通信的各种问题。

 

2.2 Android中的多进程模式

给四大组件指定 android:process 属性,可以轻易地开启多进程模式,实际使用过程中却暗藏杀机

在正式介绍进程间通信之前,我们必须先要理解 Android 中的多进程模式。通过给四大组件指定 android:process 属性,我们可以轻易地开启多进程模式,这看起来很简单,但是实际使用过程中却暗藏杀机,多进程远远没有我们想的那么简单,有时候我们通过多进程得到的好处甚至都不足以弥补使用多进程所带来的代码层面的负面影响。下面会详细分析这些问题。

 

2.2.1开启多进程模式

正常情况下,在 Android 中多进程是指一个应用中存在多个进程的情况,因此这里不讨论两个应用之间的多进程情况。首先,在Android中使用多进程只有一种方法,那就是给四 大组件 (Activity、Service、Receiver、ContentProvider) AndroidMenifest 中指定 android:process 属性,除此之外没有其他办法,也就是说我们无法给一个线程或者一个实体类指定其运行时所在的进程。

其实还有另一种非常规的多进程方法,那就是通过 JNI 在 native 层去 fork 一个新的进程,但是这种方法属于特殊情况,也不是常用的创建多进程的方式,因此我们暂时不考虑这种方式。下面是一个示例,描述了如何在Android中创建多进程:

        
            
                
                
            
        
        
        
        
        

上面的示例分别为 SecondActivity 和 ThirdActivity 指定了 process 属性,并且它们的属性值不同,这意味着当前应用又增加了两个新进程。假设当前应用的包名为"com.ryg.chapter_2",当 SecondActivity 启动时,系统会为它创建一个单独的进程,进程名为 “com.ryg.chapter_2:remote" 。

ThirdActivity启动时,系统也会为它创建一个单独的进程,进程名为"com.ryg.chapter_2.remote"同时入口 Activity MainActivity,没有为它指定 process 属性,那么它运行在默认进程中,默认进程的进程名是包名。

下面我们运行一下看看效果,启动点击跳转到SecondActivity、ThirdActivity,如图2-1所示。

Android开发艺术探索知识回顾——第2章 IPC机制:1、IPC 的基础知识_第1张图片

进程列表末尾存在3个进程,进程id分别为1804、1821、1837这说明我们的应用成功地使用了多进程技术,是不是很简单呢?这只是开始,实际使用中多进程是有很多问题需要处理的。

除了在 Eclipse 的 DDMS 视图中査看进程信息,还可以用shell来査看,命令为 :

adb shell ps 或者 adb shell ps | grep com.ryg.chapter_2

其中 com.ryg.chapter_2 是包名,如图 2-2 所示,通过 ps 命令也可以査看一个包名中当前存在的进程信息。

Android开发艺术探索知识回顾——第2章 IPC机制:1、IPC 的基础知识_第2张图片

注:第一次执行出现 error: more than one device/emulator,是因为测试的时候开了模拟器和真机,拔掉一个即可。

不知道读者朋友有没有注意到,SecondActivity 和 ThirdActivity 的 android:process 属性分别为 ":remote" 和 "com.ryg.chapter_2.remote" 那么这两种方式有区别吗?

其实是有区别的,区别有两方面:首先,":" 的含义是指要在当前的进程名前面附加上当前的包名,这是一种简写的方法,对于SecondActivity来说,它完整的进程名为 com.ryg.chapter_ 2:remote,这一点通过图2.1和图2.2中的进程信息也能看出来,而对于ThirdActivity 中的声明方式,它是一种完整的命名方式,不会附加包名信息;

其次,进程名以 ":" 开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以":" 开头的进程属于全局进程,其他应用通过 ShareUID 方式可以和它跑在同一个进程中。

我们知道 Android 系统会为每个应用分配一个唯一的 UID,具有相同 UID 的应用才能共享数据。 这里要说明的是,两个应用通过ShareUID 跑在同一个进程中是有要求的,需要这两个应用有相同的 ShareUID 并且签名相同才可以 在这种情况下,它们可以互相访问对方的私有数据,比如data目录、组件信息等,不管它们是否跑在同一个进程中。当然如果它们跑在同一个进程中,那么除了能共享data目录、组件信息,还可以共享内存数据,或者说它们看起来就像是一个应用的两个部分。

 

2.2.2多进程模式的运行机制

如果用一句话来形容多进程,那笔者只能这样说:"当应用开启了多进程以后,各种奇怪的现象都出现了"。为什么这么说呢?这是有原因的。大部分人都认为开启多进程是很简单的事情,只需要给四大组件指定 android:process 属性即可。

比如说在实际的产品开发中,可能会有多进程的需求,需要把某些组件,放在单独的进程中去运行,很多人都会觉得这不很简单吗?然后迅速地给那些组件指定了 android:process 属性,然后编译运行,发现“正常地运行起来了”。 这里笔者想说的是,那是真的正常地运行起来了吗?现在先不置可否,下面先给举个例子,然后引入本节的话题。

还是本章刚开始说的那个例子,其中 SecondActivity 通过指定 android:process 属性从而使其运行在一个独立的进程中,这里做了一些改动,我们新建了一个类叫做 UserManager,这个类中有一个 public 的静态成员变量,如下所示。

public class UserManager {
    public static int sUserId = 1;
}

然后在 MainActivity 的 onCreate 中我们把这个 sUserld 重新赋值为2,打印出这个静态变量的值后再启动 SecondActivity,SecondActivity 中我们再打印一下 sUserld 的值。按照正常的逻辑,静态变量是可以在所有的地方共享的,并且一处有修改处处都会同步,图2-3是运行时所打印的日志,我们看一下结果如何。

看了图2-3中的日志,发现结果和我们想的完全不一致,正常情况下 SecondActivity 中打印的 sUserld 的值应该是2才对,但是从日志上看它竟然还是1,可是我们的确已经在 MainActivity 中把 sUserld 重新赋值为2了。看到这里,大家应该明白了这就是多进程所带来的问题,多进程绝非只是仅仅指定一个 android:process 属性那么简单。

 

上述问题出现的原因是 SecondActivity 运行在一个单独的进程中,我们知道 Android 为每一个应用分配了一个独立的虚拟机,或者说为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本。

拿我们这个例子来说,在进程 com.ryg.chapter_2 和进程 com.ryg. chapter 2:remote 中都存在一个 UserManager 类,并且这两个类是互不干扰的,在一个进程中修改 sUserld 的值只会影响当前进程,对其他进程不会造成任何影响,这样我们就可以理解为什么在 MainActivity中修改了 sUserld 的值,但是在 SecondActivity 中 sUserld 的值却没有发生改变这个现象。

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

一般来说,使用多进程会造成如下几方面的问题:

(1)静态成员和单例模式完全失效。

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

(3)SharedPreferences 的可靠性下降。

(4)Application会多次创建。

1个问题在上面已经进行了分析。第2个问题本质上和第一个问题是类似的,既然都不是一块内存了,那么不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象。第3个问题是因为 SharedPreferences 不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,这是因为 SharedPreferences 底层是通过读/写XML文件来实现的,并发写显然是可能出问题的,甚至并发读/写都有可能出问题。

4个问题也是显而易见的,当一个组件跑在一个新的进程中的时候,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然会创建新的Application。

这个问题其实可以这么理解,运行在同一个进程中的组件是属于同一个虚拟机和同一个Application的,同理,运行在不同进程中的组件是属于两个不同的虚拟机和Application的。

为了更加清晰地展示这一点,下面我们来做一个测试,首先在 Application 的 onCreate 方法中打印出当前进程的名字,然后连续启动三个同一个应用内但属于不同进程的 Activity ,按照期望,Application onCreate 应该执行三次并打印出三次进程名不同的 log,代码如下所示。

运行后看一下 log,如图2-4所示。通过 log可以看出,Application 执行了三次 onCreate,并且每次的进程名称和进程 id 都不一样,它们的进程名和我们为 Activity 指定的 android:process 属性一致。这也就证实了在多进程模式中,不同进程的组件的确会拥有独立的虚拟机、Application以及内存空间,这会给实际的开发带来很多困扰,是尤其需要注意的。或者我们也可以这么理解同一个应用间的多进程:它就相当于两个不同的应用釆用了 SharedUID 的模式,这样能够更加直接地理解多进程模式的本质。

 

本节我们分析了多进程所带来的问题,但是我们不能因为多进程有很多问题就不去正视它。为了解决这个问题,系统提供了很多跨进程通信方法,虽然说不能直接地共享内存,但是通过跨进程通信我们还是可以实现数据交互。

实现跨进程通信的方式很多,比如通过 Intent 来传递数据,共享文件和SharedPreferences,基于 Binder 的 Messenger 和 AIDL 以及 Socket等,但是为了更好地理解各种 IPC 方式,我们需要先熟悉一些基础概念,比如序列化相关的 Serializable 和 Parcelable 接口,以及Binder的概念,熟悉完这些基础概念以后,再去理解各种 IPC 方式就比较简单了。

 

2.3 IPC基础概念介绍

IPC基础概念:Serializable接口、 Parcelable接口以及Binder

本节主要介绍 IPC 中的一些基础概念,主要包含三方面内容:Serializable接口、 Parcelable接口以及Binder,只有熟悉这三方面的内容后,我们才能更好地理解跨进程通信的各种方式。Serializable 和 Parcelable 接口可以完成对象的序列化过程,当我们需要通过 Intent Binder 传输数据时就需要使用 Parcelable 或者 Serializable 。还有的时候我们需要把对象持久化到存储设备上或者通过网络传输给其他客户端,这个时候也需要使用 Serializable 来完成对象的持久化。

下面先介绍如何使用 Serializable 来完成对象的序列化。

2.3.1 Serializable 接口

Serializable 是 Java 所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable来实现序列化相当简单,只需要在类的声明中指定一个类似下面的标识即可自动实现默认的序列化过程。

private static final long serialVersionUID = 8711368828010083044L;

在 Android 中也提供了新的序列化方式,那就是 Parcelable 接口,使用 Parcelable 来实现对象的序列号,其过程要稍微复杂一些,本节先介绍 Serializable 接口。上面提到,想让一个对象实现序列化,只需要这个类实现 Serializable 接口并声明一个serialVersionUID即可,实际上,甚至这个 serialVersionUID 也不是必需的,我们不声明这个 serialVersionUID 同样也可以实现序列化,但是这将会对反序列化过程产生影响,具体什么影响后面再介绍。User类就是一个实现了 Serializable 接口的类,它是可以被序列化和反序列化的,如下所示。

public class User implements Serializable {

    private static final long serialVersionUID = 519067123721295773L;

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

 

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

        //序列化过程
        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();
        }
        //反序列化过程
        try {
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
            User newUser = (User) in.readObject();
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }

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

刚开始提到,即使不指定 serialVersionUID 也可以实现序列化,那到底要不要指定呢? 如果指定的话,serialVersionUID 后面那一长串数字又是什么含义呢?

我们要明白,系统既然提供了这个 serialVersionUID,那么它必须是有用的。这个 serialVersionUID 是用来辅助序列化和反序列化过程的,原则上序列化后的数据中的 serialVersionUID 只有和当前类的 serialVersionUID 相同才能够正常地被反序列化。

serialVersionUID 的详细工作机制是这样的:序列化的时候系统会把当前类的 serialVersionUID 写入序列化的文件中(也可能是其他中介),当反序列化的时候系统会去检测文件中的 serialVersionUID,看它是否和当前类的 serialVersionUID 一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化;否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的,因此会报如下错误:

java.io.InvalidClassException: Main; local class incompatible: stream 
classdesc serialVersionUID = 8711368828010083044, local class serial-VersionUID = 8711368828010083043。

一般来说,我们应该手动指定 serialVersionUID 的值,比如1L,也可以让 Eclipse 根据当前类的结构自动去生成它的 hash 值,这样序列化和反序列化时两者的 serialVersionUID 是相同的,因此可以正常进行反序列化。如果不手动指定 serialVersionUID 的值,反序列化时当前类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类的 hash 值并把它赋值给 serialVersionUID,这个时候当前类的 serialVersionUID 就和序列化的数据中的 serialVersionUID 不一致,于是反序列化失败,程序就会出现crash

所以,我们可以明显感觉到 serialVersionUID 的作用,当我们手动指定了它以后,就可以在很大程度上避免反序列化过程的失败。比如当版本升级后,我们可能删除了某个成员变量也可能增加了一些新的成员变量,这个时候我们的反向序列化过程仍然能够成功,程序仍然能够最大限度地恢复数据,相反,如果不指定serialVersionUID的话,程序则会挂掉。

当然我们还要考虑另外一种情况,如果类结构发生了非常规性改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID 验证通过了,但是反序列化过程还是会失败,因为类结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构的对象。

根据上面的分析,我们可以知道,给 serialVersionUID 指定为1L或者釆用 Eclipse 根据当前类结构去生成的 hash 值,这两者并没有本质区别,效果完全一样。以下两点需要特别提一下,首先静态成员变量属于类不属于对象,所以不会参与序列化过程;其次用 transient 关键字标记的成员变量不参与序列化过程。

另外,系统的默认序列化过程也是可以改变的,通过实现如下两个方法即可重写系统默认的序列化和反序列化过程,具体怎么去重写这两个方法就是很简单的事了,这里就不再详细介绍了,毕竟这不是本章的重点,而且大部分情况下我们不需要重写这两个方法。

private void writeObject ( java.io.ObjectOutputStream out ) throws IOException {

       // write 'this' 1 to 'out'...

}

private void readObject ( java.io.ObjectInputStream in ) throws lOException, ClassNotFoundException {

     // populate the fields of 'this * from the data in 'in'...

}

 

2.3.2 Parcelable 接口

上一节我们介绍了通过 Serializable 方式来实现序列化的方法,本节接着介绍另一种序列化方式:Parcelable。Parcelable 也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并可以通过 Intent 和 Binder 传递。下面的示例是一个典型的用法。

public class User implements Parcelable {

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

    public Book book;

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


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


    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeInt(userId);
        out.writeString(userName);
        out.writeInt(isMale ? 1 : 0);
        out.writeParcelable(book, 0);
    }


   public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
        @Override
        public User createFromParcel(Parcel in) {
            return new User(in);
        }

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


   private User(Parcel in) {
       userId = in.readInt();
       userName = in.readString();
       isMale = in.readInt() == 1;
       book = in.readParcelable(Thread.currentThread().getContextClass-Loader());
    }

}

这里先说一下 Parcel,Parce l内部包装了可序列化的数据,可以在 Binder 中自由传输。 从上述代码中可以看出,在序列化过程中需要实现的功能有序列化、反序列化和内容描述。 序列化功能由 writeToParcel 方法来完成,最终是通过 Parcel 中的一系列 write 方法来完成的;反序列化功能由 CREATOR 来完成,其内部标明了如何创建序列化对象和数组,并通过 Parcel 的一系列 read 方法来完成反序列化过程;内容描述功能由 describeContents 方法来完成,几乎在所有情况下这个方法都应该返回 0,仅当当前对象中存在文件描述符时,此方法返回 1。需要注意的是,在 User(Parcel in) 方法中,由于 book 是另一个可序列化对象,所以它的反序列化过程需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。 详细的方法说明请参看表2-1

Android开发艺术探索知识回顾——第2章 IPC机制:1、IPC 的基础知识_第3张图片

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

既然 Parcelable 和 Serializable 都能实现序列化并且都可用于 Intent 间的数据传递,那么二者该如何选取呢?

Serializable 是 Java 中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量 I/O 操作。而 Parcelable Android 中的序列化方式,因此更适合用在Android平台上,它的缺点就是使用起来稍微麻烦点,但是它的效率很高,这是 Android 推荐的序列化方式,因此我们要首选 Parcelableo。Parcelable 主要用在内存序列化上,通过 Parcelable 将对象序列化到存储设备中或者将对象序列化后通过网络传输也都是可以的,但是这个过程会稍显复杂,因此在这两种情况下建议大家使用 Serializable以上就是 Parcelable Serializable 的区别。

2.3.3 Binder

Binder 是一个很深入的话题,笔者也看过一些别人写的 Binder 相关的文章,发现很少有人能把它介绍清楚,不是深入代码细节不能自拔,就是长篇大论不知所云,看完后都是晕晕的感觉。所以,本节笔者不打算深入探讨Binder的底层细节,因为Binder太复杂了。 本节的侧重点是介绍 Binder 的使用以及上层原理,为接下来的几节内容做铺垫。

直观来说,Binder 是 Android 中的一个类,它实现了 IBinder 接口。从 IPC 角度来说,BinderAndroid中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是 /dev/binder,该通信方式在 Linux 中没有;

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

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

Android开发中,Binder主要用在Service中,包括 AIDL 和 Messenger。其中普通 Service 中的 Binder 不涉及进程间通信,所以较为简单,无法触及 Binder 的核心,而 Messenger 的底层其实是 AIDL,所以这里选择用 AIDL 来分析 Binder 的工作机制。为了分析 Binder 的 工作机制,我们需要新建一个 AIDL 示例,SDK 会自动为我们生产 AIDL 所对应的 Binder 类,然后我们就可以分析 Binder 的工作过程。还是釆用本章开始时用的例子,新建 Java com.ryg.chapter_2.aidl,然后新建三个文件 Book.java,Book.aidl 和 IBookManager.aidl,代 码如下所示。

 

上面三个文件中,Book.java 是一个表示图书信息的类,它实现了 Parcelable接口。Book.aidl 是 Book 类在 AIDL 中的声明。IBookManager.aidl 是我们定义的一个接口,里面有两个方法:getBookList 和 addBook,其中 getBookList 用于从远程服务端获取图书列表,而 addBook 用于往图书列表中添加一本书,当然这两个方法主要是示例用,不一定要有实际意义。

我们可以看到,尽管 Book 类已经和 IBookManager 位于相同的包中,但是在 IBookManager 中仍然要导入 Book 类,这就是 AIDL 的特殊之处。下面我们先看一下系统为 IBookManager.aidl 生产的 Binder 类,在 gen 目录下的 com.ryg.chapter_2.aidl 包中有一个 IBookManager.java 的类,这就是我们要找的类。接下来我们需要根据这个系统生成的 Binder 类来分析 Binder 的工作原理,代码如下:

 

上述代码是系统生成的,为了方便査看笔者稍微做了一下格式上的调整。在 gen 目录下,可以看到根据 IBookManager.aidl 系统为我们生成了 IBookManager.java 这个类,它继承了 IInterface 这个接口,同时它自己也还是个接口,所有可以在 Binder 中传输的接口都需要继承IInterface接口。这个类刚开始看起来逻辑混乱,但是实际上还是很清晰的,通过它我们可以清楚地了解到 Binder 的工作机制。

这个类的结构其实很简单,首先,它声明了两个方法 getBookList 和 addBook,显然这就是我们在 IBookManager.aidl 中所声明的方法,同时它还声明了两个整型的 id 分别用于标识这两个方法,这两个 id 用于标识在 transact 过程中客户端所请求的到底是哪个方法。

接着,它声明了一个内部类 Stub,这个 Stub 就是一个 Binder 类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact 过程,而当两者位于不同进程时,方法调用需要走 transact 过程,这个逻辑由 Stub 的内部代理类 Proxy 来完成。这么来看,IBookManager 这个接口的确很简单,但是我们也应该认识到,这个接口的核心实现就是它的内部类 Stub 和 Stub 的内部代理类Proxy,下面详细介绍针对这两个类的每个方法的含义。

DESCRIPTOR

Binder 的唯一标识,一般用当前 Binder 的类名表示,比如本例中的 "com.ryg.chapter_2.aidl.IBookManager "。

asInterface(android.os.IBinder obj)

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

asBinder

此方法用于返回当前 Binder 对象。

onTransact

这个方法运行在服务端中的 Binder 线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。该方法的原型为 public Boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) 。服务端通过 code 可以确定客户 端所请求的目标方法是什么,接着从 data 中取岀目标方法所需的参数(如果目标方法有参 数的话),然后执行目标方法。当目标方法执行完毕后,就向 reply 中写入返回值(如果目标方法有返回值的话),onTransact 方法的执行过程就是这样的。需要注意的是,如果此方 法返回 false,那么客户端的请求会失败,因此我们可以利用这个特性来做权限验证,毕竟我们也不希望随便一个进程都能远程调用我们的服务。

Proxy#getBookList

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

Proxy#addBook

这个方法运行在客户端,它的执行过程和 getBookList 是一样的,addBook 没有返回值,所以它不需要从 _reply 中取出返回值。

通过上面的分析,读者应该已经了解了 Binder 的工作机制,但是有两点还是需要额外说明一下:首先,当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在 UI 线程中发起此远程请求;其次,由于服务端的 Binder 方法运行在 Binder 的线程池中,所以 Binder 方法不管是否耗时都应该采用同步的方式去实现,因为它己经运行在一个线程中了。为了更好地说明Binder,下面给出一个 Binder 的工作机制图,如图2-5所示。

Android开发艺术探索知识回顾——第2章 IPC机制:1、IPC 的基础知识_第4张图片

从上述分析过程来看,我们完全可以不提供 AIDL 文件即可实现 Binder,之所以提供 AIDL 文件,是为了方便系统为我们生成代码。系统根据 AIDL 文件生成 Java 文件的格式是固定的,我们可以抛开 AIDL 文件直接写一个Binder出来,接下来我们就介绍如何手动写一个Binder,还是上面的例子,但是这次我们不提供 AIDL 文件。

参考上面系统自动生成的 IBookManager.java 这个类的代码,可以发现这个类是相当有规律的,根据它的特点,我们完全可以自己写一个和它一模一样的类出来,然后这个不借助 AIDL 文件的 Binder 就完成了。但是我们发现系统生成的类看起来结构不清晰,我们想试着对它进行结构上的调整,可以发现这个类主要由两部分组成,首先它本身是一个 Binder 的接口(继承了IInterface),其次它的内部有个 Stub类,这个类就是个 Binder还记得我们怎么写一个 Binder 的服务端吗?代码如下所示。

 

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

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

(1)声明一个 AIDL 性质的接口,只需要继承 IInterface 接口即可,IInterface 接口中只有一个 asBinder 方法。这个接口的实现如下:

 

可以看到,在接口中声明了一个 Binder 描述符和另外两个 id,这两个 id 分别表示的是 getBookList 和 addBook 方法,这段代码原本也是系统生成的,我们仿照系统生成的规则去手动书写这部分代码。如果我们有三个方法,应该怎么做呢?很显然,我们要再声明一个 id,然后按照固定模式声明这个新方法即可,这个比较好理解,不再多说。

(2)实现 Stub 类和 Stub 类中的 Proxy 代理类,这段代码我们可以自己写,但是写出来后会发现和系统自动生成的代码是一样的,因此这个 Stub 类我们只需要参考系统生成的代码即可,只是结构上需要做一下调整,调整后的代码如下所示。

 

 

通过将上述代码和系统生成的代码对比,可以发现简直是一模一样的。也许有人会问:既然和系统生成的一模一样,那我们为什么要手动去写呢?我们在实际开发中完全可以通过 AIDL 文件让系统去自动生成,手动去写的意义在于可以让我们更加理解 Binder 的工作 原理,同时也提供了一种不通过 AIDL 文件来实现 Binder 的新方式。

也就是说,AIDL 文件并不是实现 Binder 的必需品。如果是我们手写的 Binder,那么在服务端只需要创建一个 BookManagerlmpl 的对象并在 Service 的 onBind 方法中返回即可。最后,是否手动实现 Binder 没有本质区别,二者的工作原理完全一样,AIDL 文件的本质是系统为我们提供了一种快速实现 Binder 的工具,仅此而已。

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

为了解决这个问题,Binder 中提供了两个配对的方法 linkToDeath 和 unlinkToDeath,通过 linkToDeath我们可以给 Binder 设置一个死亡代理,当 Binder 死亡时,我们就会收到通知,这个时候我们就可以重新发起连接请求从而恢复连接。那么到底如何给Binder设置死亡代理呢?也很简单。

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

其次,在客户端绑定远程服务成功后,给 binder 设置死亡代理:

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

到这里,IPC 的基础知识就介绍完毕了,下面开始进入正题,直面形形色色的进程间通信方式。

 

 

整理中。。。

 

 

 

 

 

你可能感兴趣的:(Android开发艺术探索知识回顾——第2章 IPC机制:1、IPC 的基础知识)