Android开发艺术-第二章 IPC 机制

时间:2022-08-10 19:44:59

2.1 Android IPC 简单介绍

IPC 意为进程间通信或者跨进程通信,线程是 CPU 调度的最小单元,是一种有限的系统资源。

进程一般指一个执行单元。不论什么操作系统都须要对应的 IPC 机制。

如 Windows 上能够通过剪切板 管道 和邮槽来进行;Linux 上能够通过命名管道 共享内容 信号量等来进行。

在 Android 中最有特色的进程间通信方式就是 Binder 了,同一时候也支持 Socket 实现随意两个终端之间的通信。

2.2 Android 中的多进程模式

(1) 通过给四大组件指定 android:process 属性,能够开启多线程模式。默认进程的进程名字是包名。

android:process=":sunquan"
android:process="cn.sunquan.com.xx"

“:”指当前的进程名前面附加上当前的包名,简写的方式。其次进程名以“:”开头的进程属于当前应用的私有进程,其它应用组件不能够跟它在同一个进程中,而进程名不以“:”开头的进程属于全局进程。其它的应用能够通过 ShareUID 方式和它跑到同一个进程中。

(2) 系统为每个应用分配一个唯一的 UID,具备同样 UID 的应用才干共享数据。两个应用通过 ShareUID 跑在同一个进程中须要有同样 ShareUID 的而且签名同样。在这样的情况下它们能够相互訪问对方的私有数据比方 data 文件夹、 组件信息等。无论它们是否跑在同一个进程中。

当然假设它们跑在同一个进程中,那么除了能共享 data 文件夹、组件信息,还能够共享内存数据,或者说它们看起来像一个应用的两个部门。

(3) Android 为每个应用分配了一个独立的虚拟机,或者说为每个进程都分配了一个独立的虚拟机。不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同虚拟机上訪问同一个类的对象会产生多份副本。所以执行在不同进程中的四大组件。仅仅要它们之间须要通过内存来共享数据,都会共享失败,这就是多进程所带来的主要影响。

(4) 多进程通常会造成例如以下几方面的问题:

1. 静态成员和单例模式全然失效:不在同一块内存

2. 线程同步机制全然失效:无论锁对象还是锁全局类都无法保证线程同步,由于不同进程锁的不是一个对象。

3. SharePreference 的可靠性下降:SharePreference 底层是通过读/写 XML 文件来实现,并发写显然可能出问题。SharePreference 不支持两个进程同一时候执行写操作,否则会有一定几率的丢失。

4. Application 会多次被创建:当一个组件跑在一个新的进程中。系统会创建新的进程同一时候分配独立的虚拟机。应用重新启动一次,会创建新的 Application。

执行在同一个进程中的组件属于同一个虚拟机和同一个 Application。不同进程的组件的确会拥有独立的虚拟机、Application 以及内存空间。

同一应用的不同组件,运作在不同的进程中,那跟它们分别属于两个应用的部门没有本质差别。

(5) 尽管不能直接共享内存可是通过跨进程通信还是能够实现数据交互。实现跨进程的方式:通过 Intent 来传递数据;共享文件;SharePreference;基于 Binder 的 Messager 和 AIDL 以及 socket。

2.3 IPC 基础概念介绍

(1) Serializable 是 Java 所提供的一个序列化接口,为对象提供标准的序列化和反序列化操作。Parceable 接口是 Android 提供的序列化方式。

(2) 实现 Serializable 接口,并声明一个 serialVersionUID 就可以让一个对象实现序列化。serialVersionUID 是一串long型的数字,用来辅助序列化和反序列化过程。

原则上序列化后的数字中的 serialVersionUID 仅仅有和当前累的 serialVersionUID 同样才干够正常的被反序列化。serialVersionUID 的具体工作机制:序列化得时候系统会把当前类的 serialVersionUID 写入序列化得文件里(也能够是其它中介),当反序列化得时候系统会检測文件里的 serialVersionUID,看它是否和当前类的 serialVersionUID 一致,如一致说明序列化的类的版本号和当前类的版本号同样。这个时候反序列化能够成功,否则说明当前类和序列化的类相比发生了某些变换,一般来说我们应该指定 serialVersionUID 的指。

注意:1.静态成员变量属于类不属于对象,不參与序列化过程;2.用transient 关键字标记的成员变量不參与序列化过程。

(3) 实现 Parceable 接口,一个类的对象能够通过实现序列化并能够通过 Intent 和 Binder 传递。Pacel 内部包装了可序列化的数据,能够在 Binder 中*传输,能够直接序列化得有 Intent、Bundle、Bitmap、List、Map等。前提是它们里面的每个元素都是可序列化的。

(4) Serializable 和 Parceable 的差别:Serializable 是 Java 中的序列化接口,其使用起来简单可是由于大量 I/O 操作而开销大。

Parceable 是 Android 中的序列化方式。更适用在 Android 平台上,缺点是使用起来麻烦,可是效率高。Parceable 主要用在内存序列上,而序列化到存储设备上或者序列化后通过网络传输则建立适用 Serializable。

(5) Binder 是 Android 中的一个类。它实现了 Binder 接口。从 IPC 角度来说,Binder 是 Android 中一种跨进程通信方式。Binder 还能够理解为一种虚拟的物理设备,设备驱动是 /dev/binder。该通信方式在 Linux 中没有;从 Android Framework 角度来说。Binder 是ServiceManager 连接各种 Manager 和对应 ManagerService 的桥梁;从 Android 应用层来说,Binder 是client和服务端进行通信的媒介。当 bindService 的时候。服务端会返回一个包含了服务端业务调用的 Binder 对象。通过这个 Binder 对象,client就能够获取服务端提供的服务或数据。这里的服务包含普通服务和基于 AIDL 的服务。

Android 开发中。Binder 主要用在 Service 中。包含 AIDL 和 Messager。当中普通 Service 中的 Binder 不涉及进程间的通信。较为简单。

而 Messager 的底层事实上也是 AIDL。

(6) aidl工具依据 aide文件自己主动生成 Java 接口的解析:声明了几个接口的方法,同一时候声明几个整型 id 来标识这几个接口,id 用来标识在 transact 过程中client请求的是哪个方法。接着会声明一个 内部类 Stub 。这个 Stub 就是一个 Binder 类。 当client和服务器位于同一个进程中。则不会走 跨进程的 transact 过程,假设不在同一个进程,方法调用须要走 transact 过程,这个逻辑有 Stub 内部代理 Proxy 来完毕。

其核心实现就是它的内部类 Stub 和 Stub 内部代理 Proxy。

其几个方法分析例如以下:

1. asInterface (android.os.Binder obj):用于将服务端的 Binder 对象转换成client所需的 AIDL 接口类型的对象。

假设client和服务端位于同一进程。那么此方法返回服务端的 Stub 对象本身,否则返回系统封装后的 Stub.proxy 对象。

2. asBinder:用于放回当前 Binder 对象

3. onTransact :执行在服务端中的 Binder 线程中,当client发起跨进程请求中,远程请求会通过系统底层封装后由此方法来处理。

该方法的原型为 public Boolean onTranact (int code, android.os.Parcel data, android.os.Parcel reply, int flags)。服务端通过 code 能够确定client所请求的目标方法是什么,接着从 data 中取出目标方法所需的參数(假设目标方法有參数的话)。然后执行目标方法。当目标方法执行完毕后。就向 reply 中写入返回值(假设目标方法有返回值的话)。

假设此方法返回 false。那么client的请求会失败,能够通过这个特性做权限验证。

4. Proxy#[method] :执行在client,当client调用此方法时,首先创建该方法所需的输入型 Parcel 对象 _data、输出型 Parcel 对象 _repley 和返回值对象。把该方法的參数信息写入 _data 中(假设有參数),然后调用 transact 方法发起 RPC(远程过程调用)请求,同一时候当前线程挂起。然后服务端的 onTransact 方法会被调用,直到 RPC 过程返回后。当前线程继续执行,并从 _reply 中取出 RPC 过程的返回结果。最后返回 _reply 中的数据。

注意:1.当client发起请求时,由于当前线程会被挂机直到服务端进程返回数据。假设远程方法耗时,那么不能在 UI 线程中发起此远程请求。2.服务端的 Binder 方法执行在 Binder 的线程池中。无论 Binder 方法是否耗时都应採用同步的方式去实现,由于执行在一个线程中。

5. AIDL 文件的本质就是系统提供一种高速实现 Binder 的工具。我们能够自己手动写。也能够通过 AIDL 文件让系统自己主动生成。

6. Binder 有两个非常重要的方法:linkToDeath 和 unlinkToDeath。Binder 执行在服务端,服务端进程由于某些原因异常终止了,服务端的 Binder 连接断裂,导致client远程调用失败。通过 linkToDeath 能够给 Binder 设置一个死亡代理,当 Binder 死亡时,会收到通知,这时能够又一次发起连接请求从而恢复连接。

设置 Binder 死亡代理例如以下:

首先声明一个 DeathRecipient 对象,DeathRecipient 是一个接口,内部仅仅有一个方法 binderDied,当 Binder 死亡时候。系统回调此方法,我们能够在移除之前绑定的 binder 代理并又一次绑定远程服务。

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (mRemoteBookManager == null) return; mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
mRemoteBookManager = null;
// TODO:这里又一次绑定远程Service
}
};

其次在client绑定远程服务成功后。给 Binder 设置死亡代理:

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

当中 linkToDeath 的第二个參数是个标记位。直接设置0就可以。此外通过 Binder 的方法 isBinderAlive 也能够推断 Binder 是否死亡。

2.4 Android 中的 IPC 方式

(1) 使用 Bunble:Bunble 实现了 Parcelable 接口,能够在不同的进程间传输。

Bunble 不支持的类型无法通过它在进程间传输。

(2) 使用文件共享:由于 Android 系统基于 Linux,并发读/写文件能够没有限制的进行,两个线程对同一个文件进行写操作都是执行,但可能出问题。

文件共享方式适合对数据同步要求不高的进程间进行通信。并要妥善处理并发读/写的问题。

SharedPreference 是一个特例,属于文件的一种。但系统对其的读/写有一定的缓存策略,即在内存中会有一份 SharedPreference 文件的缓存,因此在多进程下,系统对它的读/写变得不可靠,面对高并发的读/写訪问。SharedPreference 有非常大几率丢失数据。所以不建议在进程间通信中使用 SharedPreference。

(3) 使用 Messenger:Messenger 是一种轻量级的 IPC 方案,其底层实现是 AIDL,以串行方式处理client发来的消息,其服务端一次处理一个请求。不存在并发执行的情形(对于有大量并发请求,Messenger 就不合适适用)。

(4) 使用 AIDL :首先服务端创建一个 Service 用来监听client的连接请求。创建一个 AIDL 文件,将暴露给client的接口在这个 AIDL 文件里声明。最后 Service 中实现这个 AIDL接口。client绑定服务端 Service,建立连接就可訪问远程服务端的方法。

1. AIDL 支持的数据类型:基本数据类型(int、long、chat、boolean、double 等);String 和 CharSequence;List(仅仅支持 ArrayList,里面的子元素都必须能够被 AIDL 支持)。Map(仅仅支持 HashMap。里面每个元素都必须被 AIDL 支持,包含 key 和 value);Parcelable(所以实现了 Parcelable 接口的对象);AIDL(全部的 AIDL 接口也能够在 AIDL 文件里使用)。

2. 自己定义的 Parcelable 对象和 AIDL 文件就算和当前 AIDL 文件位于同一包内也要显式 import。

3. 假设 AIDL 文件里用到自己定义 Parcelable 对象,必须新建一个和它同名的 AIDL 文件,并在当中声明它为 Parcelable 类型。

4. AIDL 中除了基本数据类型,其它类型的參数必须标上方向:in、out 或者 inout。in 表示输入类型參数,out 表示输出型參数。

5. AIDL 接口中仅仅支持方法,不支持声明静态常量。差别于传统的接口。

6. 为方便 AIDL 的开发。建议把全部和 AIDL 相关的类和文件全部放入同一个包中。当client是还有一个应用时。能够直接把整个包拷贝到clientproject中。

7. RemoteCallbackList 是系统专门提供用于删除进程 listener 的接口,RemoteCallbackList 是一个泛型。支持管理随意的 AIDL 接口,全部的 AIDL 接口都继承自 Interface 接口,在它的内部有一个 Map 结构专门用来保存全部的 AIDL 回调。其 Map 的 key 是 Binder 类型,value 是 Callback 类型。当client进程终止后,它能够自己主动移除client所注冊的 listener。

另外 RemoteCallbackList 内部自己主动实现了线程同步的功能,所以使用它进行注冊和解注冊时。不须要额外的线程同步工作。使用 RemoteCallbackList 须要注意是:它不是一个 List。遍历 RemoteCallbackList 须要依照下面方式进行。当中 beginBroadcast 和 finishBroadcast 必须配对使用,哪怕仅仅是获取 RemoteCallbackList 中的元素个数:

int N =mListenerList.beginBroadcast();
for(int i = 0; i < N; i++){
//
}
mListenerList.finishBroadcast();
  1. client的 onServiceConnected 和 onServiceDisconnected 方法都执行在 UI 线程中,所以不能够在它们里面直接调用服务端的耗时方法。

    服务端的方法本身就执行在服务端的 Binder 线程池中,所以服务端方法本身就能够执行大量耗时操作,这个时候切记不要在服务端方法中开线程去进行异步任务。除非明白干什么。

  2. Binder 可能意外死亡。须要又一次连接服务。有两种方法:给 Binder 设置 DeathRecipient 监听,死亡时会收到 binderDied 方法回调,然后能够重连;一种是在 onServiceDisconnected 中重连。其二者差别:onServiceDisconnected 在client的 UI 线程中被回调。binderDied 在client的 Binder 线程池中被回调,不能訪问 UI。
  3. AIDL 使用经常使用的权限验证方法: 一是在 onBind 中进行验证,验证不通过返回 null,client直接无法绑定服务。验证方式如 使用 permission 验证,在 AndroidMenifest 中声明所需权限(自己定义 permission)。此方式同样适用于 Messenger 中。二是在服务端 onTransact 方法中进行权限验证。假设验证失败返回 false,这样服务端就不会终止执行 AIDL 中的方法从而达到保护服务端的效果。验证方式也能够採用permission。还能够採用 Uid 和 Pid 来做验证。通过 getCallingUid 和 getCallingPid 拿到client所属应用的 Uid 和 Pid。

    这个两个參数能够用来做一些验证工作,比方验证包名。

    (5) 使用 ContentProvider:ContentProvider 是 Android 中提供的专门用于不同应用间进行数据共享方式。和 Messenger 一样,其底层实现是 Binder,当事实上现过程比 AIDL 简单很多。主要以表格的形式来组织数据,能够包含多个表。还支持文件数据,比方图片和视频等。

    文件数据和表格数据的结构不同,因此处理此类数据能够在 ContentProvider 中返回文件的句柄给外界从而让文件来訪问 ContentProvider,Android 系统提供的 MediaStore 功能就是文件类型的 ContentProvider。

    ContentProvider 的底层数据看起来像一个 SQLite 数据库,可是 ContentProvider 对底层的数据存储方式没有不论什么要求。能够使用 SQLite 数据库,也能够使用普通文件,甚至能够採用内存中的一个对象进行数据的存储。要观察一个 ContentProvider 中的数据改变情况,能够通过 ContentResolver 的 registerContentObserver 方法来注冊观察者。能够通过 unregisterContentObserver 方法来解除观察者。

    (6) 使用 Socket : Socket 被称为套接字,分为流式套接字和用户数据报套接字两种。分别对应于网络的传输控制层中的 TCP 和 UDP 协议。

2.5 Binder 连接池

当项目到一定程度,无限制的添加 Service 是不正确的,Service 是四大组件之中的一个。也是一种系统资源。我们须要将全部的 AIDL 放在同一个 Service 中去管理。工作机制:每个业务模块创建自己的 AIDL 接口并实现此接口,不同的业务模块之间是不能有耦合的。全部实现细节都单独开来,然后向服务端提供自己的唯一标识和其相对应的 Binder 对象。对于服务端来说仅仅须要一个 Service 就能够了,服务端提供一个 queryBinder 接口,这个接口能够依据业务模块的特征来返回对应的 Binder 对象给它们。不同的业务模块拿到所需的 Binder 对象后就能够进行远程方法调用,由此可见,Binder 连接池的主要作用就是将每个业务模块的 Binder 请求同一转发到远程 Service 中去执行,从而避免反复创建 Service。建议在开发中使用 BinderPool。

具体源代码查看:

https://github.com/singwhatiwanna/android-art-res/blob/master/Chapter_2/src/com/ryg/chapter_2/binderpool/BinderPool.java

2.6 选用合适的 IPC 方式

Android开发艺术-第二章 IPC 机制