前言
本文已经收录到我的 GitHub 个人博客, 欢迎大佬们光临寒舍:
我的 GitHub 博客 https://lovelifeeveryday.github.io/
学习清单:
IPC 的基础概念
多进程和多线程的概念
Android 中的序列化机制和 Binder
Android 中的 IPC 方式
Binder 连接池的概念及运用
各种 IPC 的优缺点
一. 为什么要学习 IPC?
IPC 是 Inter-Process Communication 的缩写, 含义是进程间通信, 是指两个进程之间进行数据交换的过程.
有些读者可能疑惑: "那什么是进程呢? 什么是线程呢? 多进程和多线程有什么区别呢?"
进程: 是资源分配的最小单位, 一般指一个执行单元, 在 PC 和移动设备上指一个程序或应用.
线程: CPU 调度的最小单位, 线程是一种有限的系统资源.
两者关系: 一个进程可包含多个线程, 即一个应用程序上可以同时执行多个任务.
主线程(UI 线程):UI 操作
有限个子线程: 耗时操作
注意: 不可在主线程做大量耗时操作, 会导致 ANR(应用无响应). 解决办法: 将耗时任务放在线程中.
IPC 不是 Android 所特有的, Android 中最有特色的 IPC 方式是 Binder. 而日常开发中涉及到的知识: AIDL, 插件化, 组件化等等, 都离不开 Binder. 由此可见, IPC 是挺重要的.
二. 核心知识点归纳
2.1 Android 中的多进程模式
Q1: 开启多线程的方式:
(常用)在 AndroidMenifest 中给四大组件指定属性 Android:process
precess 的命名规则:
默认进程: 没有指定该属性则运行在默认进程, 其进程名就是包名.
以 ":" 为命名开头的进程:":" 的含义是在进程名前面加上包名, 属于当前应用私有进程
完整命名的进程: 属于全局进程, 其他应用可以通过 ShareUID 方式和他跑在用一个进程中(需要 ShareUID 和签名相同).
(不常用)通过 JNI 在 native 层 fork 一个新的进程.
Q2: 多进程模式的运行机制:
Andoird 为每个进程分配了一个独立的虚拟机, 不同虚拟机在内存分配上有不同的地址空间, 这也导致了不同虚拟机中访问同一个对象会产生多份副本.
带来四个方面的问题:
静态变量和单例模式失效 -->原因: 不同虚拟机中访问同一个对象会产生多份副本.
线程同步机制失效 -->原因: 内存不同, 线程无法同步.
SharedPreference 的可靠性下降 -->原因: 底层是通过读写 xml 文件实现的, 发生并发问题.
Application 多次创建 -->原因: Android 系统会为新的进程分配独立虚拟机, 相当于应用重新启动了一次.
2.2 IPC 基础概念
这里主要介绍三方面内容:
- Serializable
- Parcelable
- Binder
只有熟悉这三方面的内容, 才能更好理解 IPC 的各种方式
2.2.1 什么是序列化
含义: 序列化表示将一个对象转换成可存储或可传输的状态. 序列化后的对象可以在网络上进行传输, 也可以存储到本地.
使用场景: 需要通过 Intent 和 Binder 等传输类对象就必须完成对象的序列化过程.
两种方式: 实现 Serializable/Parcelable 接口.
2.2.2 Serializable 接口
Java 提供的序列化接口, 使用方式比较简单:
实体类实现 Serializable
手动设置 / 系统自动生成 serialVersionUID
- //Serializable Demo
- public class Person implements Serializable{
- private static final long serialVersionUID = 7382351359868556980L;
- private String name;
- private int age;
- public int getAge() {
- return age;
- }
- public void setAge(int age) {
- this.age = age;
- }
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
这里特别注意一下 serialVersionUID:
含义: 是 Serializable 接口中用来辅助序列化和反序列化过程.
注意: 原则上序列化后的数据中的 serialVersionUID 要和当前类的 serialVersionUID 相同才能正常的序列化. 当类发生非常规性变化 (修改了类名 / 修改了成员变量的类型) 的时候, 序列化失败.
2.2.3 Parcelable 接口
是 Android 中的序列化接口, 使用的时候, 类中需要实现下面几点:
实现 Parcelable 接口
内容描述
序列化方法
反序列化方法
- public class User implements Parcelable {
- public int userId;
- public String userName;
- public boolean isMale;
- public Book book;
- public User() {
- }
- public User(int userId, String userName, boolean isMale) {
- this.userId = userId;
- this.userName = userName;
- this.isMale = isMale;
- }
- // 返回内容描述 return 0 即可
- public int describeContents() {
- return 0;
- }
- // 序列化
- 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<User> CREATOR = new Parcelable.Creator<User>() {
- // 从序列化的对象中创建原始对象
- public User createFromParcel(Parcel in) {
- return new User(in);
- }
- 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().getContextClassLoader());
- }
- @Override
- public String toString() {
- return String.format("User:{userId:%s, userName:%s, isMale:%s}, with child:{%s}",
- userId, userName, isMale, book);
- }
- }
2.2.4 Serializable 和 Parcelable 接口的比较
Serializable 接口 | Parcelable 接口 | |
---|---|---|
平台 | Java | Andorid |
序列化原理 | 将一个对象转换成可存储或者可传输的状态 | 将对象进行分解, 且分解后的每一部分都是传递可支持的数据类型 |
优缺点 | 优点:使用简单 缺点:开销大(因为需要进行大量的 IO 操作) | 优点:高效 缺点:使用麻烦 |
使用场景 | 将对象序列化到存储设备或者通过网络传输 | 主要用在内存序列化上 |
2.2.5 Binder
Q1:Binder 是什么?
从 API 角度: 是一个类, 实现 IBinder 接口.
从 IPC 角度: 是 Android 中的一种跨进程通信方式.
从 Framework 角度: 是 ServiceManager, 连接各种 Manager 和相应 ManagerService 的桥梁.
从应用层: 是客户端和服务端进行通信的媒介. 客户端通过它可获取服务端提供的服务或者数据.
Q2:Android 是基于 Linux 内核基础上设计的, 却没有把管道 / 消息队列 / 共享内存 / 信号量 / Socket 等一些 IPC 通信手段作为 Android 的主要 IPC 方式, 而是新增了 Binder 机制, 其优点有:
A1: 传输效率高, 可操作性强
传输效率主要影响因素是内存拷贝的次数, 拷贝次数越少, 传输速率越高. 几种数据传输方式比较
方式 | 拷贝次数 | 操作难度 |
---|---|---|
Binder | 1 | 简易 |
消息队列 | 2 | 简易 |
Socket | 2 | 简易 |
管道 | 2 | 简易 |
共享内存 | 0 | 复杂 |
从 Android 进程架构角度分析: 对于消息队列, Socket 和管道来说, 数据先从发送方的缓存区拷贝到内核开辟的缓存区中, 再从内核缓存区拷贝到接收方的缓存区, 一共两次拷贝, 如图:
对 Binder 来说: 数据从发送方的缓存区拷贝到内核的缓存区, 而接收方的缓存区与内核的缓存区是映射到同一块物理地址的, 节省了一次数据拷贝的过程
A2: 实现 C/S 架构方便
Linux 的众 IPC 方式除了 Socket 以外都不是基于 C/S 架构, 而 Socket 主要用于网络间的通信且传输效率较低. Binder 基于 C/S 架构 ,Server 端与 Client 端相对独立, 稳定性较好.
A3: 安全性高
传统 Linux IPC 的接收方无法获得对方进程可靠的 UID/PID, 从而无法鉴别对方身份; 而 Binder 机制为每个进程分配了 UID/PID 且在 Binder 通信时会根据 UID/PID 进行有效性检测.
Q3:Binder 框架定义了哪四个角色呢?
A1:Server&Client
服务器 & 客户端. 在 Binder 驱动和 Service Manager 提供的基础设施上, 进行 Client-Server 之间的通信.
A2:ServiceManager:
服务的管理者, 将 Binder 名字转换为 Client 中对该 Binder 的引用, 使得 Client 可以通过 Binder 名字获得 Server 中 Binder 实体的引用.
A3:Binder 驱动
与硬件设备没有关系, 其工作方式与设备驱动程序是一样的, 工作于内核态.
提供 open(),mmap(),poll(),ioctl()等标准文件操作.
以字符驱动设备中的 misc 设备注册在设备目录 / dev 下, 用户通过 / dev/binder 访问该它.
负责进程之间 binder 通信的建立, 传递, 计数管理以及数据的传递交互等底层支持.
驱动和应用程序之间定义了一套接口协议, 主要功能由 ioctl()接口实现, 由于 ioctl()灵活, 方便且能够一次调用实现先写后读以满足同步交互, 因此不必分别调用 write()和 read()接口.
其代码位于 Linux 目录的
drivers/misc/binder.c
中.
ioctl(input/output control)是一个专用于设备输入输出操作的系统调用, 该调用传入一个跟设备有关的请求码, 系统调用的功能完全取决于请求码
Q4:Binder 工作原理是什么
服务器端: 在服务端创建好了一个 Binder 对象后, 内部就会开启一个线程用于接收 Binder 驱动发送的消息, 收到消息后会执行 onTranscat(), 并按照参数执行不同的服务端代码.
Binder 驱动: 在服务端成功创建 Binder 对象后, Binder 驱动也会创建一个 mRemote 对象 (也是 Binder 类), 客户端可借助它调用 transcat() 即可向服务端发送消息.
客户端: 客户端要想访问 Binder 的远程服务, 就必须获取远程服务的 Binder 对象在 Binder 驱动层对应的 mRemote 引用. 当获取到 mRemote 对象的引用后, 就可以调用相应 Binder 对象的暴露给客户端的方法.
当发出远程请求后客户端会挂起, 直到返回数据才会唤醒 Client
Q5: 当服务端进程异常终止的话, 造成 Binder 死亡的话, 怎么办?
在客户端绑定远程服务成功后, 给 Binder 设置死亡代理, 当 Binder 死亡的时候, 我们会收到通知, 从而重新发起连接请求.
- private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
- @Override
- public void binderDied(){
- if(mBookManager == null){
- return;
- }
- mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
- mBookManager = null;
- // TODO: 这里重新绑定远程 Service
- }
- }
- mService = IBookManager.Stub.asInterface(binder);
- binder.linkToDeath(mDeathRecipient,0);
2.3 Android 中的 IPC 方式
Android 中的 IPC 方式有很多种, 但本质都是基于 Binder 构建的
2.3.1 Bundle
原理: Bundle 底层实现了 Parcelable 接口, 它可方便的在不同的进程中传输.
注意: Bundle 不支持的数据类型无法在进程中被传递.
小课堂测试: 在 A 进程进行计算后的结果不是 Bundle 所支持的数据类型, 该如何传给 B 进程?
答案: 将在 A 进程进行的计算过程转移到 B 进程中的一个 Service 里去做, 这样可成功避免进程间的通信问题.
Intent 和 Bundle 的区别与联系:
Intent 底层其实是通过 Bundle 进行传递数据的
使用难易: Intent 比较简单, Bundle 比较复杂
Intent 旨在数据传递, bundle 旨在存取数据
2.3.2 文件共享
概念: 两个进程通过读 / 写同一个文件来交换数据. 比如 A 进程把数据写入文件, B 进程通过读取这个文件来获取数据.
适用场景: 对数据同步要求不高的进程之间进行通信, 并且要妥善处理并发读 / 写的问题.
特殊情况: SharedPreferences 也是文件存储的一种, 但不建议采用. 因为系统对 SharedPreferences 的读 / 写有一定的缓存策略, 即在内存中有一份该文件的缓存, 因此在多进程模式下, 其读 / 写会变得不可靠, 甚至丢失数据.
2.3.3 AIDL
2.3.3.1 概念
AIDL(Android Interface Definition Language,Android 接口定义语言): 如果在一个进程中要调用另一个进程中对象的方法, 可使用 AIDL 生成可序列化的参数, AIDL 会生成一个服务端对象的代理类, 通过它客户端实现间接调用服务端对象的方法.
2.3.3.2 支持的数据类型
基本数据类型
String 和 CharSequence
想了解 String 和 CharSequence 区别的读者, 可以看下这篇文章: String 和 CharSequence 的区别
ArrayList,HashMap 且里面的每个元素都能被 AIDL 支持
实现 Parcelable 接口的对象
所有 AIDL 接口本身
注意: 除了基本数据类型, 其它类型的参数必须标上方向: in,out 或 inout, 用于表示在跨进程通信中数据的流向.
2.3.3.3 两种 AIDL 文件
用于定义 Parcelable 对象, 以供其他 AIDL 文件使用 AIDL 中非默认支持的数据类型的.
用于定义方法接口, 以供系统使用来完成跨进程通信的.
注意:
自定义的 Parcelable 对象必须把 Java 文件和自定义的 AIDL 文件显式的 import 进来, 无论是否在同一包内.
AIDL 文件用到自定义 Parcelable 的对象, 必须新建一个和它同名的 AIDL 文件, 并在其中声明它为 Parcelable 类型.
2.3.3.4 本质, 关键类和方法
a: 本质是系统提供了一套可快速实现 Binder 的工具.
b: 关键类和方法是什么?
AIDL 接口: 继承 IInterface.
Stub 类: Binder 的实现类, 服务端通过这个类来提供服务.
Proxy 类: 服务器的本地代理, 客户端通过这个类调用服务器的方法.
asInterface(): 客户端调用, 将服务端的返回的 Binder 对象, 转换成客户端所需要的 AIDL 接口类型对象.
返回对象:
若客户端和服务端位于同一进程, 则直接返回 Stub 对象本身;
否则, 返回的是系统封装后的 Stub.proxy 对象.
asBinder(): 返回代理 Proxy 的 Binder 对象.
onTransact(): 运行服务端的 Binder 线程池中, 当客户端发起跨进程请求时, 远程请求会通过系统底层封装后交由此方法来处理.
transact(): 运行在客户端, 当客户端发起远程请求的同时将当前线程挂起. 之后调用服务端的 onTransact()直到远程请求返回, 当前线程才继续执行.
2.3.3.5 实现方法
如果感兴趣的读者想要了解具体的 AIDL 实现 IPC 的流程, 笔者分享一篇文章: Android 跨进程通信(IPC): 使用 AIDL
A. 服务端:
创建一个 aidl 文件;
创建一个 Service, 实现 AIDL 的接口函数并暴露 AIDL 接口.
B. 客户端:
通过 bindService 绑定服务端的 Service;
绑定成功后, 将服务端返回的 Binder 对象转化成 AIDL 接口所属的类型, 进而调用相应的 AIDL 中的方法.
总结: 服务端里的某个 Service 给和它绑定的特定客户端进程提供 Binder 对象, 客户端通过 AIDL 接口的静态方法 asInterface() 将 Binder 对象转化成 AIDL 接口的代理对象, 通过这个代理对象就可以发起远程调用请求.
2.3.3.6 可能产生 ANR 的情形
A. 客户端:
调用服务端的方法是运行在服务端的 Binder 线程池中, 若主线程所调用的方法里执行了较耗时的任务, 同时会导致客户端线程长时间阻塞, 易导致客户端 ANR.
在
onServiceConnected()
和
onServiceDisconnected()
里直接调用服务端的耗时方法, 易导致客户端 ANR.
B. 服务端:
服务端的方法本身就运行在服务端的 Binder 线程中, 可在其中执行耗时操作, 而无需再开启子线程.
回调客户端 Listener 的方法是运行在客户端的 Binder 线程中, 若所调用的方法里执行了较耗时的任务, 易导致服务端 ANR.
解决客户端频繁调用服务器方法导致性能极大损耗的办法: 实现观察者模式.
即当客户端关注的数据发生变化时, 再让服务端通知客户端去做相应的业务处理.
2.3.3.7 解注册失败的问题
原因: Binder 进行对象传输实际是通过序列化和反序列化进行, 即 Binder 会把客户端传递过来的对象重新转化并生成一个新的对象, 虽然在注册和解注册的过程中使用的是同一个客户端传递的对象, 但经过 Binder 传到服务端后会生成两个不同的对象. 另外, 多次跨进程传输的同一个客户端对象会在服务端生成不同的对象, 但它们在底层的 Binder 对象是相同的.
解决办法: 当客户端解注册的时候, 遍历服务端所有的 Listener, 找到和解注册 Listener 具有相同的 Binder 对象的服务端 Listener, 删掉即可.
需要用到 RemoteCallBackList:Android 系统专门提供的用于删除跨进程 listener 的接口. 其内部自动实现了线程同步的功能.
2.3.4 Messager
Q1. 什么是 Messager?
A1:Messager 是轻量级的 IPC 方案, 通过它可在不同进程中传递 Message 对象.
Messenger.send(Message);
Q2: 特点是什么?
底层实现是 AIDL, 即对 AIDL 进行了封装, 更便于进行进程间通信.
其服务端以串行的方式来处理客户端的请求, 不存在并发执行的情形, 故无需考虑线程同步的问题.
可在不同进程中传递 Message 对象, Messager 可支持的数据类型即 Messenge 可支持的数据类型.
Messenge 可支持的数据类型:
arg1,arg2,what 字段: int 型数据
obj 字段: Object 对象, 支持系统提供的 Parcelable 对象
setData:Bundle 对象
有两个构造函数, 分别接收 Handler 对象和 Binder 对象.
Q3: 实现的方法:
读者如果对 Messenger 的具体使用感兴趣的话, 可以看下这篇文章: IPC-Messenger 使用实例
A1: 服务端:
创建一个 Service 用于提供服务;
其中创建一个 Handler 用于接收客户端进程发来的数据;
利用 Handler 创建一个 Messenger 对象;
在 Service 的 onBind()中返回 Messenger 对应的 Binder 对象.
A2: 客户端:
通过 bindService 绑定服务端的 Service;
通过绑定后返回的 IBinder 对象创建一个 Messenger, 进而可向服务器端进程发送 Message 数据.(至此只完成单向通信)
在客户端创建一个 Handler 并由此创建一个 Messenger, 并通过 Message 的 replyTo 字段传递给服务器端进程. 服务端通过读取 Message 得到 Messenger 对象, 进而向客户端进程传递数据.(完成双向通信)
Q4: 缺点:
主要作用是传递 Message, 难以实现远程方法调用.
以串行的方式处理客户端发来的消息的, 不适合高并发的场景.
解决方式: 使用 AIDL 的方式处理 IPC 以应对高并发的场景
2.3.5 ContentProvider
ContentProvider 是 Android 提供的专门用来进行不同应用间数据共享的方式, 底层同样是通过 Binder 实现的.
除了 onCreate()运行在 UI 线程中, 其他的 query(),update(),insert(),delete()和 getType()都运行在 Binder 线程池中.
CRUD 四大操作存在多线程并发访问, 要注意在方法内部要做好线程同步.
一个 SQLiteDatabase 内部对数据库的操作有同步处理, 但多个 SQLiteDatabase 之间无法同步.
2.3.6 Socket
Socket 不仅可以跨进程, 还可以跨设备通信
Q1: 使用类型是什么?
流套接字: 基于 TCP 协议, 采用流的方式提供可靠的字节流服务.
数据流套接字: 基于 UDP 协议, 采用数据报文提供数据打包发送的服务.
Q2: 实现方法是什么?
A1: 服务端:
创建一个 Service, 在线程中建立 TCP 服务, 监听相应的端口等待客户端连接请求;
与客户端连接时, 会生成新的 Socket 对象, 利用它可与客户端进行数据传输;
与客户端断开连接时, 关闭相应的 Socket 并结束线程.
A2: 客户端:
开启一个线程, 通过 Socket 发出连接请求;
连接成功后, 读取服务端消息;
断开连接, 关闭 Socket.
2.3.7 优缺点比较
名称 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输 Bundle 支持的数据类型 | 四大组件间的进程间通信 |
文件共享 | 简单易用 | 不适合高并发场景,无法做到进程间的即时通信 | 无并发访问,交换简单数据且实时性不高 |
AIDL | 支持一对多并发和实时通信 | 使用稍复杂,需要处理线程同步 | 一对多且有 RPC 需求 |
Messenger | 支持一对多串行通信 | 不能很好处理高并发,不支持 RPC,只能传输 Bundle 支持的数据类型 | 低并发的一对多 |
ContentProvider | 支持一对多并发数据共享 | 可理解为受约束的 AIDL | 一对多进程间数据共享 |
Socket | 支持一对多并发数据共享 | 实现细节繁琐 | 网络数据交换 |
2.4 Binder 连接池
有多个业务模块都需要 AIDL 来进行 IPC, 此时需要为每个模块创建特定的 aidl 文件, 那么相应的 Service 就会很多. 必然会出现系统资源耗费严重, 应用过度重量级的问题. 因此需要 Binder 连接池, 通过将每个业务模块的 Binder 请求统一转发到一个远程 Service 中去执行的方式, 从而避免重复创建 Service.
Q1: 工作原理是什么?
每个业务模块创建自己的 AIDL 接口并实现此接口, 然后向服务端提供自己的唯一标识和其对应的 Binder 对象. 服务端只需要一个 Service, 服务器提供一个 queryBinder 接口, 它会根据业务模块的特征来返回相应的 Binder 对像, 不同的业务模块拿到所需的 Binder 对象后就可进行远程方法的调用了.
Q2: 实现方式是什么?
读者如果对具体的实现方式感兴趣的话, 可以看一下这篇文章: Android IPC 机制(四): 细说 Binder 连接池
为每个业务模块创建 AIDL 接口并具体实现
为 Binder 连接池创建 AIDL 接口 IBinderPool.aidl 并具体实现
远程服务 BinderPoolService 的实现, 在 onBind()返回实例化的 IBinderPool 实现类对象
Binder 连接池的具体实现, 来绑定远程服务
客户端的调用
三. 碎碎念
恭喜你, 已经完成了这次奇妙的 IPC 之旅了, 如果你感到对概念还是有点模糊不清的话, 没关系, 很正常, 不用太纠结于细节, 你可以继续进行下面的旅程了, 未来的你, 再看这篇文章, 也许会有更深的体会, 到时候就会有茅舍顿开的感觉了. 未来的你, 一定会更优秀!!!
路漫漫其修远兮, 吾将上下而求索.《离骚》-- 屈原
如果文章对您有一点帮助的话, 希望您能点一下赞, 您的点赞, 是我前进的动力
本文参考链接:
String 和 CharSequence 的区别
Android 跨进程通信(IPC): 使用 AIDL
IPC-Messenger 使用实例
Android IPC 机制(四): 细说 Binder 连接池
《Android 开发艺术探索》
要点提炼 | 开发艺术之 IPC https://www.jianshu.com/p/1c70d7306808
来源: https://www.cnblogs.com/xcynice/p/qi_miao_de_ipc_zhi_lv.html