简介:面向安卓初学者的AIDL入门示例项目,演示了通过AIDL接口实现跨进程通信(IPC)的完整流程。资源适合刚开始接触安卓组件间通信、希望掌握远程服务与客户端绑定机制的开发者,可直接导入Eclipse工程运行并观察调用效果。压缩包共58个文件,大小仅181KB,主要包含7个Java源码、9个AIDL接口定义文件、20个编译后的class字节码、8张界面PNG图片、4个XML配置以及APK、prefs、dex等文件,整体轻量且配套完整。已有1797人学习下载。通过该项目可直观理解AIDL文件定义、Stub实现、Service服务注册、客户端绑定与远程调用等关键步骤,也涵盖了Eclipse工程配置与资源文件组织方式,既可作为课程设计或期末作业的参考,也能用于面试前快速回顾安卓跨进程通信核心知识点。 第一次接触AIDL,很多人第一反应是“这是个挺底层的东西,现阶段用不上”。实际搞Android开发时间长了你会发现,AIDL不光是Binder机制的“门面”,更是跨进程通信里绕不开的一条标准路线。这次我就把一套最简单的AidlDemo从建工程到跑通的全过程原样记录下来,重点是把我踩过的坑、AS里那些容易让人迷惑的细节都标出来。
这个Demo很小,但解决的是一个很实在的问题:怎么让两个不在同一个进程里运行的模块,像调用本地接口一样去调用对方的方法。适合刚学完四大组件的开发者、准备系统梳理Binder知识点的求职者,以及第一次在项目里引入多进程通信的Android工程师参考。读完你不仅能复现一个可运行的Demo,还能顺手弄清楚AIDL文件是怎么一步步变成可跨进程调用的Java代码的。
1. AIDL到底是什么:先弄懂跨进程通信的底层逻辑
1.1 Android为什么非要“跨进程”通信
先聊一个基本问题:为什么不能像单进程一样直接互相调用函数?因为Android给每个应用默认分配的是相互隔离的进程,进程和进程之间的内存是不能相互访问的,这是操作系统层的保护机制——你在A进程里new一个对象,B进程根本不知道这个对象在内存的哪个角落。
那App之间以及系统服务跟App之间的交互怎么办?Android在这套隔离机制之上设计了一个叫Binder的IPC通信框架,用它来作为进程间通信的通用管道。Binder不是一个普通概念,它是Android整个系统架构的底座,四大组件跨进程调度、系统服务分发、应用互相调用,底层全是Binder。而AIDL,可以理解成是帮我们描述“进程间通信接口”的一门语言,你用这种语言定义一个接口文件,构建的时候IDE会自动帮你生成一套完整的Binder通信代码。
1.2 为什么是AIDL,而不是Intent、Messenger或者ContentProvider
做安卓开发的人手上至少有四五种跨进程通信手段,但它们的侧重完全不同。我把常见的几个放在一起对比,大家一眼就能看明白AIDL到底适合干什么。
| 通信方式 | 面向能力 | 优点 | 缺点 |
|---|---|---|---|
| Bundle / Intent | 一次性传少量数据 | 简单、对Intent已有耦合 | 只适合传值,没法做方法调用和大数据传递 |
| Messenger | 服务端收消息,串行处理 | 实现简单,线程安全 | 只能处理Message,不支持高并发和回调 |
| ContentProvider | 结构化数据共享 | 系统级API,权限管控成熟 | 偏数据CRUD,不适合实时业务调用和回调 |
| BroadcastReceiver | 单向通知 | 解耦、异步 | 数据体量小,不支持返回结果 |
| AIDL | 跨进程方法调用、并发、回调 | 接口清晰,类型丰富,接近本地调用 | 自定义类型要做Parcelable,调用要处理异步和异常 |
从表格能看出来,如果你要的是高频、并发、带复杂参数的方法调用,AIDL几乎是唯一能兼顾性能和可维护性的选择。Messenger底层其实也是AIDL包了一层Handler,但它是串行的;ContentProvider擅长数据共享,处理不了实时回调。所以当你需要封装一个跨进程服务来给多个业务模块共享能力,比如音乐播放器、下载管理器、输入法服务,AIDL是首选。
2. 写Demo前必须知道的AIDL语法和工程规则
2.1 AIDL文件到底长什么样
AIDL文件本身的语法和Java接口非常像,但它不是Java源码。一个最简单的AIDL接口大概长这样:
// IBookManager.aidl package com.example.aidlserver; interface IBookManager { List<String> getBookNames(); void addBook(String bookName); }看着很简单对吧?这个文件的接口方法最终会被编译成一个Java类,里面包含一个名叫Stub的Binder服务端类和一个名叫Proxy的客户端代理类。你写的每个接口方法,最终都会通过Binder驱动在进程之间传递事务码和参数。构建时AS会自动完成这个转换,核心的生成逻辑不用手写,但你要知道它背后发生了什么。
2.2 支持的数据类型和方向修饰符
AIDL能传的参数类型是有限的。Java八种基本类型、String、CharSequence、List、Map、Parcelable对象都能直接传。List和Map的元素也必须是AIDL支持的类型,不能传接口实现对象。
这里有个容易忽略的细节:非基本类型参数必须标注方向修饰符。in表示客户端把数据传给服务端,服务端改动的值不会传回来;out表示服务端负责填充数据,客户端传进来的值被忽略;inout是双向同步。这个设计对应的是Binder一次拷贝的数据传输模型,用错修饰符轻则数据对不上,重则性能浪费,后面我会具体讲。
2.3 工程里的“位置规矩”
AIDL文件不是随便放哪都能被识别的。Android Studio约定它放在src/main/aidl目录下,包名尽量和App主包名保持一致,否则生成的Java文件包路径和import会乱,这也是很多人问“为什么我的aidl文件生成失败”的隐藏原因之一。创建完成之后执行一次构建,在app/build/generated/aidl_source_output_dir目录下就能找到自动生成的接口Java文件。如果这里没有文件,说明你的AIDL文件本身就没被编译解析,后面第4章会有排查记录。
3. 一步一步实现AidlDemo
这一部分是我这次Demo的完整实操流程。我选择了最省事的方式:一个工程里同时写服务端和客户端,让Service运行在独立进程,用同一个App去绑定它,这比建两个工程要直观,也更容易调试。
3.1 创建AIDL接口文件
在Android Studio里打开项目,右键点击src/main目录,选择New -> AIDL -> AIDL File。AS会提示创建aidl目录,确认即可。记得先把编译用的SDK版本配好,我之前遇到过一次新建AIDL文件后一直报错,排查下来是SDK Build-Tools版本太老导致的。
我创建的接口叫IBookManager.aidl,完整代码如下:
// IBookManager.aidl package com.example.aidlserver; interface IBookManager { List<String> getBookNames(); void addBook(String bookName); }构建一次项目,去app/build/generated/aidl_source_output_dir/下找IBookManager.java文件。打开这个生成文件,你会看到里面有一个Stub静态抽象类和一个Stub.Proxy内部类,Stub继承自Binder,Proxy实现了你定义的这个接口。这个文件是整个AIDL通信的骨架,虽然不用自己写,但我建议至少打开看一遍,看懂之后很多Binder问题都能迎刃而解。
3.2 写服务端Service并让它跑在独立进程
接下来新建一个Service,在onBind方法里返回IBookManager接口的Stub实现。注意这里返回的是Stub对象,它是Binder在服务端的载体,客户端通过Binder驱动拿到它的句柄,最终调到的是这个实现里的方法。
public class BookService extends Service { private final CopyOnWriteArrayList<String> bookList = new CopyOnWriteArrayList<>(); private final IBookManager.Stub mBinder = new IBookManager.Stub() { @Override public List<String> getBookNames() { return new ArrayList<>(bookList); } @Override public void addBook(String bookName) { bookList.add(bookName); } }; @Override public IBinder onBind(Intent intent) { return mBinder; } }这里用了CopyOnWriteArrayList,是因为服务端Binder的方法是塞在Binder线程池里执行的,多个客户端同时调用时存在并发读写,普通ArrayList会出问题。
然后在AndroidManifest.xml里给Service加一个android:process=":remote"属性。加了它,Service才真正运行在独立进程,跨进程通信的Demo才算是名副其实。
<service android:name=".BookService" android:process=":remote" android:enabled="true" android:exported="true" />很多初学者以为只要Service写好了就自动是跨进程的,不加process属性跑通了一个仿AIDL的Demo,实质上还在同一个进程里,这是概念上的大坑。
3.3 客户端绑定服务并完成调用
客户端流程比较固定:bindService绑定远程Service,ServiceConnection回调里拿到IBinder,再通过IBookManager.Stub.asInterface(service)转成代理,然后就能调用接口方法了。
public class MainActivity extends AppCompatActivity { private IBookManager iBookManager; private boolean bound; private final ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { iBookManager = IBookManager.Stub.asInterface(service); bound = true; try { List<String> books = iBookManager.getBookNames(); Log.d("AidlDemo", "当前书单:" + books.toString()); iBookManager.addBook("Android开发艺术探索"); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { bound = false; iBookManager = null; } }; @Override protected void onStart() { super.onStart(); Intent intent = new Intent(this, BookService.class); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } @Override protected void onStop() { super.onStop(); if (bound) { unbindService(mConnection); bound = false; } } }这里要重点提醒一句:跨进程调用是同步阻塞的。虽然Demo里方法执行很快,但在真实项目里绝对不能在主线程调用远程的耗时方法,否则很容易卡到ANR。客户端拿到的IBinder是Proxy,不是服务端的Stub实例,这里面的方法调用本质上是把参数序列化后发给Binder驱动,再由驱动通知服务端执行。理解了这层代理关系,后面看性能问题会清楚很多。
4. 我踩过的那些坑:AIDL从编译到调用的排查记录
写这个Demo的过程中,我故意把几个经典的错误都踩了一遍。这一节挑几个最常见的整理成记录,大家以后遇到不用再全网搜了。
4.1 AIDL文件生成失败,或生成的Java文件不在预期位置
这个现象通常有两种表现:一是AIDL文件报红,却找不到具体语法错误;二是构建成功但没有任何生成的Java类。排查顺序是这样:
- 确认AIDL文件在
src/main/aidl目录下,而不是src/main/java里。 - 确认AIDL文件包名和应用module包名一致,尤其是那些把module目录名改掉的项目。
- 确认AIDL里import的路径和实际类路径完全一致。
- 执行一次
Build -> Clean Project,再重新构建。
如果以上都不行,在AS右侧的Gradle面板里找到app -> Tasks -> build -> assembleDebug,双击跑一次,看Build窗口里的详细日志。很多时候报错信息比编辑器红色波浪线有用得多。
4.2 自定义Parcelable对象导致“找不到符号”
我在Demo里加了一个Book对象想传点结构化数据,第一次直接写了个实现了Parcelable的Java类,然后在AIDL接口里引用它,编译直接报错。原因在于:AIDL编译器不认识你新建的这个类,除非你为它额外声明一个同名的AIDL文件。
解决办法:在同目录下再创建一个Book.aidl文件,内容就两行:
// Book.aidl package com.example.aidlserver; parcelable Book;然后在IBookManager.aidl里用import引入它:
// IBookManager.aidl package com.example.aidlserver; import com.example.aidlserver.Book; interface IBookManager { List<Book> getBookList(); void addBook(in Book book); }与此同时Java类Book必须完整实现Parcelable接口,包括describeContents()和writeToParcel(),并且要有一个参数为Parcel的构造方法。很多报“找不到符号”的情况,其实是Parcelable实现不完整。
4.3 bindService没反应,连接上了调用却没有结果
这个问题最隐蔽。我复现过三种情况:一种是包名写错,bindService根本找不到Service;一种是Service没加android:process属性,虽然能跑通但根本不算跨进程;还有一种是Service被系统直接杀掉了,客户端拿到代理后调用时抛DeadObjectException。
前两种好排查,关键看日志和init流程。第三种要注意加一些保护机制,比如在onServiceDisconnected里设置重连,或者在Service的onBind返回前做初始化判断。真实项目里Service被回收是常态,不能靠一次绑定管到底。
还有一个经常被忽略的点:跨进程调用要尽量处理RemoteException,不要只打一行空日志。RemoteException代表底层Binder通信出问题了,具体可能是进程死掉、事务太大,也可能是客户端权限不够,这条信息对排查线上问题非常关键。
4.4 方法里传List和Map时的“方向修饰符”报错
AIDL编译器要求List和Map参数必须带方向修饰符,很多从本地接口复制代码的人最容易在这块翻车。比如这么写:
void setBookList(List<String> bookList);编译会直接提示list需要in/out/inout。给参数加上in就好。在实践中我一般统一用in,除非明确知道服务端要改动并返回给客户端,再用inout。inout的开销远比in大,因为它不仅要序列化一次,还要反序列化一次,能少用就少用。
5. 升级玩法:自定义对象、回调与性能细节
5.1 自定义Parcelable对象的完整流程
自定义对象是AIDL里最常用的传参方式,步骤虽然多但不复杂,核心就是三点:Java类实现Parcelable,同名AIDL文件声明parcelable,接口文件import后使用。下面是一个完整的Book对象核心代码:
public class Book implements Parcelable { public String name; public float price; public Book(String name, float price) { this.name = name; this.price = price; } protected Book(Parcel in) { name = in.readString(); price = in.readFloat(); } public static final Creator<Book> CREATOR = new Creator<Book>() { @Override public Book createFromParcel(Parcel in) { return new Book(in); } @Override public Book[] newArray(int size) { return new Book[size]; } }; @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(name); dest.writeFloat(price); } }字段顺序在writeToParcel和readFromParcel里必须完全一致,不然跨进程拿出来的对象字段会串。这是新手最容易犯的错,我曾经把一个int和String的顺序写反,Debug了半小时才发现。
5.2 in、out、inout到底怎么选
方向修饰符直接影响Binder拷贝的数据量。in参数:客户端传值给服务端,服务端不能把改动传回,序列化时只走一轮数据;out参数:客户端传入的数据被忽略,服务端填充数据后返回给客户端;inout:客户端把值传给服务端,服务端改动后再传回来,双向往返,开销最大。
业务里90%的场景用in就够。只有当你确实需要服务端给对象赋初值或做修改再返回时才用inout。还有一点要提醒,AIDL只认修饰符,不认对象引用,跨进程传对象本质都是拷贝,你在服务端改了对象的引用,客户端是感知不到的,除非用inout或改返回值。
5.3 双向通信:用RemoteCallbackList做服务端回调
纯单向调用只能算AIDL的一半能力,实际场景里服务端经常要主动通知客户端,比如下载进度、播放状态。做法是在AIDL接口里定义一个回调接口,客户端注册进去,服务端再调用它。
// IOnBookChangeListener.aidl package com.example.aidlserver; interface IOnBookChangeListener { void onBookChanged(String bookName); }然后在主接口里加注册和反注册的方法:
interface IBookManager { List<Book> getBookList(); void addBook(in Book book); void registerListener(IOnBookChangeListener listener); void unregisterListener(IOnBookChangeListener listener); }服务端持有监听器时不要自己用普通List,要使用RemoteCallbackList。它的好处是自动管理Binder死亡导致的无用回调,不用你手动做资源清理。使用方法很简单,注册时放进register,注销时用unregister,遍历回调时用beginBroadcast和finishBroadcast包起来。
如果你的回调方法不需要返回值,可以在AIDL接口方法定义前面加oneway关键字,调用会变成异步,客户端不会因为等待服务端执行完而阻塞。Binder线程池是有上限的,耗时任务配合oneway可以有效避免把线程池占满。
6. 从Demo到框架认知:AIDL这条链路还能通向哪儿
6.1 一个真实场景:音乐播放器App的跨进程绑定
我接手过一个音乐播放器项目,播放Service运行在独立进程,界面进程通过AIDL控制播放、暂停、获取播放列表。这里AIDL的价值很明显:界面进程崩了不影响播放服务继续运行,这正是媒体类App常见的设计思路。类似的应用还有下载管理器、推送服务、输入法框架等系统级场景。
大家在做Demo时可以尝试给Service加android:process=":remote"后,用adb shell ps看看进程列表,会看到两个相同包名不同后缀的进程,那时你才真正理解了“跨进程”这三个字的含义。
6.2 性能相关:一次拷贝、主线程限制、事务大小
Binder数据传递有一个显著优势是只需要一次内存拷贝,但它不是无限制的。Binder事务缓冲区大小约1MB,超过这个量级会直接抛TransactionTooLargeException,这也是AIDL不适合传大图、大文件的原因。传大文件更稳妥的方案是把它写到磁盘,AIDL只传文件路径。
另外跨进程调用默认在主线程调用会阻塞等待,具体是否ANR取决于服务端执行耗时。系统的Binder线程池数量有限,一般每个进程几百个,大量耗时任务会拖垮整个进程的IPC能力,所以AIDL实现里的方法尽量要短平快,耗时操作拆成异步或开独立线程去做。
6.3 AIDL的“下一站”:从客户端到Framework的完整链路
AIDL文件编译成Java类之后,整个调用链大概是这样的:客户端持有Proxy代理,调用接口方法时把参数写入Parcel,然后通过Binder驱动把事务请求发给目标进程;服务端的Binder线程池收到请求后,把数据解包,派发给Stub里对应的业务方法执行;执行结果再沿同样路径返回。整个过程中,AIDL只是接口描述语言,真正干活的是Binder驱动和数据序列化机制。
这也是为什么面试或者深入Framework时,大家总会从AIDL切入Binder。Android系统服务的对外接口大量使用了AIDL,比如ActivityManager、PackageManager这些系统服务,它们就像一个个运行在系统进程里的AIDL服务,App通过Binder代理访问它们。看懂了AIDL生成代码,再回头去翻AOSP里那些系统服务源码,会发现很多概念是共通的。
在AidlDemo的基础上去扩展,我建议下一步做三件事:一是把自定义Parcelable对象完整跑通;二是用RemoteCallbackList实现一个服务端主动推送的小功能;三是打开IDE生成的那个Java文件,逐行对比Stub和Proxy的调用逻辑。把这三件事做完,你对Android进程间通信的理解,基本就超过绝大多数只停留在“会用bindService”层面的开发了。
最后分享一个小技巧:遇到AIDL相关诡异问题,先看adb logcat里有没有关于Binder、Transaction、DeadObject的异常输出,往往比看代码更早定位到问题源头。再配合生成在build目录下的AIDL对应Java文件一起看,大多数“玄学”都能变成可解释的工程问题。
本文还有配套的精品资源,点击获取