进程间通信(二)
这一部分主要会介绍Android中特有的几个IPC机制。分别是: Intent、Binder、AIDL、ContentProvider
https://juejin.cn/post/7244018340880007226
https://juejin.cn/post/6844903764986462221
Binder
https://juejin.cn/post/7244018340880007226
https://juejin.cn/post/6897868762410811405
https://juejin.cn/post/7278103488097452092
Binder机制在安卓知识体系中的位置是非常重要的,理解Binder是成为高级安卓开发工程师的第一步。首先需要思考为什么需要Binder?
Android 系统是基于 Linux 内核的,Linux 已经提供了管道、消息队列、共享内存和 Socket 等 IPC 机制。那为什么 Android 还要提供 Binder 来实现 IPC 呢?主要是基于性能、稳定性和安全性几方面的原因。
这里我们可以整体回忆一下之前在进程间通信(一)里面提到的哪几种IPC机制。
- 管道:管道一次通信需要经历2次数据复制(进程A -> 管道文件,管道文件 -> 进程B)并且是通过内核缓冲区实现的,这个缓冲区是有限的,如果传输的数据大小超过缓冲区上限,或者在阻塞模式下没有安排好数据的读写,会出现阻塞的情况。管道所传送的是无格式字节流,这就要求管道的读出方和写入方必须事先约定好数据的格式。管道自带同步机制。
- 共享内存:共享内存是几种IPC机制中最快的,性能最好的,但是没有同步机制,安全性较差。
- 消息队列:息队列是存放在内核中的消息链表,每个消息队列由消息队列标识符表示。消息队列允许多个进程同时读写消息,发送方与接收方要约定好,消息体的数据类型与大小。消息队列克服了信号承载信息量少、管道只能承载无格式字节流等缺点,消息队列一次通信同样需要经历2次数据复制(进程A -> 消息队列,消息队列 -> 进程B)
- Socket:UNIX Domain Socket 是典型的C/S架构,一个Socket会拥有两个缓冲区,一读一写,由于发送/接收消息需要将一个Socket缓冲区中的内容拷贝至另一个Socket缓冲区,所以Socket一次通信也是需要经历2次数据复制
而我们现在提到的 Binder,是Android 系统通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder驱动(Binder Dirver)。
Binder 底层使用了内存映射(mmp)的机制,实现了传输数据只需要一次拷贝!
Binder机制的底层原理
由于Binder机制的知识量过于庞大,我这里先放着,只稍微记录一下他的思想。
Binder基于C/S的结构下,定义了4个角色:Server、Client、ServerManager、Binder驱动,其中前三者是在用户空间的,也就是彼此之间无法直接进行交互,Binder驱动是属于内核空间的,属于整个通信的核心,虽然叫驱动,但是实际上和硬件没有太大关系,只是实现的方式和驱动差不多,驱动负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。
Binder能够实现的最重要的概念,就是内存映射。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
比如进程中的用户区域是不能直接和物理设备打交道的,如果想要把磁盘上的数据读取到进程的用户区域,需要两次拷贝(磁盘–>内核空间–>用户空间);通常在这种场景下 mmap() 就能发挥作用,通过在物理介质和用户空间之间建立映射,减少数据的拷贝次数,用内存读写取代I/O读写,提高文件读取效率。
一次完整的 Binder IPC 通信过程通常是这样:
首先 Binder 驱动在内核空间创建一个数据接收缓存区;
接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
Binder的优势
Binder 在 Android 系统中具有显著的性能、稳定性和安全性优势,具体表现如下:
- 性能优势
- 高效数据传输:Binder 仅需一次数据拷贝(发送进程到接收进程),性能上接近共享内存,优于传统的 Socket、消息队列和管道(均需两次拷贝)。
- 低开销:与通用 Socket 接口相比,Binder 适用于本地进程间高效通信,开销更小,传输效率更高。
- 稳定性优势
- 架构清晰:Binder 基于客户端-服务器(C/S)架构,客户端发起请求,服务端响应执行,职责分明且相互独立,保证了系统的高稳定性。
- 简化控制:相比共享内存,Binder 通信机制简单,不需开发者管理复杂的共享内存控制逻辑,降低了出错几率。
- 安全性优势
- 可靠身份验证:Binder 支持进程的用户 ID(UID)和进程 ID(PID)鉴别,杜绝了传统 IPC 无法确认对方身份的缺陷,确保通信方的可信性。
- 控制访问权限:支持实名和匿名 Binder,限制未授权的应用程序访问,避免恶意程序通过猜测接入点地址进行未经许可的连接,安全性更高。
总结
Android 使用 Binder 作为核心 IPC 机制,满足系统对高性能、稳定性和安全性的需求,实现了高效、可靠的进程间通信。
Intent
熟悉安卓开发的小伙伴应该对Intent非常了解了,不管是启动活动(Activity)、服务(Service)、广播接收器(Broadcast Receiver),还是传递数据和操作,都离不开Intent的参与。可以说,Intent是四大组件之间的纽带。
首先还是贴出官方概念:https://developer.android.com/reference/android/content/Intent
基本概念
Intent,中文可翻译为“意图”,可用于Android同个应用程序中各个组件之间的交互,或者不同应用程序之间的交互。可以用来表明当前组件的思想和意图,比如想执行某个动作,想发送某些数据等等。每个组件都有不同的启动方法:
● Activity:可以调用startActivity() 或 startActivityForResult() 传递 Intent来打开新的Activity;
● Service:可以调用startService()传递 Intent 来启动服务,也可通过 bindService() 传递 Intent 来绑定到该服务;
● Broadcast,可以调用sendBroadcast()、sendOrderedBroadcast() 或 sendStickyBroadcast() 等方法传递 Intent 来发起广播;
Intent分为显式Intent和隐式Intent,我们以打开新的Activity为例进行讲解。
分类
● 显式intent:通过在intent中明确设置想要启动组件的类名,来显式地向安卓系统表达本次启动的目的。
Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent);
● 隐式intent:在intent中不会显式设置欲启动的组件类名,而是通过设置action、category等信息,安卓系统会自动根据各个组件在AM文件中设置的intent-filter来进行过滤和匹配,匹配成功的组件将会被启动。如果匹配到不止一个组件,将会通过弹窗的方式让用户选择处理该intent的组件
// 清单文件中 MyActivity 提前声明好如下:<activity android:name=".MyActivity"><intent-filter><action android:name="com.example.android.Mytest"/><category android:name="android.intent.category.DEFAULT"/><category android:name="com.example.android.Mycategory"/></intent-filter></activity>// 代码调用Intent intent = new Intent();intent.setAction(com.example.android.Mytest);intent.addCategory(com.example.android.Mycategory);startActivity(intent); //1
执行 [注释1]的代码后,系统会发现MyActivity的所设定的内容,和当前Intent所设定的内容最匹配,系统就会打开MyActivity,但这个过程中,我们并没有显式的指出打开MyActivity,而是通过设置了一些特定条件进行匹配,如“action”,“category”等,从而隐式地打开了MyActivity。
Intent的组成部分
“action”,“category”都是Intent的组成部分。为了更好的理解隐式Intent,需要了解一个Intent由几部分组成:
- componentName(组件名):目的组件
- action(动作):用来表现意图的行动
- uri(统一资源标识符):用于指定资源的具体位置
- category(类别):用来表现动作的类别
- data(数据):表示与动作要操纵的数据
- type(数据类型):对于data范例的描写
- extras(扩展信息):扩展信息
- Flags(标志位):期望这个意图的运行模式
安卓系统匹配隐式intent的过程:
- 加载安装所有的intent-filter的组件
- 剔除action匹配失败的组件
- 剔除URI数据匹配失败的组件
- 剔除category匹配失败的组件
- 如果匹配到的组件数量大于1,按照优先级返回最高优先级的组件。
常见构造
1. 创建Intent
import android.content.Intent;Intent intent = new Intent(); // 空构造
Intent intent = new Intent(String action); // 隐式intent,传入action
Intent intent = new Intent(String action, Uri uri); // 隐式intent
Intent intent = new Intent(Context packageContext, Class<?> cls); // 显式intent,参数1是当前组件的context,参数2是目标组件的类名
Intent intent = new Intent(String action, Uri uri, Context packageContext, Class<?> cls); // 显式intent
2. 设置component目的组件(显式intent)
Component属性明确指定Intent的目标组件的类名称。(属于显示Intent)
如果component这个属性有指定的话,将直接使用它指定的组件。指定了这个属性以后,Intent的其它所有属性都是可选的。
Intent intent = new Intent();
ComponentName component = new ComponentName(MainActivity.this, SecondActivity.class);
intent.setComponent(component);
startActivity(intent);// 简写成
Intent intent = new Intent(MainActivity.this,SecondActivity.class);
startActivity(intent);
3. 设置action动作
action表示intent欲要完成的动作,在Intent类中,定义了一批量的动作,比如ACTION_VIEW,ACTION_PICK等, 基本涵盖了常用动作。
action也可以是一个用户定义的字符串,用于标识一个用户自定义的 Android 应用程序组件。
一个 Intent Filter 可以包含多个 Action,但是一个intent对象只可以设置一个action。
在 AndroidManifest.xml 的Activity 定义时,可以在其intent-filter节点指定一个Action列表用于标识 Activity所能接受的“动作”。
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
具体有哪些Action动作可选,可以参考:https://developer.android.com/reference/android/content/Intent
4. 添加category类别
只有action和category中的内容同时能够匹配上Intent中指定的action和category时,这个活动才能响应intent。
用户在AM中声明安卓组件时,如果这个组件没有特别的category,则需要显式地将DEFAULT类型的category添加到category列表中,因为当调用startActivity()方法的时候会自动将这个DEFAULT 类型的category添加到Intent中。
自定义类别: 在Intent添加类别可以添加多个类别,那就要求这些类别能同时被匹配上,才算该intent被组件匹配成功。
操作Activity的时候,如果没有类别,须加上默认类别。
Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);
5. 设置data数据和type类型
Data属性是Android要访问的数据,和action和category声明方式相同,也是在intent-filter中。
多个组件匹配成功显示优先级高的; 相同显示列表。
Data是用一个uri对象来表示的,uri代表数据的地址,属于一种标识符。通常情况下,我们使用action+data属性的组合来描述一个意图:做什么。
使用隐式Intent,我们不仅可以启动自己程序内的活动,还可以启动其他程序的活动,这使得Android多个应用程序之间的功能共享成为了可能。比如应用程序中需要展示一个网页,没有必要自己去实现一个浏览器(事实上也不太可能),而是只需要调用系统的浏览器来打开这个网页就行了。
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("http://www.baidu.com"));
startActivity(intent);
6. 设置和读取扩展数据extra
通过intent.putExtra(String,XXX)方法以键值对的方式传递数据,其中XXX可代表基本数据类型、数组等。
获取数据通过intent.getXXXExtra(String)方法,其中XXX可代表基本数据类型、数组等。
Intent传递Serializable对象:
Intent intent = new Intent(ActivityA.this, ActivityB.class);
intent.putExtra("key", value)
7. 设置flags标志位
Intent 的 flags 标志位用于指定启动 Activity 的行为,比如启动模式和任务栈处理方式。
//用于启动一个新的任务栈,常用于从非 Activity 的上下文启动 Activity,比如从 BroadcastReceiver 或 Service 中启动 Activity。Intent intent = new Intent(context, TargetActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);//可以组合使用多个 flags,比如同时设置 FLAG_ACTIVITY_CLEAR_TOP 和 FLAG_ACTIVITY_NEW_TASK:
Intent intent = new Intent(context, TargetActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
Intent 数据传输的限制
Intent 传输数据的大小是有限制的,大概是1MB左右(这个数据不是固定的,有可能0.8M也会报错)。这是因为Intent 对象在 Android 系统中是通过 Binder 机制在进程间传递的。Binder 机制设计用于高效地处理进程间通信(IPC),但它并不适合传输大量数据。如果尝试通过 Intent 传递大量数据,那么这将显著增加 IPC 过程的开销,进而影响应用程序的性能。
如果我们调用
intent.putExtra("key", value) // value超过1M
则会出现报错,报错信息如下:
android.os.TransactionTooLargeException: data parcel size xxx bytes
如果非要想传输大一点的数据可以参考:https://juejin.cn/post/7109862936604049415。
Intent的底层原理
这里主要可以参考博客:https://juejin.cn/post/7252951745013727292
大致的意思是,在使用Intent去启动活动等时,PackageManagerService(PMS)就会启动,PMS将分析所有已安装的应用信息,构建相对应的信息表,当用户需要通过Intent跳转到某个组件时,会根据Intent中包含的信息,然后从PMS中查找对应的组件列表,最后跳转到目标组件。
总体有一个:扫描➡️解析➡️绘制信息树➡️信息匹配 的过程。
AIDL
AIDL(Android Interface Definition Language)是一种用于定义和实现跨进程通信接口的语言。它是 Android 系统中用于进程间通信(IPC)的一种机制,允许一个进程向另一个进程发送请求并获取响应。
AIDL 是 Binder 的高级封装:
- AIDL 是为简化使用 Binder 而设计的接口描述语言,开发者可以使用 AIDL 定义服务接口,系统会自动生成用于跨进程通信的接口代码。
- 通过 AIDL,开发者不需要直接操作底层的 Binder API,只需定义接口方法,AIDL 编译器会生成 Binder 的代理类和服务端接口,从而简化了 IPC 的实现。
Android AIDL 的使用方法和示例
在 Android 中使用 AIDL(Android Interface Definition Language)来实现跨进程通信,主要包括以下步骤:
- 创建 AIDL 文件并声明接口方法。
- 创建服务(Service),在其中实现 AIDL 接口并返回 Binder 对象。
- 在客户端绑定服务,通过
ServiceConnection
获取 AIDL 接口对象并调用其方法。
使用方式可以参考:
https://juejin.cn/post/7126801737561669639
https://juejin.cn/post/7123129439898042376
这里由于篇幅有限,不介绍AIDL 的具体使用方法,后续会开专题介绍。
AIDL 的原理
服务端(Server)
服务端需要实现一个 Service,作为远程服务的提供者,包含具体的业务逻辑。
客户端(Client)
客户端需要绑定服务,获取远程服务的 Binder 对象后,通过该对象调用服务端的方法。
绑定过程
- 绑定服务:客户端调用 bindService 方法绑定远程服务。
- 接收 Binder:通过 ServiceConnection 获取远程服务的 Binder 对象。
- 远程调用:通过 Binder 调用服务端定义的方法。
Stub 类(用于服务端)
- 作用:Stub 类在服务端实现了接口的所有方法,并将其绑定到 Binder 的通信逻辑中。服务端的所有逻辑处理都会在 Stub 的实现类中完成。
- 功能:
- 处理客户端的请求。
- 提供跨进程方法的具体实现。
onTransact 方法
- 作用:
onTransact
是 Stub 类的核心方法,用于接收并处理客户端的远程调用请求。 - 工作流程:
- 根据方法标识符(
code
参数)判断客户端调用的是哪一个接口方法。 - 从
Parcel
对象中解析客户端传递的数据。 - 调用对应的接口方法。
- 将结果写入返回的
Parcel
对象,并发送给客户端。
- 根据方法标识符(
Proxy 类(用于客户端)
- 作用:
Proxy
是客户端的代理类,负责将客户端调用的方法转化为 Binder 的底层通信。 - 工作流程:
- 通过
transact
方法将调用请求发送到服务端的onTransact
方法。 - 使用
Parcel
对象传递请求参数和接收响应结果。 - 将服务端返回的数据封装为客户端需要的格式。
- 通过
示例调用链:获取书籍列表
以 getBookList() 为例,具体调用链如下:
- 客户端调用 Proxy.getBookList():
- 客户端通过 Proxy 调用方法。
- Proxy 封装请求,发送到服务端。
- 服务端接收请求:
- Stub.onTransact() 方法接收并解析请求。
- 根据方法标识符调用 getBookList() 的具体实现。
- 服务端返回结果:
- 服务端将结果写入 Parcel。
- 返回数据通过 Binder 传递到客户端。
- 客户端接收结果:
- Proxy 解包数据,将结果返回给客户端。
ContentProvider
ContentProvider的底层实现是采用Android中的Binder机制,它允许不同应用程序之间或者同一个应用程序的不同部分之间共享数据。ContentProvider本质上就是封装了一层接口,用来屏蔽各种数据存储的方式。 不管是数据库、磁盘、还是网络存储,只需要通过contentProvider 提供的方式来获取就行。使用方不用关心具体的实现逻辑。
提供的方法包括: query、insert、update、delete。
contentProvider的特点:
- 数据共享:ContentProvider允许应用程序之间共享数据,这些数据可以是结构化的(如数据库中的数据)、非结构化的,甚至是文件中的数据
- 统一接口:ContentProvider提供了一个统一的接口,使得其他应用程序可以通过这个接口访问和操作数据,包括增删改查等操作
- URI访问:ContentProvider使用URI(统一资源标识符)来标识数据,其他应用程序可以使用URI来定位和访问特定数据
- 权限控制:ContentProvider可以定义权限来控制哪些应用程序有权访问数据,从而保护数据安全性
- 基于Binder机制:ContentProvider的底层实现是采用Android中的Binder机制,这是一种高效的IPC方式,允许跨进程调用
- 通知机制:当ContentProvider中的数据发生变化时,它可以通知所有已注册的侦听器(ContentObserver),这样其他应用程序可以及时响应数据的变化
- 跨进程通信的实现:通过在AndroidManifest.xml中声明ContentProvider,并在其中实现抽象方法,如query、insert、update和delete,应用程序可以接收来自其他进程的请求,并处理数据
contentProvider的使用方式
provider提供方:
- 需要继承ContentProvider抽象类。并且实现相应的query方法
用户需要实现如下抽象方法:
// 创建数据库并获得数据库连接
public abstract boolean onCreate()// 查询数据
public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)// 插入数据
public abstract Uri insert(Uri uri, ContentValues values)// 删除数据
public abstract int delete(Uri uri, String selection, String[] selectionArgs)// 更新数据
public abstract int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)// 获取数据类型(MIME类型,如:"text/html"、"image/png"、"message/rfc882"、"vnd.android-dir/mms-sms")
public abstract String getType(Uri uri)
参数说明:
- uri:数据表路径
- projection:需要查询的字段名称
- selection:查询条件
- selectionArgs:查询条件中的参数列表
- sortOrder:排序
- URI的定义
协议:授权标识:路径:
content://authority/data_path/id
Content://com.demo.gradle/android/db
- 在AndroidManifest.xml 中注册
<provider
android:authorities="com.demo.gradle"
android:name=".MyProvider"
android:exported="true"
/>
provider调用方:
当contentResolve要查询或者插入的时候,就需要解析uri。
val resolver = context.contentResolver
var uri=Uri.parse("content://com.demo.gradle/android/db")resolver.query(uri)
第一步,获取contentResolver对象
第二步,开始执行query
代码范例可参考:https://blog.csdn.net/m0_37602827/article/details/109912206
了解更多
ContentProvider基础概念
使用 ContentObserver 监听数据变化
高级场景 - 数据分页加载
最佳实践和常见问题