跨进程调用这个坎:为什么应用之间不能直接"打电话"
先从一个我实际遇到的场景说起。一次项目里需要让一个音乐播放Service单独跑在另一个进程里,主界面要实时拿到当前播放进度、歌名,还要能下达"暂停""切歌"指令。最朴素的想法是写个静态方法直接调用,但Android的进程隔离机制不允许A进程直接new一个B进程的对象再调它的方法——每个进程有自己的内存空间、自己的虚拟机实例,地址与对象根本不互通。这时候就必须引入一套"跨进程通信"机制,而AIDL(Android Interface Definition Language,Android接口定义语言)正是Google为这套机制准备的官方标准化工具。
不少人一开始会把AIDL理解成"一种让两个进程互相调方法的黑魔法",其实没那么玄。它本质上是一种简化声明方式:你只用写一个接口文件,描述清楚"我想跨进程暴露哪些方法",构建工具会自动把接口编译成Java代码,负责把参数打包、跨进程传输、再还原成目标进程里的方法调用。真正干活的是底层Binder驱动,但开发者不需要直接面对Binder的事务号和底层数据结构。AIDL的价值在于:把"跨进程调用"这种生态复杂度封装成了接近"本地接口调用"的体验,同时保留了清晰的边界感。
这篇文章面向两类读者:第一类是刚接触进程通信、准备在自己的App里使用AIDL的Android开发;第二类是把AIDL当面试八股背过、但实际动手时总会遇到编译或运行时问题的同学。我会按照从原理到工程落地再到排错的顺序走一遍,所有细节都来自项目里的真实实践,不是文档搬运。
拆开AIDL的包装:接口定义、代码生成与Binder的关系
在写代码之前,先建立一张脑内地图:AIDL到底处于整个调用链路里的哪个位置。
2.1 一次跨进程方法调用的完整旅程
想象你在进程A里调用了一个位于进程B的方法getBookList()。进程A里的调用对象其实是一个代理(Proxy),它长着和目标接口一模一样的方法签名,但方法体内没有业务逻辑,只负责把方法名、参数值塞进一个Parcel数据包里,然后通过Binder.transact()把数据包交给内核里的Binder驱动。Binder驱动根据你持有的Binder令牌,找到目标进程B里对应的Stub对象,唤醒B进程里的Binder线程池,把数据包交过去。Stub解包后调用真正的业务实现类,拿到返回值后再原路打包返回。
这整条链路里,AIDL干的事就是:编译时根据你写的.aidl接口文件,自动生成Proxy和Stub的样板代码。你写的接口方法就是两边的通用契约,两端都按这个契约解析数据。
2.2 编译产物:aidl文件究竟变成了什么
在Android Studio里每创建一个.aidl文件并完成一次构建后,去app/build/generated/aidl_source_output_dir目录下翻一翻,你会看到同名的.java文件。以IBookManager.aidl为例,生成的IBookManager.java内部结构大致是这样:
- 继承
android.os.IInterface的公开接口,声明了你定义的业务方法。 - 内部静态抽象类
Stub,继承Binder并实现IBookManager接口,它同时扮演"服务端骨架"和"Binder实体"。 Stub内部还有一个Proxy类,服务于调用方进程,负责把方法调用编码成事务并发送出去。Stub类里维护了一个INTERFACE_TRANSACTION常量(接口标识)以及每个方法对应的TRANSACTION_xxx常量(事务编号)。
这里有个很容易被忽略的关键点:服务端到底继承的是Stub,而客户端拿到的外型是Proxy。两端能对上,靠的是同一个接口文件、同一个包名、同一个DESCRIPTOR字符串。很多"怎么连不上""方法调不通"的问题,根子都出在服务端和客户端各自维护了一份不同包名或改过签名但没同步的aidl文件上。
2.3 为什么偏偏选Binder而不是Socket或共享内存
这个问题的答案,直接解释了AIDL存在的必要性。进程间通信手段很多,Socket基于网络栈实现,通用但性能差、数据拷贝次数多、还需要自己处理连接状态;共享内存效率高但同步和生命周期管理极其复杂,容易踩内存踩踏。Android选择的Binder采用了"一次拷贝 + 内核态校验"的设计:发送方把数据拷入内核,接收方通过内存映射直接读取,整个过程只需一次拷贝,性能远好于Socket的多次拷贝;同时Binder自带UID/PID校验能力,可以做权限控制,正好匹配Android"每个应用一个进程、进程间需要安全隔离"的模型。
AIDL不替代Binder,它是Binder的用户态脚手架。没有AIDL,你依然可以直接手写Binder代码实现跨进程调用,但需要自己处理Parcel打包、事务分发、异常传递等琐碎工作,代码量大概是AIDL方案的5倍以上,而且极易出错。
敲出第一个AIDL接口并让工程顺利编译
下面对应实际工程,走一遍最基础的AIDL接口创建流程。我用的环境是Android Studio最新稳定版,Gradle插件版本8.x,Kotlin和Java混用项目。
3.1 创建AIDL文件的位置与目录规范
新建AIDL文件时,很多人会困惑它应该放在src/main/java还是src/main/aidl。在老版本Gradle里,AIDL文件默认放在src/main/aidl包目录下;但Android Studio提供了快捷方式:在任意Java包上右键 → New → AIDL → AIDL File,它会自动在src/main/aidl下创建和所选包名一致的目录结构。
这里必须强调一个规范:AIDL文件的包名必须和.java文件包名保持同一个包。比如在com.example.aidl包下创建IBookManager.aidl,那么文件里的第一行必须是package com.example.aidl;。如果包名不一致,编译阶段会直接报"找不到类"之类的奇怪错误,因为生成的Java类会放在包名对应的位置,而调用方代码引入的是另一个包名下的类。
3.2 最简接口文件的语法与基本数据类型
一个完整的IBookManager.aidl长这样:
// IBookManager.aidl package com.example.aidl; import com.example.aidl.Book; interface IBookManager { List<Book> getBookList(); void addBook(in Book book); }AIDL接口的语法看起来像Java接口,但支持的类型集合是有限制的:
- Java八种基本数据类型以及String、CharSequence可以直接用。
List、Map可以直接用,但元素类型必须是AIDL支持的类型。- 所有自定义类型(比如
Book)必须是Parcelable,并且必须显式import,即使它们在同一个包内也要import。 - 接口文件里不允许有常量定义(相比Java接口的限制更严),不能有静态成员。
我把常用支持类型整理成一张表:
| 类型 | 支持情况 | 说明 |
|---|---|---|
| boolean、byte、char、short、int、long、float、double | 直接支持 | 无特殊处理 |
| String、CharSequence | 直接支持 | 跨进程传递带字符编码处理 |
| List、ArrayList | 直接支持 | 元素类型必须受支持 |
| Map、HashMap | 直接支持 | key/value类型必须受支持 |
| Parcelable对象 | 需import | 对象必须实现Parcelable接口 |
| AIDL接口本身 | 需import | 可传递Binder引用 |
| Bundle | 直接支持 | 常用于辅助传参 |
| InputStream、OutputStream | 有限支持 | 不推荐,复杂且易错 |
3.3 自定义Parcelable对象的定义方式
定义Book类时,除了让它实现Parcelable接口,还必须额外写一个同名的.aidl声明文件。这个文件极其简短,只需要包名和parcelable关键字的声明,不需要任何成员变量。
// Book.aidl package com.example.aidl; parcelable Book;这一步是很多新手会卡住的点:明明Book类已经实现Parcelable了,为什么还要一个空的Book.aidl?原因在于AIDL编译器需要知道"这个类型是允许跨进程传输的"。没有这个声明,编译器不会把Book识别为合法参数类型。
Book.java本身要完整实现writeToParcel、CREATOR和构造函数。这里有个容易踩的坑:Parcelable实现里字段读写顺序必须和writeToParcel一致。AIDL跨进程传输时完全按照Parcel的读写顺序还原对象,字段顺序错了不会报编译错,但运行时会拿到错位的值,这是最阴间的一类Bug。
3.4 编译链路中容易翻车的地方
.aidl文件创建好后,执行同步与构建。如果一切正常,build/generated/aidl_source_output_dir下会生成对应的Java文件。但如果本地SDK Build-Tools版本太老,或Gradle插件版本和AIDL工具不兼容,会报一些比较误导性的错误,比如"AIDL file is missing"或者"connect EPIPE"。
我建议优先检查这几项:
- Android SDK Build-Tools版本不要太低,建议30.0.0以上。
- aidl目录结构和包名是否匹配。
- 自定义Parcelable类是否设置了
CREATOR字段,且字段名必须叫CREATOR(全大写的静态常量)。 - 如果用了Kotlin,自定义类的Parcelable实现建议用
Parcelize注解,但要注意Parcelize生成的CREATOR字段名和Java版一致,通常没问题。但跨进程传输的类尽量避免使用Kotlin的data class加Parcelize组合,实测在某些AGP版本下会出现序列化异常。
服务端回话:Service内部如何承载并处理跨进程调用
AIDL接口写好后,接下来要做的是把它"挂载"到一个运行中的Service上。服务端的核心任务是:创建Binder实例、在onBind里把它返回给系统,让客户端进程能够拿到这个Binder。
4.1 Service的onBind与Stub的三种实现方式
最标准的方式是让Service持有一个内部类继承IBookManager.Stub:
public class BookManagerService extends Service { private final CopyOnWriteArrayList<Book> bookList = new CopyOnWriteArrayList<>(); private final IBookManager.Stub binder = new IBookManager.Stub() { @Override public List<Book> getBookList() throws RemoteException { return new ArrayList<>(bookList); } @Override public void addBook(Book book) throws RemoteException { bookList.add(book); } }; @Nullable @Override public IBinder onBind(Intent intent) { return binder; } }这段代码里有几个操作层面的细节值得展开。
Stub对象本身就是一个Binder,它会在Binder驱动里注册一个实体节点,当客户端通过bindService拿到IBinder后,就相当于拿到了这个节点的引用。onBind里返回的IBinder就是系统要交给客户端进程的"通信凭证"。如果返回null,客户端bindService会回调onServiceDisconnected。
服务端不要在主线程里直接实现耗时业务。Stub中的方法最终会由Binder线程池调用,但如果你在方法内部访问了UI或做了耗时操作,需要自己安排线程。默认情况下Binder线程池会并发调用多个方法,因此服务端要特别注意线程安全。上面代码里用CopyOnWriteArrayList而不是普通ArrayList,就是为了应对多线程并发读写的场景。但这个类也有坑:跨进程传递ArrayList时,如果元素是自定义Parcelable,必须保证元素实现了Parcelable;CopyOnWriteArrayList本身序列化没问题,但实际传递给客户端时会被还原成ArrayList类型,这在客户端侧接收时要注意。
4.2 服务端进程的配置与生命周期
要让Service真的运行在独立进程里,需要在AndroidManifest.xml里配置:
<service android:name=".BookManagerService" android:enabled="true" android:exported="true" android:process=":remote" />android:process=":remote"表示这个Service跑在应用的一个私有子进程里。如果你写的是android:process="com.example.remote"这样不带冒号的形式,就会跑在一个全局共享进程里,这在多应用场景下会有权限隐患,但也可以用于多应用共享同一个服务进程。:remote是当前应用上下文里的进程名,其他应用无法直接访问,安全性更高一点。
android:exported="true"的意义在于让其他进程的应用也能绑定这个Service。如果只有自己App内部跨进程使用,建议保持exported="false",再通过显式Intent绑定,避免被第三方应用恶意连接。
还有一点:Service跑在哪个进程,它的onCreate、onBind就在哪个进程执行。这意味着服务端的onCreate里可以做一些初始化工作,但这些工作会阻塞当前进程的主线程,同样要注意耗时操作。
4.3 服务端死亡回调与资源清理
跨进程通信最麻烦的地方是:你没法保证对端进程还活着。客户端可能被系统杀掉、用户可能手动清后台,这时候服务端持有的Binder引用并不会立刻失效。系统会在Binder实体所在的进程死亡时向所有持有引用的客户端发送死亡通知,服务端也可以做反向监听,但相对少用。
服务端的资源清理主要靠onDestroy,但要注意Binder线程池是系统级的,Service销毁后已经发出的调用仍可能继续。所以不要在onDestroy里释放掉Stub还依赖的底层资源,否则接下来到达的调用会直接崩掉。稳妥做法是增加一个"标记关闭"的原子布尔值,在Stub方法入口先检查再放行。
客户端这一侧:bindService、连接回调与代理对象
客户端的工作相对轻量,但对API的理解程度决定了你是否会在特定机型上踩到生命周期坑。
5.1 绑定流程与ServiceConnection的正确写法
客户端绑定Service的代码模式比较固定:
private IBookManager bookManager; private boolean bound = false; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { bookManager = IBookManager.Stub.asInterface(service); bound = true; } @Override public void onServiceDisconnected(ComponentName name) { bookManager = null; bound = false; } }; private void bindService() { Intent intent = new Intent(this, BookManagerService.class); intent.setPackage(getPackageName()); bindService(intent, connection, Context.BIND_AUTO_CREATE); }IBookManager.Stub.asInterface(service)的底层逻辑:如果service本身就在当前进程(比如绑定的是同进程Service),它直接返回service作为Stub,实际就是本地调用;如果service来自其他进程,则包装成Proxy。对普通业务代码来说,不需要关心这个分支,但对于调试有一定的启发意义:同进程绑定时,AIDL调用是直接在当前线程执行的,不存在Binder线程池切换。很多人把Service放同进程跑、拿AIDL做"解耦",然后奇怪为什么方法回调不在子线程,原因就在这里。
5.2 客户端调用与异常捕获
拿到bookManager后,调用接口方法:
try { List<Book> books = bookManager.getBookList(); bookManager.addBook(new Book("Android AIDL实战")); } catch (RemoteException e) { // 处理跨进程异常 }RemoteException是所有跨进程调用方法统一声明的受检异常。它在两类情况下出现:一类是Binder驱动无法完成事务,比如目标进程死亡、Binder引用失效、事务数据太大(通常超过1MB就会触发TransactionTooLargeException);另一类是服务端Stub方法内部主动抛出了跨进程异常。
特别注意:跨进程调用会阻塞调用线程。如果在主线程调用一个耗时很长的AIDL方法,会直接卡住UI,严重时触发ANR。Google官方不推荐在主线程进行跨进程调用,但实际项目里总有这种写法。我的建议是:如果方法本身是轻量级的,比如getBookList返回几KB数据,主线程调用问题不大;但涉及大量数据或网络等耗时操作,一定要放到子线程或协程里,并且用newSingleThreadExecutor统一管理,避免每个调用各开线程造成资源浪费。
5.3 自动绑定与手动解绑的平衡
Context.BIND_AUTO_CREATE参数会在绑定时自动创建Service,并在最后一个客户端解绑时自动销毁Service。这个机制很方便,但要注意:如果同进程里多个组件先后绑定同一个Service,解绑时机不当可能导致Service频繁创建销毁。
实际项目里,如果客户端Activity和客户端Service都会用到bookManager,建议在Application级去做一次绑定,再用引用计数维护使用者数量。别偷懒在onStart绑定、onStop解绑,低端机型上Service重建的开销会被放大好几倍。
5.4 进程被杀后的重连机制
客户端进程被系统回收后,系统会重建Activity,但ServiceConnection并不会自动重连。在某些ROM上,你会遇到界面正常但bookManager为null的情况。我的处理方式是在Application的onCreate里主动执行一次bindService,然后暴露一个getBookManager()方法,内部若为null则再次触发绑定。如果绑定失败,还可以用startService的方式保活Service,但保活策略要谨慎,现在主流系统对后台启动Service的限制越来越严格,频繁后台拉起会被系统标记为耗电应用。合理的设计是:只在用户主动进入相关页面时才绑定,离开页面就解绑,后台任务靠WorkManager之类的组件去做,而不是靠Service常驻。
最容易埋雷的三件套:参数方向、自定义对象与线程阻塞
AIDL使用中90%的疑难杂症都集中在三个方面,单独拿出来细说。
6.1 参数方向in、out、inout到底改变了什么
AIDL方法的每个非基本类型参数(除了String等少数类型)前面都要标注方向。这是最容易让人误解的语法。
in:客户端把参数值传入服务端,服务端对参数的修改不会回传客户端。out:客户端不关心传入值,服务端对该参数的赋值会回传到客户端。inout:双向传输,客户端先传入,服务端修改后返回,两端都能看到最新值。
底层影响是:in参数在客户端写入Parcel后即完成任务,服务端解包时无需回写;out参数则反过来,客户端只读一个空对象骨架,服务端填充字段后回写;inout两边都要序列化。序列化开销和潜在Bug数从in到inout递增,所以原则是:能用in就用in,能不用inout就不用inout。
有个经典坑:在in模式下,服务端对参数对象的字段修改,客户端是感知不到的。有人写了一个更新图书信息的方法,参数标成in,服务端改了书名的字段,客户端接着去读发现还是旧值,第一反应是Binder没通,折腾半天才发现是方向标识写错了。
6.2 Parcelable对象跨进程序列化的一致性要求
对于自定义Parcelable对象,跨进程传递时的一致性要求比同进程传递严格得多。这里点出几个最常见的坑:
字段顺序必须一致。writeToParcel里先写bookId再写bookName,那么CREATOR.createFromParcel里必须按相同顺序读。顺序错乱不会编译报错,运行时会拿到完全错位的值。
特殊字段的读写有陷阱。比如String可空时,写入需要判断null,否则读出时可能会拿到"null"字符串或直接NPE。List字段如果可空,写入时建议写成空列表而不是null,因为在某些Parcel实现里writeList(null)会写入负数长度,读出时得到null,两端行为在Android版本之间表现并不完全一致。我在多版本真机测试中发现,Parcel对null的处理在不同系统版本有过差异,最稳妥的做法是约定可空字段必须赋值默认值。
不要用Serializable替代Parcelable。AIDL不支持Serializable对象直接传递,编译器会直接报错。虽然Bundle里允许放Serializable对象,但跨进程传递时效率很低,且容易触发BadParcelableException。
6.3 跨进程调用是同步的,而且是阻塞的,这不是可以谈的条件
AIDL的默认调用方式是同步阻塞的。客户端进程调用方法后,当前线程会挂起等待服务端返回。这个"等待"不受调用方意志控制,即使你的业务只是简单查询,只要服务端卡住,客户端就会一直等。
所以服务端Stub里的方法实现要遵守一个铁律:不要做耗时操作,不要做耗时操作,不要做耗时操作。常见反例包括Service内部直接访问网络、读大文件、做复杂计算,这些都会让Binder线程池的线程被占用,当并发请求变多时,线程池耗尽会导致后到的调用长时间排队,客户端表现为卡顿或超时。
如果你的业务确实需要异步,有两个方案:
- 客户端用线程池或协程发起调用,配合超时机制。
- 服务端使用
oneway关键字修饰接口方法,这样调用会以非阻塞方式发送给服务端,服务端排队处理,客户端立刻返回。但oneway会丢失返回值,也不能保证执行顺序,适合日志上报、通知刷新这类场景。
interface INotifyManager { oneway void notifyBookChanged(Book book); }6.4 线程模型对比:客户端进程 vs 服务端进程
再补充一张线程视角的对照表,方便理解AIDL调用后的代码执行线程:
| 位置 | 执行线程 | 说明 |
|---|---|---|
| 客户端调用的代码 | 调用方线程 | 同步阻塞直到服务端返回 |
| 服务端Stub方法 | Binder线程池线程 | 多个请求并发到达时,多个线程并行执行 |
| 回调接口(客户端Listener) | Binder线程池线程 | 服务端反向调用客户端时,回调运行在客户端Binder线程池 |
oneway方法 | 客户端调用线程立刻返回,服务端排队执行 | 不保证顺序,无返回值 |
理解这个表的意义在于:服务端返回后,客户端线程恢复执行,但如果你在AIDL方法里注册了一个回调Listener,服务端在某个时间回调它时,回调执行在客户端的Binder线程池,而不是你当初发起调用的线程。这意味着回调里不能直接刷新UI,必须切回主线程。我用一个简单的Handler(Looper.getMainLooper())来切,协程项目里也可以用withContext(Dispatchers.Main)。
编译报错、运行时崩溃与进程被杀:高频问题的排查清单
作为实战总结,我把这些年遇到的AIDL问题分成三类,每一类都给出可直接照做的排查路径。
7.1 编译阶段的"幽灵错误"如何定位
现象一:AIDL文件创建后,项目同步报错,提示找不到生成的类或找不到符号。先看生成目录是否存在文件,如果存在,检查是否改过接口方法后没有重新构建。Android Studio有时不会自动增量编译aidl文件,我习惯手动执行一次Build > Clean Project。如果生成目录为空,问题大概率出在soureSets配置上,检查build.gradle里是否误删了src/main/aidl这个sourceSet。在Kotlin DSL里如果手写了android.sourceSets,很容器丢漏。
现象二:报错信息里出现了"duplicate class"或者"overrides final method"。这通常是在Java侧手动写了和生成类同名的类,比如你在业务包里自己创建了一个IBookManager.java,而AIDL生成的也是IBookManager.java,名字撞车。解决方案:删除手写的类,永远不要手动创建与AIDL生成类同名的文件。
现象三:报错"AIDL文件中的接口方法抛出了不受支持的异常类型"。AIDL方法可以声明抛出RemoteException,也可以声明抛出自定义Exception吗?答案是不行。跨进程的异常传递只支持RemoteException及少数系统异常。如果你的业务需要把校验异常传回客户端,建议在返回对象里加一个错误码字段,不要在接口方法上声明自定义异常。
7.2 运行时"可以绑定但调用无反应"的问题排查
这类问题比编译错误更加恼人。常规现象:客户端成功回调onServiceConnected,但调用bookManager.getBookList()后线程卡死,或者返回结果为空。
排查顺序:
- 在服务端Stub方法入口加日志,确认方法是否被调用。如果没进方法,检查服务端进程是否真的创建了,用
adb shell ps | grep 包名看进程列表。 - 如果进了方法但客户端还卡着,很可能是方法内部真的执行了耗时操作,把Binder线程占死了,用
adb shell kill -3 服务端进程PID抓ANR trace看栈。 - 如果方法正常返回但客户端拿到空数据,检查自定义Parcelable的读写顺序,以及字段是否为null。
- 检查是否开启了混淆且没有保留AIDL相关类。Release包下AIDL经常出诡异问题,和混淆配置有很大关系。必须在
proguard-rules.pro里对携带AIDL的包做keep:
-keep class com.example.aidl.** { *; } -keep class * implements android.os.IInterface { *; }7.3 Binder死亡与重连:进程级稳定性设计
当客户端调用AIDL方法抛出DeadObjectException,说明Binder实体已经死亡。这种异常常见于系统内存不足时杀掉了服务端进程,或用户手动清后台时连带着清理了服务端进程。项目里如果对稳定性有要求,推荐做法是:
- 用
linkToDeath注册死亡回调,在服务端进程死亡时收到通知并触发重连。 - 在异常捕获里判断
RemoteException的类型,对DeadObjectException做单独处理,比如自动重试一次。 - 客户端和服务端约定好重连的最大次数和退避间隔,避免死循环疯狂重连。
这里给一个简化但可用的死亡监听工具类思路:
public static void linkToDeath(IBinder binder, IBinder.DeathRecipient recipient) { try { binder.linkToDeath(recipient, 0); } catch (RemoteException e) { e.printStackTrace(); } }DeathRecipient.binderDied()回调同样运行在客户端Binder线程池,里面要做的核心事是置空bookManager引用并触发重新绑定逻辑,记得用Handler切主线程。
7.4 方向标识引发的一个真实Bug复盘
最后复盘一个让我印象深刻的Bug。某个版本里,客户端传入一个Book对象到服务端,服务端修改了price字段,返回后客户端显示的还是旧价格。代码review了半天,最终发现方法签名里参数标的是in而不是inout。但那版需求确实不需要反向传值,所以问题不在接口定义,而在客户端引用了服务端修改后的对象字段。这个案例的教训是:使用in方向参数时,绝对不要期望服务端的修改能传回客户端,这是一个文档层面写得很清楚、但编码时被忽略的语义。为了防再犯,我在项目规范里加了一条:跨进程传对象时,凡是需要回传服务端变更的数据,必须在返回值里显式返回新对象,而不是依赖对象引用,因为引用本身是不会跨进程的。
写在最后的选型思考:AIDL不是唯一答案
做技术选型的时候,最怕的就是"手里拿着锤子,看什么都像钉子"。AIDL确实强大,但它不是所有跨进程场景的最优解。
如果你的场景只是前台Activity跟同一个应用内的后台Service通信,而且不需要高并发、不需要双向大量数据传输,Messenger其实是一个更轻量的选择。它内部封装了AIDL,基于Handler消息驱动,天然线程安全,缺点是不支持跨进程方法调用只支持消息传递。
如果是不同应用之间的数据共享,ContentProvider比AIDL更合适,因为系统对ContentProvider有完善的权限管理和URI授权机制,而AIDL要自己做权限校验。
如果是进程间需要广播通知,用LocalBroadcastManager(现在推荐registerReceiver配RECEIVER_EXPORTED标志)或者LiveData在进程内做就够了,完全不需要上AIDL。
AIDL最能发挥价值的地方,是那种接口明确、调用密集、需要双向交互、甚至需要跨应用复用同一套服务能力的场景,比如系统级的音乐播放控制、输入法框架、插件化架构里的宿主-插件通信。在这些场景下,AIDL的接口契约能力能帮你把边界划得很干净,编译期的类型检查也能把很多低级错误提前挡在门外。
回看这些实践,有一个一直受用的心得:跨进程通信的本质问题不是技术难度,而是边界意识——你的对象不会真的飞过去,你的修改不会凭空回来,你的线程不会因为切了进程就变安全。把这条边界记在脑子里,再用好AIDL这个标准工具,绝大多数跨进程需求都能稳定落地,而且不会把自己绕进去。