谈谈IPC

最近在看艺术开发探索,思来想去还是想把这个部分的东西进行一个记录,虽然没有理解为什么要将这个内容放置在第二章,但是本着存在即合理的本质,它的存在一定有它自己的道理。所以思考再三还是进行一个记录对日后的自己进行一个启发。
可能有一些偏差性的错误,有的话劳烦指出,不胜感激。

可能当我们看见IPC这个单词的时候都百思不得其解,指它具体是什么,干什么用的,是怎么实现的。那么我们开始谈谈的话,也从这些地方开始吧。
自然,因为自己的技术原因,不会过分深究太过于底层的实现,仅仅参考《Android开发艺术探索》和部分博文来对于这一部分进行说明。

初识

是什么

在这里可以很直白的告诉你IPC,就是跨进程通信的意思。
更具体的一点,就是指两个进程交换数据的过程。
那这个时候问题就来了,进程是什么?为什么要跨进程进行通信?
那么,提及进程,可能有人就会联想到线程,但是其实这两个并不是同一个东西。

  • 进程:
    你可以将它理解为是每一个应用/程序占用的内存空间,正式地来说叫做一个执行单元,就可以将它类比成一个生产零件的车间。
  • 线程:
    你可以将它理解成是每一个对应的内存空间之内,每次要进行一些操作的时候使用的一个东西,正式地解释是CPU的最小执行单元。
    其实这个就可以类比成车间内不同的车间工人,他们每个人身上可能都背负着不同的任务,但是有时候又有几个人协同完成一个工作内容的情况。
    当然这个比喻也可以应用到线程的其他部分里面去,在这里就不进行展开了。

这样兴许就能够分辨清楚两者之间的差异,线程是包含在每一个不同的进程中的,而每一个进程能够通过自身的一些需求来对不同的线程进行调度。
放在Android之内,最明显的例子就是每一个不同的App应用就对应着不同的进程,而每一个App内的UI线程,以及后台进行计算或者是其他操作的线程都是包括在一个进程内。
那么就可以说通过进程,将不同内容的应用进行切割、分离,让用户能够更加清晰地了解到这两个东西不是同一个App,而线程就是为了展现不同进程之中的内容,而服务于进程。

为什么

那么这个时候就有人想问了,为什么我们要使用IPC呢?
那这样吧,首先讲讲在什么常见的情况之下使用IPC,你就知道我们为什么要使用IPC了,啊,不过这些情景不一定在Android环境下的,只是提出为了更好理解,为什么我们创造并使用了它。

  • Windows:剪贴板
  • Linux:数据传递

其实想想也是情有可原,不同的应用在不同的进程,但是数据通信在某些时候也是必要的,所以说IPC也就这么产生了。

但是对于Android开发者来说,IPC大部分用于以下情况

  • 分担主进程过重的加载任务
    例如:大量图片加载(易OOM),读取一大段数据存入数据,加载Flash,频繁绘制一个页面,视频播放等…
  • 将进程crash对于用户的影响降到最低
    在前些日子看见的文章里面,发现QQ音乐的开发也是才用了多进程的方式,这样播放音乐的进程不会因为其他进程在交互过程中出现了错误产生crash,进而导致进程的关闭,影响用户体验。
  • 一个App需要源自于其他App的对应数据
    在这个情况下,我们一般使用ContentProvider,但是其实它底层的实现还是跟跨进程通信相关,只不过这个东西被Android封装了,我们使用的过程之中感知不到一个跨进程通信的过程而已。关于它的具体描述在之后会提及。

这样的解释之下兴许能够了解它是在什么范围内进行使用的了。

怎么做

现在,兴许大家对它有了那么一点模糊的概念了,可能就想上手尝试一下这个东西用起来具体是个什么样子。
那从两个部分开始讲述:如何在自己的App中开启另外一个线程,以及如何进行跨进程通信:

  • 在开启另外一个进程
    首先我们需要知道的是,在Android里面只能够通过在AndroidMenifest文件里面,通过对四大组件声明android:process=以表示对应的四大组件在使用的时候是在另外一个新的进程之中进行的。
    啊,是的,你没有看错,假如说多进程运行这个App,只能够通过这个方式来执行。

  • 如何在Android之中进行跨进程通信
    那么对于跨进程通信来说,就是利用一下不同的方式来完成跨进程通信了,其中可能会有优势,有劣势。

    • Bundle
    • Messenger
    • 文件共享
    • ContentProvider
    • Socket
    • AIDL

如何开启一个进程

在之前我们就提到了,在Android里面开启另外一个新的进程只能够通过在AndroidMenifest文件里面,通过对四大组件声明android:process=xxx来表示该组件在使用的时候是在对应的另外一个进程里面。

android:process

这个属性可谓是打开了多进程的大门,但是这个时候你可能又有问题了,这个对应的属性值又代表什么呢?其实就是这个进程对应的进程名。
譬如说

1
2
android:process=":remote"
android:process="com.cynthia.demo.remote"

在这里面就是分别声明了两个进程,一个是com.cynthia.demo:remote,一个是com.cynthia.demo.remote,这个时候你可能又会疑惑了,为什么一种这种表达方式可以简写呢?
主要是:这个符号含义就是要在当前的进程名前面附加上当前的包名,所以有这样的效果。当然,这个符号不仅仅也只有这一个作用,它同时也表示它本身是一个私有进程。
假如说没有这个符号的话,它就属于全局进程。当然,名字不能偷懒简写了。

那么第二个问题来了,私有进程和全局进程又是什么?

  • 私有进程
    仅仅只能够在当前应用之中进行工作,其他应用假如说想跟这个部分共享同一个进程,显然是不能的。
  • 全局进程
    其他的应用在进行一些特殊的操作之后可以跟这个部分共享同一个进程。

在这里,共享进程前提是他们的shareUID和签名相同,假若两者同时运行在同一个进程中,这两个应用之间就可以相互访问对方的私有数据(例如data目录,组件信息,共享内存数据)。在两者没有同时运行在同一个进程中的时候,也可以共享data目录和组件信息。
那么,shareUID又是什么呢,它是Android对每个App会分配的唯一的一个id
这个id的存在就是为了该应用的文件设置权限只对该用户好应用自身可见。保证了文件的安全性。

但是这样虽然是说开启了多进程的大门,但是多进程真的只是这个样子吗?

啊,显然并不是这个样子。

使用多进程会造成的问题

在开始讲述这部分内容之前,我们得知道Android对于每个不同的应用(也可以说是进程)分配一个独立的虚拟机进行对应的操作。自然,不同的虚拟机在内存的分配上面有不同的地址空间。
那再这样的概述之下,兴许聪明的你已经猜到问题了。

不同的虚拟机在访问同一个类信息的时候会产生多个副本

这样就会导致,不同的进程之中数据不同步的情况。相对应的,你可以将其理解成线程之中没有加同步锁然后进行数据改写的情况。这样显然会导致我们的业务要求会在跳转到不同的进程的时候拿的只是一个数据的副本,原进程也无法感知到新进程数据的修改,导致实际的差距与理论差距甚远。
那么你会说,单例模式/静态成员了解一下?
其实在这样的环境之下,同样也是失效的。
按照之前的解释来说,不同的进程会分配不同的地址空间,一开始运行的时候是将原进程的数据拷贝到自己对应的进程对应的内存,从根本上来说这个东西都不已经属于之前的那一个内存的,什么都是全新的。

那么在这样的情况之下就会有以下的问题发生

  • 静态成员和单例模式完全失效
  • 线程同步机制失效
  • Application会多次创建
  • SharedPreferences的可靠性下降

前三个其实原理跟先前解释得原因相近,第三条无非是因为每次开启一个进程相当于开启一个新的应用程序,伴随着新的“应用程序”的开启,Application也会再次创建。

那么针对第四个问题的话,来源是关于SharePreferences本身,因为这个在官方文档里面是这么说的:

Note: This class does not support use across multiple processes.

本身SharePreferences就不能够适用于跨进程通信。因为它自己实现数据的存储和读取是通过写入和读取XML文件夹来实现的。同时,在操作它的时候应用的内存会对其对应的键值对进行缓存,这样在进程之中对其进行并发的读写的操作的时候导致其内容有一定的概率丢失。或者是说读取的数据并不是最新的数据,可能是上一代,或者上上代的。

但是又有人说,不是有一个MODE_MULTI_PROCESS的Flag吗?啊,对于这个官方的解释是这样的。

@deprecated MODE_MULTI_PROCESS does not work reliably in some versions of Android, and furthermore does not provide any mechanism for reconciling concurrent modifications across processes. Applications should not attempt to use it. Instead, they should use an explicit cross-process data management approach such as {@link android.content.ContentProvider ContentProvider}.

大概翻译一下的话,意思就是这个Flag在Android的某些版本并不可靠,并且未来也不会在对其提供支持,如果要跨进程对数据进行传输的话还是推荐ContentProvider

所以说SharePreferences在跨进程数据传递可靠性还是会降低的。在实际的开发之中如果要多进程进行数据读取或者存储需要考虑其他的方式。

如何在Android之中进行跨进程通信

在刚才,我们知道了多进程的打开方式,但是同时我们也知道了这其中也有很多的问题。自然,每个进程之间也是需要进行通信的,那我们怎么进行通信呢?
在前面,我们已经大概列出来了几个方式,在现在,就开始慢慢进行一定的讲解。

Bundle

我们现在知道的是,四大组件中的三大组件(Activity、Service、Receiver)都是支持在Intent之中传递Bundle数据的。那我们为什么说Bundle可以支持跨进程传输的呢?
因为Bundle是声明了Parcelable接口的一个类,可以支持序列化和反序列化。

那么在这里,我们就先讲一下ParcelableSerializable这两个接口,以便于对于之后有更好的理解。

Serializable

它自己本身只是Java自己提供的一个反序列化的接口,当我们点入源码的时候才发现它其实是一个空接口,相当于只是一个说可以进行序列化的标志。
而序列化就标志着这个东西可以跨进程进行传输了。
在其中,有些时候需要自己指定一个serialVersionUID(或者让Java自己给你生成一个)来进行表示。那么这个值的存在意义是什么呢?

这个值是一个标识值,是在每次进行序列化的时候系统会写入文件的一个值,在进行反序列化的时候会将当前的这个值与写入文件的值进行比对,假若说两者相等,才能够进行反序列化的操作。不然会报错。
但是就是自己指定的值跟Java自己默认生成的值又有什么差异呢?
默认生成的值会跟随这个类里面的成员变量和方法的变化而更新这个值。
但是说假如自己指定的话,就会一定程度上避免了在序列化之后更改了成员变量/方法,反序列化失败这样的事情发生。

当然,假如说类结构已经发生了毁灭性的改变的话(例如修改类名,修改成员变量的类型等),即便是ID验证通过了,反序列化仍旧是失败的。

自然,我们还需要注意的是,静态成员变量和transient关键字修饰的变量不参与序列化过程。

所以说我们自己实现这个接口的时候最好声明serialVersionUID 防止在之后对类进行了非毁灭性的结构改变的时候还能够正常的恢复与写入数据。

Parcelable

Parcelable接口是Android这边给进行序列化提供的一个接口,这个跟Serializable接口相比,实现就会复杂很多,示例大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class User implements Parcelable {
private int id;
private String name;

// 省略了对外的getter和setter方法

//从Parcel容器之中读取数据,反序列化过程
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}

// 创建指定长度的原始对象数组
@Override
public User[] newArray(int size) {
return new User[size];
}
};

private User(Parcel in) {
id = in.readInt();
name = in.readString();
}

//返回当前对象的内容描述,如果有文件描述符,返回1
//但是基本上都是返回0
@Override
public int describeContents() {
return 0;
}

/* 
序列化过程,将当前的信息写入Parcel之中
第二个为标值,只有0与1的情况
当flags为1时,表明当前对象需要作为返回值返回,不能能立刻释放资源,
但是flags基本上就只有0的情况
*/
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
}
}

在声明了这个接口的类,都要复写上面展示的这几个方法,以便完成对应的序列化以及反序列化的过程。在整个过程之中,Parcel这个类包装了可序列化的数据,而每个需要实现序列化的类都是通过Parcel的一系列方法完成的,例如说readwrite
我们还需要注意的一种情况是,假若说在声明了Parcelable的接口里面还含有声明了可序列化的接口的其他自定义类,在序列化以及反序列化的过程之中都有一些差异。
在这里,就拿UserCar来做例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Car implements Parcelable {
private int id;
private String name;
private User user;

//省略了其他需要声明的方法,只留下了不同的部分。

private Car(Parcel in) {
id = in.readInt();
name = in.readString();
user = in.readParcelable(User.class.getClassLoader());
}

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

在其中就可以发现一个可序列化对象在反序列化过程的时候需要传递一个User.class.getClassLoader(),这个是表明了当前的类加载器,否则在进行加载的时候会报错。

但是这样又有一个问题了,既然SerializableParcelable这两个接口都是为了序列化,那么这两个又有什么区别呢?

  • Serializable
    这个是Java自带的序列化接口,但是效率不高。
    缘由是声明接口很简单,但是里面进行了大量的I/O操作,导致了序列化和反序列化效率的降低,但是该接口针对数据持久化操作是稳定的。例如说保存到本地以及进行网络传输。
  • Parcelable
    这个是为了优化Serializable在内存传输过程中效率过慢,Android进行开发的一个接口,但是这个也只是适合于内存之中的序列化操作。
    因为android的不同版本也可能导致Parcelable的不同,导致在序列化过程之中有一定不如人意的情况出现,所以不推荐利用该接口进行数据持久化

好的,现在讲完了这两个接口之后,兴许就能够理解为什么Bundle能够传递数据了,因为它自己本身就声明了Parcelable接口,然后借助Intent就能够在内存里面进行序列化和反序列化。

当然,Bundle之中传递的对象也需要能够进行序列化的,不能够利用Bundle进行传输的东西在该类源码之中已经列出,各位有兴趣的可以看看源码。

文件共享

这个方式的原理就是两个不用的进程对于相同的文件进行并读写操作来达到不同进程之间的数据同步。因为Android是基于Linux进行开发的,所以说Android对于文件的并发读写是没有限制的。
在文件共享里面,还是通过Serializable接口进行序列化,然后写入文件之中。
而其中主要运用ObjectInputStreamObjectOutputStream来进行具体的操作。

这种方式在跨进程交换信息的时候非常方便,不过,这个方式在交换的数据格式是没有要求的,需要交换数据的双方来约定好具体的内容格式。同时,需要注意并发读写的问题,因为假如说并发读写的话,到时候读取的数据不一定最新的数据。所以要使用这种方式来进行IPC的话需要注意一下并发读写可能会产生的问题。
自然,假若说你要将它放置于一个数据同步要求不高的场景之中,也是完全可以的。

Messenger/AIDL

为什么要把这两个东西放在一起呢,因为Messenger的底层实现就是AIDL,只是Android对于它做了一些简单的封装以便于我们更加方便的调用这个东西而已。
而且这两个东西的实现都依赖于Binder的存在,从跨进程通信的角度来说,它就是一个实现跨进程通信的一种方式。而从Android的应用层来说,它就是服务端与客户端进行通信的的一个媒介,在bindSerice的时候,就会返回有一个拥有服务端相对应业务的Binder,客户端就能够基于这个Binder来与服务器端进行通信,获得需要的数据以及服务。自然,这里的服务就包含普通服务和跨进程服务。

因为Messenger对于其封装得太好了,可能在讲解的时候一些原始的东西都不能够看出来,所以说从AIDL入手,通过AIDL来讲解Binder运作的具体核心。
当然,因为Binder底层的工作原理还是比较难以理解的,所以说这里只是从应用层面开始入手,了解Binder是如何使用来完成跨进程通信的即可
嗯,使用还是使用之前的User类,然后我们需要新建两个AIDL文件,这个在Android Studio之中还是比较容易的,在这里,我们需要新建一个UserIUserManager,自然,后缀是.aidl

首先是User.aidl

1
2
3
4
// User.aidl
package com.cynthia.demo;

parcelable User;

在这里我们需要注意的是这里需要声明一个parcelable的变量User,代表了它在AIDL之中的声明。(这里注意一下,之前的存放bean类的AIDL文件名字必须跟bean类名字相同,不然在之后引入到下一个文件之中,编译会有错误)
同理,所有自己声明了Parcelable接口的对象,如果要在AIDL之中使用,就需要建立对应的文件进行声明。

其次是IUserManager.aidl

1
2
3
4
5
6
7
8
9
10
// IUserManager.aidl
package com.cynthia.demo;

import com.cynthia.demo.User;

interface IUserManager {

void addUser(in User user);
List<User> getUsers();
}

我们关注到,在之前首先引入了之前声明的User类,是因为AIDL之中,假若你是用的类不是默认实现了序列化的类,都需要通过import的形式将东西进行引入,不然会找不到。AIDL文件并不是支持每一种数据类型的。能够支持的如下:

  • 基本数据类型
  • String/CharSequence
  • List (ArrayList),里面的元素也要能够被AIDL支持
  • Map (HashMap),key和value也要能够被AIDL支持
  • 所有实现了Parcelable的对象
  • AIDL接口本身能够在AIDL文件使用

尤其是后两个,在使用的时候要像这里的User类一样,显式import进来。

在AIDL之中,非Android自己默认实现的可序列化的类,作为参数传递进入需要添加in/out/inout关键字,这个是一个定向的tag,代表的是跨进程通信之中数据的流向:

  • in:只能够从客户端流向服务器端
  • out:只能够从服务器端流向客户端
  • inout:可以双向流通

这个流通又代表着什么呢?可以理解为一种权限。

  • in:客户端可以发送信息给服务器端,但是服务器端更改传递过来的对象参数不会影响到客户端本身的对象
  • out:假如说客户端发送数据给服务器端,服务器端只能够接收到一个空对象,但是假如说服务器端对于这个空对象有改动,客户端的对象能够对应地进行变动
  • inout:服务器端能够接收到完整的信息,客户端也能够跟着服务器端数据的变动而进行变动

在返回值之中,则需要注意这前面不添加任何的修饰符
然后进行编译之后,就能够在gengeratedJava的对应包名下找到对应的系统生成的类(这个是用Android项目模式作为目录层级引导才有的文件夹,假如是Project的话在app->build->generated->source->aidl->debug->包名 之中找到对应的文件)

然后我们就能够通过这个类来具体了解一下Binder的工作原理了,这个部分的代码有点长,然后我也根据它自己生成的东西进行了一定的代码调整。讲解的话我们会具体拆分成几个部分来进行讲解。

首先先忽略内部类里面的具体的代码,仅仅只是看最外围的东西。

1
2
3
4
5
6
7
8
9
10
11
public interface IUserManager extends android.os.IInterface {

public void addUser(com.cynthia.demo.User user)
throws android.os.RemoteException;

public java.util.List<com.cynthia.demo.User> getUsers()
throws android.os.RemoteException;

public static abstract class Stub extends android.os.Binder
implements com.cynthia.demo.IUserManager {...}
}

我们能够发现这个对应生成的类本体还是一个接口,只是继承了一个IInterface的类,这个类的具体的作用我们先不管,然后就能够看见下面是我们之前在aidl之中声明的两个方法,声明方法的同时也指出了对应的抛出异常。
之后在其中声明了一个内部类Stub,我们能够发现这个类是继承了Binder的,诶,重头戏来了。所以说我们探究原理的时候首先还是从这里面开始探索。

之后只看里面的内部类的部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static abstract class Stub extends android.os.Binder 
implements com.cynthia.demo.IUserManager {

private static final java.lang.String DESCRIPTOR = "com.cynthia.demo.IUserManager";

static final int TRANSACTION_addUser = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_getUsers = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);

public Stub() {
this.attachInterface(this, DESCRIPTOR);
}

public static com.cynthia.demo.IUserManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.cynthia.demo.IUserManager))) {
return ((com.cynthia.demo.IUserManager) iin);
}
return new com.cynthia.demo.IUserManager.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 {...}

private static class Proxy implements com.cynthia.demo.IUserManager {...}
}

省去一个核心代码块和一个内部类Proxy,我们现在开始一点点的开始讲解Stub。

关于Stub的概述的话就是Binder的一个载体,同时假如说没有进行跨进程的请求的话,是这个类本身作为媒介,否则就是稍后进行讲解的Stub.Proxy的内部类。
(下面的方法省略对应的传入参数和返回值)

  • DESCRIPTOR
    这个是Binder之间的唯一标识,一般使用声明这个aidl接口的包名来进行标识
  • TRANSACTION_xxx
    方法名标识,后面一般跟的是方法声明的名字,这个是用于之后的onTransact方法,在这里不过多讲述
  • Stub()
    显然,只是一个构造方法,但是我们发现里面调用了attachInterface,传入了本身和前面提及的DESCRIPTOR,这个就相当于是一种Binder和描述符的一种绑定,方便以后的调用
  • asInterface(…)
    这个方法是把原来的IBinder类经过不同的情况强制转换为不同的Binder对象。
    首先,假如传递过来的是空的,自然返回空的,假如说不返回空的,就通过queryLocalInterface这个方法,传入既定的描述符,来获取我们在构造这个类的时候绑定的那个Binder,之后的一个if判断语句是在判断现在服务器端以及客户端是否是在同一个进程之中的,假如在同一个进程之中,直接返回经过强制转换后的当前类
    否则就会如同我们看见的,会返回静态内部类Proxy,关于这个类,我们稍后讲解。
  • asBinder()
    这个很简单,只是返回当前的Binder对象而已。

  • onTransact(…)
    恩,这个是一个很重要的方法了,同时我们需要知道,这个是运行在Binder的服务端线程池之中发起的跨进程请求托管到该方法之中进行处理
    这个时候我们再贴个代码看看里面具体是什么情况。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    @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_addUser: {
    data.enforceInterface(descriptor);
    com.cynthia.demo.User _arg0;
    if ((0 != data.readInt())) {
    _arg0 = com.cynthia.demo.User.CREATOR.createFromParcel(data);
    } else {
    _arg0 = null;
    }
    this.addUser(_arg0);
    reply.writeNoException();
    return true;
    }
    case TRANSACTION_getUsers: {
    data.enforceInterface(descriptor);
    java.util.List<com.cynthia.demo.User> _result = this.getUsers();
    reply.writeNoException();
    reply.writeTypedList(_result);
    return true;
    }
    default: {
    return super.onTransact(code, data, reply, flags);
    }
    }
    }

首先还是对四个传入的参数和传出的参数进行讲解。

  • code
    这个作为传入的参数搭载的就是在Stub这个类里面声明的方法名标识。在这里作为一个参数进行匹配,然后在里面进行不同的方法。
  • data
    通过Parcel容器,拿到执行对应方法需要的参数
  • reply
    通过Parcel容器,在执行完方法的时候将结果写入,进行返回
  • flags
    附加操作标志,只有0和1两个取值
    • 0:双向的远程过程调用(RPC)
    • 1:单向RPC
  • 返回值
    在这里能够看见返回值是boolean,true表示请求成功,false表示请求失败
    同时,也可以利用这个返回值作一个权限验证

之后就能够看见是根据传入的code进行匹配,然后进入对应的方法进行调用这个接口之中的本拥有的方法(就是在最外层的addUser和getUsers两个方法)
不过在这里看着似乎很复杂,其实简单解释一下就是通过了Parcel这个类来获取传入的参数,然后在创建新的对象的时候是调用的Parcelable之中的CREATOR方法来创建的一个新的对象
在getUsers方法标志对应的代码块之中,我们发现在这里面又多了一个_result参数,这个就是作为一个跨进程返回结果的标志,同时需要将这个参数写入reply之中以正确回复。
writeNoException这一步只是告诉系统,我在进行这些的时候没有出错,这个意思。

如果什么都不匹配的话,就会进行默认的方法,在这里就不进行深究了。

那么接下来我们再讲讲Stub类之中的Proxy类,在前面我们知道,如果判断出来是跨进程进行的通信,Binder就会调用这个类,进行代理(毕竟Proxy的中文意思就是代理),这个时候,我们来看看这个类又具体做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static class Proxy implements com.cynthia.demo.IUserManager {
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 void addUser(com.cynthia.demo.User user) throws android.os.RemoteException {...}

@Override
public java.util.List<com.cynthia.demo.User> getUsers() throws android.os.RemoteException {...}
}

还是先不看两个核心方法,先看看这个类在创建之初做了些什么。
首先看见这个类的构造方法是传入了一个Binder的,这个Binder就是在之前进行是否跨进程判定的时候使用的那个Binder。在这里就相当于是这个代理类绑定了这个Binder,之后进行跨进程通信到Stub类之中去执行方法。
那现在我们来看看后面的两个方法,但这个具体的内容大体相似,我就只贴一个部分出来了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public java.util.List<com.cynthia.demo.User> getUsers() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.cynthia.demo.User> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getUsers, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.cynthia.demo.User.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}

首先,一开始的obtain方法就是建立了Parcel,然后我们看见在先写入了对应的标识符来匹配对应的Binder,之后调用transact传入这些对应的参数,之后就开始RPC请求(可以粗泛的理解为此时就开始了跨进程通信),同时这个时候请求的线程挂起,然后通信到对应的Stub类,调用其中的onTransact方法来进行操作,之后将内容返还。
然后之中还有一个readException方法,就能够知道在处理过程是否产生错误,如果产生了错误,就直接停止内容的读取。
最后记得需要回收资源。
又因为这个方法是有返回值的,所以需要return。
但是在void的方法之中,就没有_result这个变量和返回值了。

这样,一个系统自己生成的接口的逻辑就被理清楚了,虽然说逻辑并不复杂,但是这样也跟理清楚了Binder在跨进程通信之中的工作原理。

当然,假如说你想自己手动实现一个Binder,也是完全没有问题的,基本原理跟上面所讲述的相似,你只需要自己调整一下架构可能就差不多了。在这里因为篇幅原因就不过多的进行展开了。

自然,有连接的成分在里面,就有可能会产生非正常断开连接的现象。这个时候我们又要介绍Binder的几个方法

  • linkToDeath
    Binder与死亡代理进行绑定
  • unlinkToDeath
    解除Binder与死亡代理的绑定
  • isBinderAlive
    返回布尔值,告诉当前的Binder是否死亡

而在其中,假如说进行绑定的话,我们又要提及DeathRecipient,这其中有一个binderDied方法,在Binder死亡的时候就会回调这个方法。这样,我们就能够在Binder死亡的时候将原来的Binder移除,并创建新的,恢复原来的连接状态。代码参考如下:

1
2
3
4
5
6
7
8
9
10
11
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void BinderDied() {
if (userManager == null){
return;
}
userManager.asBinder().unlinkToDeath(mDeathRecipient,0);
userManager = null;
// 恢复连接的操作
}
};

在服务绑定成功之后,就可以通过linkToDeath设置死亡代理了

好的,现在关于Binder的东西介绍,再开始Messenger和AIDL的话能够更加的理解他们是如何完成跨进程通信的了。
不过再最后还是要补充一下,当客户端发起远程请求的时候,由于当前请求的线程是被挂起等待回复,这是一个耗时操作,所以说不要在UI线程里面发起远程请求。
同时,因为在服务端之中的请求处理本身都是在Binder的线程池中的,所以Binder不论怎么耗时也只能够通过同步的方法去实现。

Messenger

在前文我们就已经提及,Messenger作为一种轻量级的IPC方式,其底层实现就是AIDL。这个东西在它的两个构造方法之中就能够看出来了。

1
2
3
4
5
6
7
public Messenger(Handler target) {
mTarget = target.getIMessenger();
}

public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}

我们看见了熟悉的IMessengerStub,相信各位都已经能够联想到刚刚在分析代码的时候的对应的类了吧?
自然,因为Messenger每次只处理一个请求,所以说在服务端我们不需要考虑线程同步的问题(没有并发的情况),这样下来Messenger的实现就更加简单了。

  • 服务端进程
    首先我们需要创建一个Service来处理客户端的连接请求,在里面创建一个Handler对象,通过它来声明一个Messenger对象,之后在onBind方法中返回Messenger对象底层的Binder对象就能够完成绑定了。
    当然,我们需要注意的是,假若说需要跨进程通信,需要Service在AndroidMenifest文件里面声明process属性。

  • 客户端进程
    客户端这边首先需要跟服务端对应的Service进行绑定,之后通过ServiceConnected这个类传入的Binder构建一个Messenger对象,这样这边的Messenger就能够作为Service给服务端传输信息了。
    在这里面,可以通过Bundle的方式存储你想要传递的信息,之后放入Message对象之中,再通过Messenger进行传输就可以了。

    顺便多提一句,客户端实现Service通信还是通过ServiceConnection这个类,复写了onServiceConnected方法,在这里面拿到了通信的service,之后在进行数据传递的。
    同时,在绑定的时候需要传入指向服务端Service的Intent,跟之前声明的ServiceConnect类一同作为参数传入bindService()这个方法之中。

假若说需要服务器对客户端进行反应的话,服务器端再声明一个接收消息的Messenger对象,这个是通过Handler传入的Message对象的replyTo方法获得。
同时,客户端需要额外声明一个Handler和Messenger来处理服务器反馈的信息,同时再给服务端发送信息的时候,要将接受服务器反馈的Messenger赋值给Message的replyTo属性。
这样,服务端能够通过与客户端发出信息相似的方法来回复客户端,这样就能够完成我们自己需要的通信要求了。

这样我们能够发现其实Messenger的数据传递需要将数据包裹在Message之中,通过传输Message来达到传递数据的过程。而假如说需要传递我们自定义的可序列化对象,就需要通过强大的Bundle来帮助我们完成操作了。

AIDL

在前面,我们通过了AIDL来具体了解了Binder的工作机制,也介绍了Messenger来进行IPC。那么有人问,Messenger作为一个轻量级的IPC工具,为什么我们还要使用AIDL作为IPC的一种方式呢?
在前面也提及过,Messenger每次只处理一个请求,我们可以将其理解为这个是一个串行处理请求的队列。假若说产生了大量的并发请求,Messenger的工作机制在这种场景之下就显得不大合适了;或者说我们需要跨进程调用服务端,这个时候Messenger也显得无能为力。
这个时候,我们就需要AIDL来进行工作。它的使用方式还是要分成服务端和客户端两个部分进行操作。

  • 服务端进程
    在这里,我们也需要一个Service来监听客户端的连接请求,同时在Service声明在AIDL文件之中声明的那个接口即可。

  • 客户端进程
    首先绑定Service,之后将服务端返回的Binder强转为AIDL文件之中声明的接口,就可以对应调用方法进行使用了。

啊,对了,为了方便开发,我们一般将AIDL相关的类和文件全部放入同一个包之中,这样我们每次进行相似的开发的时候,就能够直接复制相关文件过去。
同时,AIDL在服务端与客户端之间需要保持结构一致,不然在序列化和反序列化的过程之中会出现问题。

为了处理并发的问题,在Service之中,我们一般想要使用List的时候都会用CopyOnWriteArrayList进行代替,因为这个类本身就支持并发的读写,所以在处理并发的问题上面我们就能够不用太费心了。
但是在前面我们也提到了,在AIDL之中,List只支持ArrayList类型,但是CopyOnWriteArrayList并不是继承自ArrayList的。只是因为AIDL之中只支持的是一个接口List,在读取数据的时候会根据List的读取方式进行读取,最终形成一个ArrayList传递给客户端,所以说是可以接受的。同样作用的还有ConcurrentHashMap

自然,AIDL进阶的内容就是进行方法的回调,就例如说list内的user增多,这个信息需要发送给所有想知道用户是否增多的人,这个时候我们就需要方法的回调了。
表面上看着只是一个添加listener、通知listener、不需要移除listener的过程,但是这其中又有很多奥秘在其中了。

总体的实现过程的话就是在AIDL文件里面声明一个对应的接口,之后在Manager接口之中里面声明两个方法实现listener的绑定/解除。
注意,在Manager接口之中,listener接口还是需要显式import进来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// IListener.aidl
package com.cynthia.demo;

import com.cynthia.demo.User;

interface IListener {
void onNewUserArrvied(in User user);
}

// IUserManager.aidl
package com.cynthia.demo;

import com.cynthia.demo.User;
import com.cynthia.demo.IListener;

interface IUserManager {

void addUser(in User user);
List<User> getUsers();
void registerListener(IListener listener);
void unRegisterListener(IListener listener);
}

在服务器端实现方法的时候将这个listener保存通信过程中所有listener的一个list之中,每当有新的用户加入的时候,就对这个list进行遍历,通知每个listener有新的用户加入了。就基本上完成了listener的绑定以及通知操作。
但是在移除的过程之中我们会发现,在服务器端并不能够找到当前客户端对应的这个listener,那么就会解除绑定失败,但是其实我们知道这个listener还是在服务端存放listener的list之中的。
我们应该记得,客户端通过对应的方法添加listener,最终还是传递给服务端的一个保管所有listener的一个list进行的存储,那我们自然能够知晓这个listener是经过了跨进程传输的,那么基于序列化与反序列化,在服务端的listener已经是一个全新的对象了。那么在进行移除的时候进行匹配是肯定找不到的。
但是我们再换一个思路,因为传输都是通过Binder,那么每一个不同的listener应该就对应着每一个不同的Binder,在进行解除绑定的操作的时候,我们只需要找到这个当前客户端的Binder,之后在服务端寻找在之前对应传递过来的listener,就大功告成。
有人说,那这个实现起来多麻烦啊。别担心,Android之中里面有一个RemoteCallbackList,它的诞生就是专门为了处理跨进程listener解除绑定的情况。
其工作原理就是利用一个ArrayMap存储了所有的AIDL回调,每一个不同的Binder对象作为key,绑定不同的Callback对象,而这里的Callback对象就是在声明这个List的时候传入的泛型参数

1
ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();

就拿刚才的UserManager举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class UserService extends Service {

private CopyOnWriteArrayList<User> users = new CopyOnWriteArrayList<>();
private RemoteCallbackList<IListener> listeners = new RemoteCallbackList<>();

private Binder mBinder = new IUserManager.Stub() {
@Override
public void addUser(User user) throws RemoteException {
users.add(user);
}

@Override
public List<User> getUsers() throws RemoteException {
return users;
}

@Override
public void registerListener(IListener listener) throws RemoteException {
listeners.register(listener);
}

@Override
public void unRegisterListener(IListener listener) throws RemoteException {
listeners.unregister(listener);
}
};

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

// 新用户到达的操作
private void onNewUserArrvied(User user){
users.add(user);
final int N = listeners.beginBroadcast();
for (int i = 0; i < N; i++) {
IListener listener = listeners.getBroadcastItem(i);
if (listener != null){
try{
listener.onNewUserArrvied(user);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
listeners.finishBroadcast();
}
}

我们看见,RemoteCallbackList声明之后只需要通过registerunregister这两个操作就能够简单实现listener跟对应的Binder的绑定了。
同时,在这里也写出了当服务端感知到新用户到达的操作,除了添加到users这个list之外,还需要通过RemoteCallbackList来进行遍历通知,这里面的具体操作有一些差异。嗯,也就是方法上的差异,原理的话是都差不多的。

注意:在使用的时候一定要考虑到耗时操作导致请求线程挂起,如果请求线程是UI线程极容易导致ANR,所以请求的时候请放在非UI线程

再补充的一点就是前文已经提到过的Binder意外死亡的情况,除了通过Binder本身的方法进行操作,也可以在客户端的ServiceConnection之中的onServiceDisconnected方法之中设置相应的解除绑定和重新恢复Binder服务的操作。
不过这个方法跟binderDied方法是有区别的,onServiceDisconnected是在客户端的UI线程被回调,而binderDied方法是在Binder线程池之中被回调,也就是说binderDied方法之中我们不能够访问UI。

之后还有一个就是权限问题,但是在这里因为各种原因推荐看到这里的各位还是去寻找一下相关的文章,因为这个涉及到自定义权限,以及利用前面提到的onTransact方法,还有其他很多方式,在这里就不讲了。

当一个程序之中的Service过多的时候,我们其实可以考虑使用Binder连接池对其进行管理,通过不同的flag来调用不同的Binder从而实现了一个统一管理。
嗯,篇幅原因,在这里就不过多讲述了。

ContentProvider

它本身就是Android提供的一个类来支持不同程序应用之间的数据共享,就可以说它天生就支持跨进程通信,自然,它的底层实现还是使用的Binder,可见Binder在Android的IPC的核心性了。而且Android将其封装得很好,导致我们使用它的时候根本察觉不到Binder存在的痕迹。
虽然说使用它的时候还是需要关注很多细节,但是在这里只是着重讲一下它作为IPC的一种方式,是如何进行的。
作为四大组件之一,Android本身已经预置了很多的ContentProvider在其中了,假如说需要跨进程访问这些信息只需要对应实现方法即可。在这里还是主要讲解针对自定义的ContentProvider我们该如何操作。
首先声明类并继承ContentProvider,就会发现我们要复写以下六个方法。

  • onCreate()
    代表这个ContentProvider的创建,在这里面我们进行一些初始化操作
    返回布尔值,代表这个ContentProvider是否创建完毕
  • getType()
    返回Uri请求的对应的MIME类型,这个就是指媒体类型,譬如说图片,视频。
    假如说我们的应用不关注这一点,直接返回null或者"*/*"
  • quert/insert/delete/update
    这个对应数据库里面的增删改查操作

并且,除了onCreate方法是运行在主线程里面的,根据Binder的工作原理我们也能够知道,其他的对应的方法应该都是在Binder的线程池之中运行的。

首先我们得了解,ContentProvider是类似于表格的形式组织数据,在这里你可以将它类比于sql数据库,可以含有多个表,每个表的行列都对应着不同的数据。同时,ContentProvider独特的地方是它还可以存储文件的信息,不过在调取的时候需要通过返回句柄给外界,使得外界能够跟对ContentProvider进行操作。

在使用的时候自然要在AndroidMenifest进行注册,同时我们需要注意的是android:authorities是每个ContentProvider的唯一标识,用过这个属性在其他应用能够访问这个ContentProvider,推荐在声明的时候加上对应的包名。
同时为了让它独立于一个进程内进行工作,我们需要声明android:process属性,同时加上权限android:premission,假如说外部应用需要调用这个ContentProvider,就需要声明对应的权限。
自然,其中对应还有android:readPremissionandroid:writePremission,对应读属性和写属性,如果要调用的话,外部应用对应的需要声明读/写权限,不然外部应用会因为没有权限进行读取而意外终止。

关于ContentProvider内的具体操作在这里就不过多的进行讲述了。

其中还需要注意的一个地方就是,其中的query/insert/update/delete是可以存在多线程并发访问的,所以说我们在声明这里面的方法内容的时候需要注意做好线程同步。

Socket

我们知道,Socket是用于网络通信的,其中可以支持TCP通信和UDP通信,这两个通信模式的具体内容在这里就不做具体展开了。不过我们需要知道的是这两种协议方式本身就支持任意字节流在网络上的传输,所以说作为IPC方式的一种形式也是可以的。
实现的话,请注意首先要在AndroidMenifest之中声明网络权限,其次使用网络请求相关的东西不能够在UI线程上进行(为什么就不在这里赘述了)
在这里简单描述一下,一对多的模式。

  • 服务端进程
    其中代码部分跟在Java上实现相似,不过在这边外面包裹了一层Service
    在线程之中声明ServerSocket并监听一个指定的端口,之后通过一个Service是否死亡的flag来进行while循环,其中就是一旦有客户端进行连接,就在开启另外一个线程来处理该客户端发送过来的信息。当Service未死亡且客户端还能够进行相应的时候就能够进行通信了
  • 客户端进程
    在启动对应的Activity的时候,就会在onCreat()里面开启一个线程去连接对应的服务端Socket,在这里是通过指明ip地址和端口号来进行连接的。
    在这里为了避免连接不上的情况,在这里我们可以采取一个超时重连的策略,大概的意思就是连接失败之后等待一段时间之后再尝试重新连接。
    连接成功之后两遍就可以进行通信了,在Activity进行销毁的时候就需要终止循环并推出线程,并关闭保持连接的Socket。

之后就是通过分别获取Socket的输入流和输出流来获得对应传递的信息了。

当然,Socket的作用不止IPC通信,它在不同的设备之间也可以进行通信,不过就不是这里提及的范畴了。

在什么时候使用什么方式实现IPC

  • Bundle
    四大组件之间进行通信
  • 文件共享
    无并发访问,数据同步要求不高
  • Messenger
    低并发的一对多及时通信,没有RPC需求或者没有需要返回值的RPC需求。
  • AIDL
    一对多通信且有RPC需求
  • ContentProvider
    一对多进程之间的数据共享
  • Socket
    网络数据交换(一对多实时通信)