一、前置知识
(一)进程间隔离
A进程无法访问B进程内存,B进程无法访问A进程的内存
在 Android 中,不同 App运行在不同进程,这是 Linux 内核的进程隔离机制。
默认情况下,同一个 App 的所有组件(Activity、Service 等)都运行在同一个主进程中,但为了隔离内存泄漏、防止主界面崩溃拖垮核心后台服务,成熟的大型应用通常会通过
android:process属性,将特定组件(如音乐播放 Service)拆分到独立进程(如:music)中运行。注意:一个进程运行着若干个 Activity 实例
(二)进程间通信
(Inter-Process Communication,简称IPC):
进程间通信必须依赖 AIDL / Bundle / ContentProvider 等跨进程机制,这也是 Android Framework 开发中 AIDL 被广泛使用的核心场景之一。
Android中提供了多种可直接调用的IPC方式:
| IPC 方式 | 数据传递范围 | 核心特点 | 典型场景 | 实现层级 |
|---|---|---|---|---|
| 管道 (Pipe) | 极少量字节流 | 单工/半双工,延迟极低,仅做信号通知 | Looper 唤醒、子进程标准输出 | Linux 内核 |
| 文件共享 | 可序列化数据 | 简单,并发不安全 | 低实时性数据交换 | 文件系统 |
| Bundle / Intent | 基本类型、Parcelable | 简单易用,数据量小(<1MB) | Activity 跳转传参 | 基于 Binder |
| Messenger | Bundle 类型 | 轻量,串行队列处理 | 低并发消息推送 | |
| AIDL | 几乎所有类型 | 功能强,支持 RPC,并发 | 多进程音乐服务 | |
| ContentProvider | 结构化数据(Cursor) | 标准数据共享接口 | 系统通讯录访问 | |
| Socket (UDS/TCP) | 任意字节流 | 跨网络、全双工、可传输超大文件 | Zygote 孵化 App、adb 调试、聊天室 | Linux 网络栈 |
(三)Binder机制
Binder是Android专属的IPC内核机制,是Android Framework所有高阶IPC的底层基座,基于Linux内核驱动实现C/S(客户端/服务端)架构,是Android系统最核心、使用最广泛的IPC方案。
核心优势:一次内存拷贝、支持RPC远程调用、自带权限校验、统一服务管理、线程池调度、并发安全可控。
层级 | 简单理解运行态 | 核心东西 | 最简单作用(人话) |
|---|---|---|---|
应用层(Java) | App普通代码,用户态(权限低) | AIDL、Messenger、Bundle、IBinder、Parcel | 开发者唯一会接触的层。负责定义接口、打包数据、发起跨进程调用。 |
Framework Native层 | 系统底层代码,用户态(权限低) | 客户端代理、服务端桩代码 | 翻译官。把Java的请求翻译成系统能懂的指令,传给内核,屏蔽所有复杂底层逻辑。 |
Binder内核驱动层 | 系统内核代码,内核态(权限最高) | /dev/binder 驱动、内存映射、数据传输 | 真正干活的。唯一能跨进程传数据、拷贝内存、调度线程、管控通信的核心层。 |
ServiceManager 服务管理层 | 独立小程序,用户态运行 | 服务注册、服务查询 | 前台登记处。所有服务在这里注册,App需要通信时,在这里找到对应的服务。 |
| 核心机制 | 解释 | 后果 |
|---|---|---|
| 一次拷贝 | 发数据进内核(1次),收数据直接内存映射(0次),省掉一次 CPU 搬运。 | 性能优于 Socket/管道,但不能在主线程调耗时 RPC。 |
| 1MB 限制 | Binder 缓冲区默认约 1MB,单次事务塞多了会爆。 | 崩溃TransactionTooLargeException(传大图/长列表时常见)。 |
| 虚拟隔离 | 进程间地址互不相通,无法传对象指针。 | 多进程下静态变量失效,SharedPreferences不安全,必须用 Parcelable。 |
(四)Parcelable
| 对比 | 二者都属于序列化,作用都是把对象转成可传输的二进制字节流 | |
|---|---|---|
| Parcelable(Android 专属) | Serializable(Java 原生) | |
| 实现复杂度 | 较复杂(需手动写writeToParcel等) | 极简单(仅实现空接口) |
| 序列化速度 | 极快(无反射,直接操作二进制) | 极慢(依赖反射,开销大) |
| 内存占用 | 紧凑(仅存字段值) | 臃肿(存大量类描述信息) |
| 定向 Tag | 完美支持in/out/inout | 不支持(无法精细控制流向) |
| AIDL | ✅唯一正确选择 | ⚠️ 技术上可行,但严禁使用 |
Parcelable 是 Android 专属的“快速打包/拆包”协议。
当你要把一个 Java/Kotlin 对象(比如User对象,里面有name和age)从一个进程传到另一个进程时,内存里的对象是散落的(引用指向不同地址)。不能直接扔过去,必须把它“打包”成二进制字节流,对方收到后再“拆包”。
打包(序列化):调用
writeToParcel(),把name和age按顺序写进Parcel(快递盒)。拆包(反序列化):调用构造函数
createFromParcel(),按顺序从Parcel里读出name和age,组装成新对象。
Parcelable 就是 Android 为了这个“打包/拆包”过程,给你定的一套必须实现的接口规则。
java的实体类 >= kotlin数据类
(五)Service
Service (后台隐形打工人,分两种上班模式)基本上分为两种形式 :
- 启动服务(startService):不跟页面交互,后台长期存活,
- 绑定服务(bindService):页面实时操控服务,没绑定就自动销毁
- -----IBinder(通话电话线):做电话线
- 扩展 Binder 类(同 APP+同进程专用)
- 使用Messenger(跨 APP+ 跨进程,单排队处理消息(×多线程并发))
- 使用 AIDL(跨 APP +跨进程+多线程并发,同时处理大量请求,需自己处理进程安全)
所有 IPC( Inter-Process Communication,进程间通信。Binder/Messenger/AIDL,)全部建立在「绑定服务」之上
经典场景:音乐播放器
切后台锁屏:用startService启动,页面关掉音乐不会停
重新打开 APP:用bindService绑定服务,拿到播放控制权(暂停 / 切歌 / 看进度)
彻底关闭音乐:页面解绑 + 调用 stopService,缺一不可
操作系统:一座大型工厂
进程:独立的生产车间,
线程:车间里各司其职的工人
(六)Android权限
系统预定义权限 | 允许应用 |
|---|---|
android.permission.INTERNET | internet,访问网络 |
android.permission.ACCESS_FINE_LOCATION | access_fine_location 获取精确 GPS 位置信息 |
android.permission.READ_EXTERNAL_STORAGE | read_external_storage 读取外部存储(如手机相册、文件) |
android.permission.RECORD_AUDIO | record_audio 通过麦克风录音 |
android.permission.CAMERA | 打开摄像头拍照或录像。 |
android.permission.READ_CONTACTS | read_contacts 允许应用读取用户联系人列表 |
<!-- 在服务端的 AndroidManifest.xml 中 --> <permission android:name="com.example.myapp.ACCESS_SECURE_DATA" android:protectionLevel="normal" />
android:name:权限的唯一标识,通常用包名作为前缀。
android:protectionLevel:权限的保护级别,常见的有:
normal:低风险,系统会自动授予。
dangerous:高风险,需要用户运行时确认。
signature:仅当调用方的 APK 与服务端 APK 使用相同签名时,才会授予。这是系统服务间通信最常用的级别,安全性最高。
二、AIDL
Android接口定义语言
(一)AIDL操作步骤
1. 合同(跨进程接口协议):
- 新建后缀为.aidl的文件(IMyAidlInterface.aidl),定义服务的编程接口;
- Android SDK自带的工具会解析该文件,自动生成对应的抽象类,这个抽象类可以完成接口实现与进程间通信(IPC)的底层处理工作;
2. 服务端:MyAidlService
在服务中扩展(继承extends/实现implements)抽象类,实现具体的业务逻辑
3.客户端(Activity)
(二)数据流向:定向 Tag
AIDL 参数定向标记,作用:告知 Binder 驱动数据传输方向
| 标记 | 适用对象 | 基础类型是否可用 | 流向逻辑 |
|---|---|---|---|
| in | Parcelable / 集合 / 基础类型 | 可用(默认) | 客户端→服务端,修改不回传 |
| out | 仅 Parcelable、集合 | 不可用 | 客户端传值丢弃,服务端填充数据传回 |
| inout | 仅 Parcelable、集合 | 不可用 | 客户端原始数据传给服务端,修改后同步返回 |
(三)调用模式:oneway关键字
AIDL 中一个用来改变跨进程调用(IPC)行为的关键字。((同进程调用时,oneway不生效))
核心是将默认的同步阻塞调用(“打电话”),变为异步非阻塞调用(“发短信”)。
1. 使用与限制
返回值:因为不等待结果,被
oneway修饰的方法返回值必须是void参数:
oneway不影响参数传递方向,参数仍需配合in、out、inout使用位置:可以单独修饰方法,也可以修饰整个接口,使其所有方法都变成
oneway
// 单独修饰方法 interface IPlayer { oneway void play(); // 异步,客户端不等待 int getCurrentPosition(); // 同步,客户端需要等待结果 } // 修饰整个接口(所有方法均为 oneway) oneway interface IUpgradeCallback { void onProgress(int percent); // 隐式 oneway void onStatusChanged(int status); // 隐式 oneway }2. 典型场景
发送指令,不关心结果:如播放/暂停音乐、开始下载任务等。
回调接口(服务端 → 客户端):服务端向客户端发送进度更新或状态通知。在项目中,
ICallback接口被标记为oneway,就是为了防止服务端因等待客户端处理回调而被阻塞
3. 注意事项
(1)无法通过RemoteException感知服务端异常
oneway的优势是:客户端调用不阻塞;代价是:失去了即时异常反馈。
在设计oneway方法时,务必要确保调用方“不需要知道执行结果”,或者通过状态查询/回调机制来异步获取结果。在系统服务(如升级、下载、播放控制)中,这种设计非常常见。
当客户端调用一个同步 AIDL 方法时,如果服务端进程死亡或抛出异常,RemoteException会立即抛给客户端。客户端可以捕获它并做出处理,例如重试或提示用户。
try { int result = mService.add(100, 200); // 同步调用 } catch (RemoteException e) { // ✅ 能捕获到异常,客户端知道出错了 e.printStackTrace(); // 可以尝试重连 reconnect(); }当调用oneway方法时,情况完全不同:
客户端调用
mService.onewayMethod()后立刻返回,RemoteException此时无法抛给客户端,因为调用已经“结束”了。如果服务端在执行
onewayMethod()时抛出了异常,这个异常只会被服务端自己捕获,客户端无法感知
// 服务端方法 oneway void doSomething() { throw new IllegalStateException("服务端内部错误"); // 客户端不会收到这个异常 }客户端调用mService.doSomething()不会收到任何异常。客户端只会看到调用“成功返回”,但实际上服务端的业务逻辑可能已经失败了。
解决方案
既然客户端无法通过异常感知错误,系统服务通常会采用以下两种设计方案:
方案一:增加状态查询接口
客户端调用oneway指令后,如果有需要确认执行结果的需求,可以额外提供一个getStatus()方法:
interface IUpgrade { // 异步指令 oneway void startUpgrade(String path); // 同步查询:客户端调用后轮询这个方法来确认执行结果 int getUpgradeStatus(); }方案二:状态变更通过回调返回
客户端先注册一个回调接口,服务端在oneway方法执行完毕后(无论成功还是失败),通过回调把结果通知给客户端:
interface IUpgrade { oneway void startUpgrade(String path); } interface IUpgradeCallback { // 服务端执行完 startUpgrade 后,调用此回调通知结果 oneway void onUpgradeResult(int code, String message); }(2)在服务端是串行执行的
这是oneway最关键且最容易被忽视的特性:从同一个客户端线程发往同一个服务端 Binder 对象的oneway调用,在服务端是串行执行的
普通 AIDL 方法:不同客户端、不同线程的调用可以并发执行。服务端 Binder 线程池中有多个线程,可以同时处理多个请求。
| 条件 | 是否串行 |
|---|---|
| 同一个客户端线程 → 同一个 Binder 对象 | ✅串行(保证顺序) |
| 不同客户端线程 → 同一个 Binder 对象 | ❌ 不保证(但仍按顺序入队) |
| 不同客户端 → 同一个 Binder 对象 | ❌ 不保证(可以并行处理) |
假设客户端依次调用:
mService.onewayMethodA(); // 调用1 mService.onewayMethodB(); // 调用2 mService.onewayMethodC(); // 调用3 //这三个调用在服务端的执行顺序永远是 A → B → C(先到先处理)。 //A 执行完,才会执行 B,再执行 C。这是因为:oneway方法不返回结果,如果允许并发执行,可能导致指令顺序错乱。例如:
mService.stop(); // 调用1 mService.play(); // 调用2 //如果这两个 oneway 调用在服务端并发执行, //play() 可能先被处理,而 stop() 后执行,导致状态异常。串行化保证了指令执行的顺序与客户端调用的顺序一致,避免了这类状态错乱问题。
在系统服务中,许多控制类方法被设计为oneway,正是为了利用这个“顺序保证”特性。例如:
音频控制:
pause()→play()→seekTo()升级流程:
prepare()→start()→cancel()
(四)Binder 死亡监听机制
在真实系统中,服务端进程可能因为以下原因突然死亡:
进程崩溃:服务端代码出现未捕获异常,进程被系统杀死。
系统内存不足:系统(Low Memory Killer)回收后台进程,服务端进程被杀死。
用户手动停止:用户在“设置 → 应用”中强行停止应用。
系统重启:手机重启,所有进程被清空。
在 Framework 开发中,系统服务(如 AMS、WMS)如果突然死亡,所有依赖它的 App 都会受到影响。因此,客户端必须能感知服务端的死亡,并做出相应处理(如重连、清理资源、提示用户)。
Android 提供了DeathRecipient接口,允许客户端在Binder 对象上注册一个“死亡监听器”。当 Binder 对象所在的进程死亡时,系统会回调客户端的binderDied()方法。
| 步骤 | API |
|---|---|
| 在 Binder 对象上注册死亡监听 | IBinder.linkToDeath(DeathRecipient recipient, int flags) |
| 取消注册死亡监听(防止内存泄漏)。 | IBinder.unlinkToDeath(DeathRecipient recipient, int flags) |
| 当 Binder 对象所在进程死亡时,系统回调此方法。 | DeathRecipient.binderDied() |
| 步骤 | 核心动作 | 在哪里写 | 关键说明 |
|---|---|---|---|
| 实现死亡监听接口 | 创建DeathRecipient对象,重写binderDied() | 客户端 Activity / 任意持有 Binder 引用的类 | binderDied()运行在Binder 线程池,不能直接更新 UI,需要切到主线程 |
| 注册死亡监听 | 在onServiceConnected()中拿到IBinder后,调用linkToDeath() | ServiceConnection.onServiceConnected() | 注册时机必须是在绑定成功后,否则IBinder为 null;linkToDeath可能抛出RemoteException,需 try-catch |
| 处理死亡回调 | 在binderDied()中:① 清空引用 ② 更新 UI ③ 触发重连 | DeathRecipient.binderDied() | 重连时需加防重复标志(如isReconnecting),防止多次绑定;重连成功后需要重新注册回调监听器和重新注册死亡监听 |
| 取消注册 | 在客户端销毁或解绑时调用unlinkToDeath() | onDestroy()或解绑方法中 | 必须调用,否则会内存泄漏(服务端已死但客户端仍持有DeathRecipient引用) |
| 重连后恢复状态 | 重新绑定成功后,重新注册回调、重新注册死亡监听 | onServiceConnected()(重连后的再次回调) | 重连成功后,所有之前注册的监听器都会失效,需要重新执行步骤 2 的注册逻辑 |
(五)回调管理:RemoteCallbackList
在 AIDL 跨进程通信中,服务端经常需要主动向客户端推送消息(如升级进度、状态变化)。这需要客户端把回调接口注册到服务端,服务端持有这些回调并在适当时机调用。
然而,当 Client 和 Server 处于不同进程时,客户端进程可能随时崩溃或退出。如果服务端仍向已死亡的客户端发送回调,会触发RemoteException,轻则抛出异常,重则导致服务端进程崩溃。"Takes care of the grunt work of maintaining a list of remote interfaces, typically for the use of performing callbacks from a Service to its clients."——官方
替你打理维护远程接口列表的繁琐工作,典型用途是从 Service 向其客户端执行回调。
RemoteCallbackList:存放 AIDL 回调接口的“安全容器”。让服务端能安全地向多个客户端发送回调,并自动清理已经死亡的客户端,防止内存泄漏和服务端崩
//泛型定义 public class RemoteCallbackList<E extends IInterface> // E 必须是 AIDL 接口类型int count = mListeners.beginBroadcast(); // 开始遍历 for (int i = 0; i < count; i++) { mListeners.getBroadcastItem(i).onProgress(50); } mListeners.finishBroadcast(); // 必须调用,否则会死锁并泄漏(六)版本兼容性:Stable AIDL
AIDL 版本兼容性在系统级开发是一个非常重要的话题。当你的 AIDL 接口更新后,需要确保旧版本的客户端(比如已发布的 App)仍然能正常工作,不会因为接口变动而崩溃。
当前 Android 解决这个问题的标准方案是Stable AIDL,它提供了一套完整的版本管理机制。Stable AIDL 是 Android 在 Android 10引入的一种机制,它主要围绕几个核心概念运作:
- 接口即契约 (Interface as a Contract):一个 AIDL 接口一旦发布,就成为一个永久、向后兼容的契约。这意味着不能修改或删除已发布版本中的任何方法,只能进行添加。
- 冻结 (Freeze)与版本控制 (Versioning):通过“冻结” (Freeze)操作,为 AIDL 接口创建一个不可变的快照。每一个快照对应一个版本。新版本的接口会与旧版本并存,系统会自动进行管理。
- 版本号管理:每个 Stable AIDL 接口都在`aidl_api/<interface_name>/`目录下有自己的版本历史。构建系统通过 `versions_with_info` 等字段来管理这些版本。
三、Android系统权限安全校验
在 Android 系统中,运行着许多拥有极高权限、提供核心功能的系统服务(System Service),它们运行在独立的system_server进程中。例如,负责管理四大组件生命周期的ActivityManagerService (AMS),以及负责管理窗口的WindowManagerService (WMS)。
客户端 App 通过 Binder 机制与这些系统服务进行跨进程通信(IPC),以调用其提供的功能。然而,系统服务级别的 AIDL 接口默认是没有权限屏障的——任何进程只要能拿到 Binder 引用,就能调用服务端暴露的所有方法。
因此,权限校验是系统服务的第一道安全防线。系统服务在执行敏感操作前,必须校验调用方的身份(UID/PID)和权限(Permission),确保调用者是合法的、有授权的。这套校验逻辑内置于 AMS、WMS 等核心系统服务中,是 Android 安全模型的重要组成部分。
客户端(App)不会主动去校验自己,在实际系统交互中,客户端想要敲开服务端的门,是需要先“出示证件”的:
客户端必须在自己的
AndroidManifest.xml里声明<uses-permission>(比如我要用ACCESS_MY_SERVICE权限)。当客户端发起 Binder 调用时,系统内核(Binder 驱动)会自动把客户端的UID(身份证号)和PID(临时工号)附加在数据包上发给服务端。
客户端无法伪造这个 UID(这是 Linux 内核干的活,App 没权限改)。
AIDL 服务端实现一套完整的身份认证与权限校验体系?
| 时间点 | 动作 | 关键说明 | 操作人 |
|---|---|---|---|
| 开发阶段 | 自定义系统权限 | 在AndroidManifest.xml中用<permission>声明权限名称和保护级别(normal/dangerous/signature)。 | 服务端 |
| 运行时 | 识别调用者身份 | 在onTransact()或 AIDL 接口方法实现中,通过Binder.getCallingUid()/Binder.getCallingPid()获取身份(UID 是校验首选)。 | 服务端 |
| 运行时 | 校验调用者权限 | 在onTransact()中使用checkCallingPermission(权限名)进行校验,确认调用方是否拥有相应权限。 | 服务端 |
| 运行时 | 拦截 / 放行 | 权限不足 → 抛出 权限充足 → 调用 | 服务端 |
四、Binder 连接池
Android 中一种用单个 Service 管理多个业务 Binder 的设计模式。客户端只需绑定一次,通过查询码获取不同业务的 Binder,从而解决多模块多 Service 带来的资源开销和代码耦合问题。
一个 Service 管理多个 Binder,新增模块只需在queryBinder中增加一个分支,无需新增 Service,实现模块解耦和代码精简。
问题:在传统 AIDL 使用方式中,一个 Service 只能绑定一个 AIDL 接口。当业务模块增多时,每个模块都需要独立的 Service,导致服务数量膨胀、资源开销增大、客户端绑定逻辑复杂。
解决方案:引入Binder 连接池模式。 服务端只暴露一个统一的 Service 和一个
IBinderPool接口,客户端通过queryBinder(int code)方法,根据业务码获取对应的 Binder,再转为具体的业务接口使用。
五、结语
这几天的学习核心是AIDL。
学习起点是 Linux 内核的进程隔离机制——不同 App 天然运行在不同进程;同一 App 内的不同组件(如 Activity、Service)默认运行在同一进程,但可通过android:process属性拆分到不同进程,从而实现进程隔离。
因为有进程隔离,进程间通信(IPC)就成了绕不开的课题。Android 提供了多种可直接调用的 IPC 方式,例如基于 Binder 机制的 Intent(跨进程场景)、Messenger、AIDL、ContentProvider,以及基于 Linux 内核的管道、Socket 等。
接着正式进入 AIDL 的学习:先明确什么场景下使用它,由此延伸到 Service 的两种启动形式——startService(后台长期运行)和bindService(页面实时操控)。bindService又对应三种跨进程通信方式:扩展 Binder 类(同 App 同进程)、Messenger(跨进程,轻量串行)、AIDL(跨进程,多线程并发)。随后补充了 Binder 机制原理、Parcelable 序列化规范、Android 权限体系等前置知识,并用一个简单 Demo 完成实操。
在 Demo 基础上,对 AIDL 进行了功能拓展:
定向 Tag(in / out / inout):控制跨进程传递数据的流向;
Binder 死亡监听机制:客户端通过
linkToDeath感知服务端进程死亡并容错重连;回调管理
RemoteCallbackList:让服务端能安全地向多个客户端发送回调,并自动清理已死亡的客户端;Android 系统权限校验:服务端在
AndroidManifest.xml中用<permission>定义权限,客户端用<uses-permission>声明使用;服务端通过Binder.getCallingUid()识别调用者身份,结合checkCallingPermission()校验权限,校验失败则抛出SecurityException拦截;Binder 连接池:服务端只暴露一个统一 Service 和一个
IBinderPool接口,客户端通过queryBinder(int code)查询并获取不同业务模块的 Binder,实现多接口复用