1. 课程导入
在Android系统中,应用进程默认运行在独立的沙箱环境里,拥有各自的虚拟地址空间、文件权限与运行时资源。一个应用想要调用另一个应用的能力、把大数据交给系统服务处理、让多个进程协同完成同一项任务,就必须跨越进程边界。这个跨越边界的过程,就是进程通信(IPC);而当数据量很大、拷贝成本很高时,还需要借助共享内存来减少复制、提升吞吐。本课程围绕这两条主线展开,帮助你从“会用API”走向“理解机制、能定位问题、可设计工程方案”。
一、核心概念:从进程隔离到跨进程调用
Android基于Linux内核,进程之间天然隔离。每个应用进程通常对应一个Linux用户ID,拥有独立的Dalvik/ART虚拟机实例。系统通过Zygote孵化应用进程,通过Binder驱动提供跨进程调用通道。Binder并不是简单的“函数调用转发”,它包含代理对象、实体对象、引用计数、线程池、事务缓冲区等一整套机制。客户端拿到的往往是Proxy,服务端真正执行的是Stub,二者通过驱动完成数据编组与线程调度。
共享内存则走另一条路径:它不依赖Binder搬运大块数据,而是让多个进程把同一段物理内存映射到各自虚拟地址空间。Android中常见形式包括Ashmem、MemoryFile、SharedMemory、AHardwareBuffer以及底层mmap。共享内存的关键难点不在“映射”,而在同步、生命周期和访问控制:谁先写、谁后读、何时释放、如何避免越界和竞态,都是工程中必须回答的问题。
二、方法流程:一次跨进程调用的完整链路
以AIDL调用为例,客户端调用接口方法后,实际经历如下阶段:
- 接口声明:在
.aidl文件中定义方法、参数方向(in/out/inout)与可序列化类型。 - 代码生成:构建系统生成
Stub与Proxy,Proxy负责把参数写入Parcel。 - 驱动转发:Binder驱动把事务从客户端线程投递到服务端Binder线程池。
- 服务端执行:Stub的
onTransact解析Parcel,调用真实业务实现。 - 结果回传:返回值或异常沿原路返回,客户端线程被唤醒。
共享内存的流程则不同:通常先由一端创建共享区域,另一端通过文件描述符或Binder传递句柄完成映射,然后双方约定读写偏移、长度和同步方式。若涉及跨进程同步,常配合Mutex、ConditionVariable或Fence使用。
// AIDL 接口示例
interface IDataService {
int sendData(in byte[] payload);
void readShared(in SharedMemory memory);
}
// 共享内存创建示例(Kotlin)
val sharedMemory = SharedMemory.create("ipc_shm", 4096)
val buffer = sharedMemory.mapReadWrite()
buffer.putInt(0, 42)
sharedMemory.setProtect(OsConstants.PROT_READ)
三、案例与场景:什么时候用Binder,什么时候用共享内存
假设你要做一个跨进程的图片处理服务。如果只是传递“处理某张图片”的指令和状态码,用AIDL即可;如果要把一张几十兆的位图交给另一个进程处理,继续用Binder就会触发事务缓冲区限制,甚至抛出TransactionTooLargeException。此时更合理的做法是:用Binder传递共享内存句柄,让两个进程映射同一块内存,图片数据只写一次、读一次,避免多次拷贝。
| 通信方式 | 典型数据量 | 是否跨应用 | 主要优势 | 主要风险 |
|---|---|---|---|---|
| Binder / AIDL | 小到中等 | 支持 | 调用语义清晰、权限可控 | 事务大小限制、线程阻塞 |
| Messenger | 小 | 支持 | 基于消息队列、实现简单 | 并发能力弱、不适合大数据 |
| ContentProvider | 中等 | 支持 | 数据共享规范、权限模型成熟 | 性能一般、接口偏重 |
| Socket | 中等到大 | 支持 | 灵活、可跨设备 | 需自行处理协议与安全 |
| 共享内存 | 大 | 支持 | 零拷贝、高吞吐 | 同步复杂、生命周期难管理 |
四、误区与复盘:初学者最容易踩的坑
复盘时建议按“现象—链路—根因—验证”四步走:先确认是调用超时、数据错误还是进程崩溃;再沿Binder事务或共享内存映射链路逐段排查;然后定位到权限、大小、同步或生命周期问题;最后用日志、dumpsys、strace或perfetto验证结论。只有把机制、接口和排障串起来,才算真正掌握Android进程通信与共享内存工程。