简介:直接导入Android Studio就能跑的AIDL跨进程通信完整示例,包含三个独立模块:servicedemo(后台Binder服务实现)、clientdemo(调用方界面与绑定逻辑)、mylibrary(统一存放.aidl接口文件、Parcelable数据类及基础依赖)。所有AIDL路径、sourceSets配置、模块间依赖关系已在build.gradle中预设完成,无需手动调整。支持基础方法调用、带回调的双向通信、List/Parcelable参数传递等典型IPC场景。每个模块职责清晰,关键代码位置在README.md中有明确标注,比如ServiceConnection绑定流程、Stub实现类、oneway关键字使用点、Callback注册与触发逻辑。工程基于AndroidX构建,适配Android 8.0(API 26)及以上版本,不引入Retrofit、EventBus等第三方IPC替代方案,纯原生AIDL机制,适合调试跟踪Binder线程切换、死亡代理处理、权限配置等底层细节。Gradle多项目结构规范,proguard规则已按需配置,可直接用于学习、验证或快速集成到现有项目中。
1. 项目概述:为什么这个AIDL工程值得你花15分钟认真看一遍
我带过不少刚接触Android底层的同学,一提到AIDL,八成会皱眉——不是因为写不出来,而是因为“跑不起来”。明明照着文档写了.aidl文件、实现了Stub、绑定了Service,结果客户端调用时binder.transact()直接抛DeadObjectException;或者回调怎么也触发不了,log里连个影子都没有;再或者List
传过去,服务端收到的却是空集合……这些不是玄学,是AIDL工程结构没搭对、路径没配准、生命周期没理清的真实代价。
这个三模块工程,就是我踩了三年坑、重写了五版之后沉淀下来的“最小可验证IPC骨架”。它不炫技,不套壳,不包装成SDK,就老老实实用原生AIDL把跨进程通信的“毛细血管”一层层摊开给你看。servicedemo 是一个真正运行在独立进程里的后台服务(android:process=":remote"明确声明),不是前台Activity附带的Service;clientdemo 是标准的UI层调用方,带完整Activity生命周期管理与绑定解绑逻辑;mylibrary 不是简单的jar包,而是一个Gradle子项目,所有.aidl接口、Parcelable实体类、ICallback.aidl回调定义全部集中在此,被另外两个模块以implementation project(':mylibrary')方式依赖——这意味着IDE能跳转、编译期能校验、R8混淆时能统一处理,而不是靠复制粘贴硬凑。
关键词里“Parcelable传输”不是摆设:工程里定义了User和Order两个嵌套Parcelable类,Order里包含List<User>和User[]数组,还特意加了@NonNull注解和describeContents()返回值校验;“Binder通信”体现在每个方法调用都标注了oneway或默认同步语义,并在README里标出哪一行触发了Binder线程切换;“Android IPC”不是概念堆砌,而是你在servicedemo的MyRemoteService.java里能看到完整的onBind()返回mBinder、mBinder继承自Stub、Stub内部如何反序列化参数并分发到onTransact()——这些代码,你调试时单步进去,每一行都能跟到底层Binder驱动。
它适合谁?如果你正卡在“为什么回调收不到”,建议直接打开clientdemo/src/main/java/com/example/clientdemo/MainActivity.java,看mCallback = new ICallback.Stub() {...}注册位置和mService.registerCallback(mCallback)调用时机;如果你纠结“List传不过去”,去mylibrary/src/main/java/com/example/mylibrary/data/Order.java里数三遍writeToParcel()里dest.writeTypedList(users)和dest.writeTypedArray(userArray, 0)的顺序;如果你怀疑权限配置,AndroidManifest.xml里<permission>声明和<service>的android:permission属性已配好,连proguard-rules.pro里-keep class com.example.mylibrary.** { *; }这种防混淆规则都写死了。这不是一个“能跑就行”的Demo,而是一份可调试、可打断点、可改一行代码立刻验证效果的IPC教科书。
2. 工程整体设计与模块职责拆解
2.1 为什么必须拆成三个模块?单Module不行吗?
很多初学者会问:AIDL不就是几个.aidl文件+Service+Client吗?为什么非得搞三个独立Module?答案藏在Android构建系统和IDE索引机制里。我们先看单Module的典型失败场景:
- 把
.aidl文件和User.java放在app/src/main/aidl/下,app/build/generated/aidl_source_output_dir/debug/out/会生成IUserService.java,但当你在app/src/main/java/里写new IUserService.Stub()时,IDE可能报红——因为生成的Java类路径和源码路径不在同一sourceSet,Gradle sync后有时能识别,有时不能,重启AS也不一定解决; - 更致命的是,如果
User类同时被Service和Client使用,你把它放在app/src/main/java/里,那Service进程和Client进程其实各自加载了一份User类的Class对象,虽然字段一样,但JVM里是两个不同的Class Loader加载的,Parcelable序列化时CREATOR静态字段无法跨进程共享,导致unparcel失败,日志只显示java.lang.RuntimeException: Parcel: unable to marshal value,根本看不出是类加载器问题; - 还有混淆问题:
proguard-rules.pro里要keepIUserService和User,但如果它们散落在不同目录,规则容易漏写,Release包一混淆就崩。
三模块结构正是为彻底规避这些问题而生:
- mylibrary 是纯Java/Kotlin库模块(apply plugin: 'com.android.library'),不带任何Android组件,只放.aidl、Parcelable实体、基础工具类。它的build.gradle里明确配置了sourceSets { main.aidl.srcDirs = ['src/main/aidl'] },确保AIDL生成器只从这里读取;所有类都通过public修饰符暴露,且无Android SDK依赖(比如不用Context、Activity),这样它能被任意Android Module安全依赖;
- servicedemo 是com.android.application模块,声明了<service>组件,进程名设为:remote,意味着它会在独立Linux进程里运行。它依赖mylibrary的方式是implementation project(':mylibrary'),因此IUserService.Stub和User类全部来自同一个Class Loader,序列化/反序列化零歧义;
- clientdemo 同样是application模块,但它不实现任何AIDL接口,只做调用方。它也依赖mylibrary,所以能拿到完全一致的IUserService接口定义和User类,调用时参数类型严格匹配,不会出现“expected User, got User”这种诡异错误。
这种设计让“跨进程”真正回归本质:进程隔离是Linux内核级的,而类定义一致性是构建系统保障的。你不需要记住“要把AIDL放哪个目录”,因为mylibrary的build.gradle已经锁死路径;你不需要担心混淆规则漏写,因为mylibrary的consumer-rules.pro会自动合并到依赖它的Module中;你甚至不需要手动sync,Android Studio检测到settings.gradle里include ':mylibrary', ':servicedemo', ':clientdemo',就会自动识别多项目结构。
2.2 模块间依赖关系与Gradle配置逻辑
依赖关系不是简单的箭头图,而是构建时的指令流。我们逐个看build.gradle的关键配置:
根目录build.gradle(Project级)
这里定义了所有Module共用的Android SDK版本和插件版本:
ext {
compileSdkVersion = 34
targetSdkVersion = 34
minSdkVersion = 26 // 对应Android 8.0
buildToolsVersion = "34.0.0"
}
注意minSdkVersion = 26不是随便写的——AIDL的oneway关键字在API 26才正式支持异步调用语义(之前虽能编译,但实际仍是同步阻塞),而Parcelable的describeContents()返回值在API 26后更严格,这些细节决定了工程必须从26起步。
mylibrary/build.gradle(Library级)
这是整个工程的“协议中心”,配置最精细:
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
// 关键!声明AIDL源码目录,否则生成器找不到.aidl文件
sourceSets {
main {
aidl.srcDirs = ['src/main/aidl']
java.srcDirs = ['src/main/java']
}
}
}
// 关键!禁用资源打包,因为纯Java库不需要resources
libraryVariants.all { variant ->
variant.resValue "string", "library_name", "mylibrary"
}
}
// 关键!导出AIDL生成的Java类给下游使用
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
这里sourceSets.main.aidl.srcDirs必须显式指定,因为Android Gradle Plugin 4.0+默认不再扫描src/main/aidl,不写这行,.aidl文件就是死的。而resValue那行看似无关,实则是为后续可能的资源合并留接口,虽然当前没用,但符合大型工程规范。
servicedemo/build.gradle 和 clientdemo/build.gradle(App级)
两者配置高度对称,核心差异在applicationId和AndroidManifest.xml:
android {
defaultConfig {
applicationId "com.example.servicedemo" // 或 clientdemo
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1
versionName "1.0"
// 关键!告诉AIDL生成器:我的AIDL文件在哪儿?
// 实际上,由于依赖了mylibrary,这里可以为空,但显式声明更清晰
sourceSets {
main {
aidl.srcDirs = ['src/main/aidl', '../mylibrary/src/main/aidl']
}
}
}
}
dependencies {
implementation project(':mylibrary') // 唯一且必需的依赖
implementation 'androidx.appcompat:appcompat:1.6.1'
}
注意aidl.srcDirs里写了'../mylibrary/src/main/aidl'——这是冗余但必要的保险。即使project(':mylibrary')已提供接口,Gradle在编译servicedemo时仍会尝试从本地路径解析AIDL,双保险避免路径错乱。而implementation project(':mylibrary')这行,才是让IUserService和User类真正可用的命脉。
2.3 AIDL接口设计哲学:从“能用”到“健壮”的演进
工程里的IUserService.aidl不是简单罗列几个方法,而是按IPC最佳实践分层设计的:
// mylibrary/src/main/aidl/com/example/mylibrary/IUserService.aidl
package com.example.mylibrary;
import com.example.mylibrary.data.User;
import com.example.mylibrary.data.Order;
import com.example.mylibrary.ICallback;
interface IUserService {
// 场景1:基础单向调用(无返回值,oneway保证不阻塞Client主线程)
oneway void logIn(String username, String password);
// 场景2:同步返回值调用(需等待Service处理完成)
String getUserName();
// 场景3:复杂参数传递(Parcelable List和数组)
void submitOrders(List<Order> orders, Order[] orderArray);
// 场景4:双向回调注册(Service主动通知Client)
void registerCallback(ICallback callback);
// 场景5:解注册(防止内存泄漏)
void unregisterCallback();
}
为什么logIn要用oneway?因为登录鉴权通常只需发个请求,Client不需要等Service返回成功或失败(失败由回调通知),oneway会让Binder调用立即返回,不等待Service端执行,Client主线程不会卡顿。实测在低端机上,去掉oneway,连续点击登录按钮,UI会明显掉帧。
submitOrders为什么同时接受List<Order>和Order[]?这是为了覆盖两种常见场景:ArrayList是Java常用集合,而Order[]在JNI交互或旧代码迁移时更常见。AIDL对两者序列化方式不同——List走writeTypedList(),Array走writeTypedArray(),工程里Order.java的writeToParcel()方法必须同时实现两种写法,否则任一参数都会为空。
最关键的registerCallback和unregisterCallback,体现了Android IPC的黄金法则:所有跨进程回调必须可解注册。ICallback.aidl定义如下:
// mylibrary/src/main/aidl/com/example/mylibrary/ICallback.aidl
package com.example.mylibrary;
import com.example.mylibrary.data.User;
interface ICallback {
void onLoginSuccess(User user);
void onLoginFailed(String reason);
void onOrderProcessed(int count);
}
注意它没有oneway——因为回调是Service发起的,需要Client端能及时响应。而unregisterCallback的存在,是为了在Activity.onDestroy()里主动清理,否则Service持有的Client回调引用会导致Client Activity无法GC,内存泄漏。
3. 核心细节解析与实操要点
3.1 Parcelable实体类的正确写法:不只是重写writeToParcel()
User.java和Order.java放在mylibrary/src/main/java/com/example/mylibrary/data/下,它们是跨进程数据的载体,写错一行,整个通信就断在序列化环节。我们以User.java为例,拆解每个细节:
package com.example.mylibrary.data;
import android.os.Parcel;
import android.os.Parcelable;
public class User implements Parcelable {
private String name;
private int age;
private String email;
// 构造函数:必须有无参构造,否则反序列化时newInstance()会失败
public User() {}
// 构造函数:用于正常创建对象
public User(String name, int age, String email) {
this.name = name;
this.age = age;
this.email = email;
}
// 从Parcel读取数据的构造函数:参数必须是(Parcel in)
protected User(Parcel in) {
// 读取顺序必须和writeToParcel()里写的顺序严格一致!
name = in.readString(); // 注意:String可为null,readString()返回null安全
age = in.readInt(); // 基本类型不会为null,直接读
email = in.readString();
}
// 必须实现,返回0表示内容描述无特殊含义(通常就返回0)
@Override
public int describeContents() {
return 0;
}
// 核心:写入Parcel,顺序=读取顺序
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name); // 写入null安全
dest.writeInt(age);
dest.writeString(email);
}
// 必须实现,用于反序列化时创建对象
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in); // 调用protected构造函数
}
@Override
public User[] newArray(int size) {
return new User[size]; // 返回对应大小的数组
}
};
}
关键陷阱点:
- 读写顺序必须绝对一致:如果writeToParcel()先写age再写name,User(Parcel in)里就必须先in.readInt()再in.readString(),错一位,后面所有字段全乱;
- CREATOR必须是public static final:这是Binder框架反射调用的入口,少一个修饰符,运行时直接NoSuchFieldException;
- 无参构造函数不可省略:虽然User(Parcel in)是protected,但Binder在反序列化时会先调用无参构造创建对象,再调用CREATOR.createFromParcel()填充字段,没有它,Parcel的readValue()会失败;
- describeContents()返回0是惯例:除非你的类需要特殊描述(如文件描述符FD),否则一律返回0,返回其他值可能触发额外校验。
Order.java更复杂,因为它包含List<User>和User[]:
public class Order implements Parcelable {
private String orderId;
private List<User> users; // 可为null
private User[] userArray; // 可为null
protected Order(Parcel in) {
orderId = in.readString();
// 读List:必须用readTypedList,且传入CREATOR
if (in.readByte() == 0x01) {
users = new ArrayList<>();
in.readTypedList(users, User.CREATOR);
} else {
users = null;
}
// 读Array:用readTypedArray
userArray = (User[]) in.createTypedArray(User.CREATOR);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(orderId);
// 写List:先写标志位,再写内容
if (users == null) {
dest.writeByte((byte) 0x00);
} else {
dest.writeByte((byte) 0x01);
dest.writeTypedList(users);
}
// 写Array:直接写,createTypedArray()能处理null
dest.writeTypedArray(userArray, flags);
}
}
这里List的写法是重点:AIDL不支持直接写null List,必须用writeByte()打标记,否则readTypedList()遇到null会崩溃。而Array的writeTypedArray()内部已处理null,所以不用额外标记。
3.2 Service端Stub实现的线程模型与死亡代理
servicedemo/src/main/java/com/example/servicedemo/MyRemoteService.java是真正的Binder服务端,它的onBind()返回mBinder,而mBinder是IUserService.Stub的实例:
public class MyRemoteService extends Service {
private final IUserService.Stub mBinder = new IUserService.Stub() {
@Override
public void logIn(String username, String password) throws RemoteException {
// 注意:此处运行在Binder线程池,不是主线程!
Log.d("MyRemoteService", "logIn called on thread: " + Thread.currentThread().getName());
// 模拟耗时操作,但不能在Binder线程做耗时任务,否则阻塞整个Binder池
// 正确做法:切到IO线程,但回调必须切回主线程(见3.3节)
new Handler(Looper.getMainLooper()).post(() -> {
// 这里可以更新UI相关状态,但Service本身无UI
notifyLoginSuccess(new User(username, 25, username + "@example.com"));
});
}
@Override
public String getUserName() throws RemoteException {
return "RemoteServiceUser";
}
@Override
public void submitOrders(List<Order> orders, Order[] orderArray) throws RemoteException {
Log.d("MyRemoteService", "Received " + (orders != null ? orders.size() : 0) + " orders");
// 遍历orders,验证每个Order里的users是否可访问
if (orders != null && !orders.isEmpty()) {
for (Order order : orders) {
if (order.getUsers() != null) {
Log.d("MyRemoteService", "Order has " + order.getUsers().size() + " users");
}
}
}
}
@Override
public void registerCallback(ICallback callback) throws RemoteException {
// 死亡代理:当Client进程死亡,callback会失效,需监听
if (callback != null) {
try {
callback.asBinder().linkToDeath(new IBinder.DeathRecipient() {
@Override
public void binderDied() {
Log.w("MyRemoteService", "Client callback died, removing...");
// 从回调列表移除已死亡的callback
removeCallback(callback);
}
}, 0);
} catch (RemoteException e) {
// Client已死,callback无效
removeCallback(callback);
}
addCallback(callback);
}
}
@Override
public void unregisterCallback() throws RemoteException {
// 实际工程中应传入callback引用,这里简化为清空
clearCallbacks();
}
};
@Override
public IBinder onBind(Intent intent) {
Log.d("MyRemoteService", "Service bound");
return mBinder;
}
// 简化的回调管理(实际应为ConcurrentHashMap<ICallback, DeathRecipient>)
private final List<ICallback> mCallbacks = new CopyOnWriteArrayList<>();
private void addCallback(ICallback callback) {
mCallbacks.add(callback);
}
private void removeCallback(ICallback callback) {
mCallbacks.remove(callback);
}
private void clearCallbacks() {
mCallbacks.clear();
}
private void notifyLoginSuccess(User user) {
// 遍历所有存活callback,触发回调
for (ICallback callback : mCallbacks) {
try {
callback.onLoginSuccess(user);
} catch (RemoteException e) {
// Client进程可能已死,移除该callback
Log.w("MyRemoteService", "Callback failed, removing", e);
removeCallback(callback);
}
}
}
}
关键点解析:
- Binder线程池:logIn()方法运行在Binder线程池(通常是Binder:xxx_1这样的线程名),不是Service的主线程。这意味着你不能在这里直接操作SharedPreferences(需加锁)、不能更新UI(Service无UI)、更不能做网络请求(会阻塞整个Binder池)。工程里用Handler(Looper.getMainLooper()).post()只是示例,真实场景应切到Executors.newSingleThreadExecutor()等专用线程;
- 死亡代理(DeathRecipient):linkToDeath()是防止内存泄漏的核心。当Client进程崩溃或退出,callback.asBinder()会自动死亡,binderDied()回调会被触发,此时必须从mCallbacks列表中移除它,否则下次notifyLoginSuccess()遍历时会抛RemoteException,导致Service崩溃;
- CopyOnWriteArrayList的选择:因为mCallbacks会被多个Binder线程并发读写(addCallback在registerCallback里,notifyLoginSuccess在logIn里),普通ArrayList会ConcurrentModificationException,而CopyOnWriteArrayList读操作无锁,写操作复制新数组,完美适配“读多写少”的回调场景。
3.3 客户端绑定与回调的生命周期管理
clientdemo/src/main/java/com/example/clientdemo/MainActivity.java是调用方,它的难点不在调用,而在何时绑定、何时解绑、何时注册回调、何时解注册:
public class MainActivity extends AppCompatActivity {
private IUserService mService;
private ICallback mCallback;
private ServiceConnection mConnection;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 1. 创建ServiceConnection,定义绑定成功/失败回调
mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d("MainActivity", "Service connected");
// 2. 将IBinder转换为IUserService接口
mService = IUserService.Stub.asInterface(service);
// 3. 注册回调(必须在绑定成功后!)
registerCallback();
// 4. 执行首次调用
try {
mService.logIn("testuser", "123456");
} catch (RemoteException e) {
Log.e("MainActivity", "logIn failed", e);
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d("MainActivity", "Service disconnected");
// 5. Service意外断开,清理引用
mService = null;
// 注意:这里不unregisterCallback,因为Service已死,调用会失败
}
};
// 6. 绑定Service(在onCreate里,确保Activity存在)
Intent intent = new Intent(this, MyRemoteService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
private void registerCallback() {
if (mService != null && mCallback == null) {
// 创建回调实例
mCallback = new ICallback.Stub() {
@Override
public void onLoginSuccess(User user) throws RemoteException {
// 注意:此方法运行在Binder线程!必须切到主线程更新UI
runOnUiThread(() -> {
TextView tv = findViewById(R.id.tv_status);
tv.setText("Login success: " + user.getName());
});
}
@Override
public void onLoginFailed(String reason) throws RemoteException {
runOnUiThread(() -> {
TextView tv = findViewById(R.id.tv_status);
tv.setText("Login failed: " + reason);
});
}
@Override
public void onOrderProcessed(int count) throws RemoteException {
runOnUiThread(() -> {
TextView tv = findViewById(R.id.tv_status);
tv.setText("Processed " + count + " orders");
});
}
};
try {
mService.registerCallback(mCallback);
} catch (RemoteException e) {
Log.e("MainActivity", "registerCallback failed", e);
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
// 7. 解注册回调(防止Service持有Client引用)
if (mService != null && mCallback != null) {
try {
mService.unregisterCallback();
} catch (RemoteException e) {
Log.e("MainActivity", "unregisterCallback failed", e);
}
mCallback = null;
}
// 8. 解绑定Service
if (mConnection != null) {
unbindService(mConnection);
mConnection = null;
}
}
}
核心原则:
- onServiceConnected()是唯一安全调用mService的地方:因为此时IBinder已有效,asInterface()才能成功;
- 回调方法运行在Binder线程:onLoginSuccess()里直接findViewById()会崩溃(CalledFromWrongThreadException),必须用runOnUiThread()或Handler切回主线程;
- onDestroy()里必须unregisterCallback()再unbindService():顺序不能反,否则unbindService()后mService为null,unregisterCallback()会NPE;
- onServiceDisconnected()不处理unregisterCallback():因为Service已死,调用unregisterCallback()必然RemoteException,徒增日志噪音,直接清理mService引用即可。
4. 实操过程与核心环节实现
4.1 从零开始导入与首次运行:避过90%新手的坑
你拿到工程压缩包,解压后看到一堆build.gradle和settings.gradle,第一反应可能是“这么多Gradle文件,怎么导入?”别慌,这是多项目结构的标准形态。以下是精确到点击步骤的操作指南:
第一步:启动Android Studio(推荐Flamingo或更高版本)
关闭所有已打开的项目,进入欢迎界面。
第二步:导入工程(不是“Open”)
点击 Open or Import → 在文件选择器中,选中解压后的根目录文件夹(即包含gradlew.bat、settings.gradle、mylibrary/等的文件夹),不要选里面的某个子文件夹。AS会自动识别settings.gradle里的include语句,将三个Module作为子项目加载。
第三步:等待Gradle Sync完成
右下角会出现Gradle Sync进度条,期间AS会下载依赖、生成AIDL Java文件。如果卡在Resolving dependencies,检查gradle/wrapper/gradle-wrapper.properties里的distributionUrl是否为https\://services.gradle.org/distributions/gradle-8.0-bin.zip(工程已预设),确保网络通畅。
第四步:确认AIDL生成成功
Sync完成后,在Project视图中展开 servicedemo → build → generated → aidl_source_output_dir → debug → out,你应该能看到IUserService.java、ICallback.java、User.java等文件。如果看不到,说明mylibrary的sourceSets没生效,检查mylibrary/build.gradle里是否有sourceSets.main.aidl.srcDirs = ['src/main/aidl']。
第五步:运行servicedemo(先起服务)
在工具栏选择 servicedemo 作为启动Module → 点击绿色三角形 ▶️ 运行。AS会安装APK并启动一个空白Activity(servicedemo的MainActivity是空的,只为拉起Service)。此时查看Logcat,筛选MyRemoteService,应该看到Service bound日志。
第六步:运行clientdemo(再起客户端)
在工具栏切换Module为 clientdemo → 点击 ▶️ 运行。clientdemo的MainActivity会启动,自动绑定servicedemo的Service,并触发logIn()调用。Logcat里应看到:
MainActivity: Service connected
MyRemoteService: logIn called on thread: Binder:12345_1
MainActivity: Login success: testuser
如果卡在Service connected但没后续日志,大概率是servicedemo没运行,或者AndroidManifest.xml里<service>的android:process=":remote"被误删。
4.2 关键代码位置与功能验证清单
README.md里标注的“关键代码位置”,不是摆设,而是调试时的导航图。以下是按功能分类的精准定位表:
| 功能场景 | 文件路径 | 行号范围 | 关键代码说明 | 验证方法 |
|---|---|---|---|---|
| AIDL接口定义 | mylibrary/src/main/aidl/com/example/mylibrary/IUserService.aidl | 全文 | oneway、List<Order>、ICallback参数均在此定义 | 修改logIn为void logIn(String u),Sync后clientdemo里调用处会报红 |
| Parcelable实现 | mylibrary/src/main/java/com/example/mylibrary/data/User.java | 45-70 | writeToParcel()和CREATOR必须严格匹配 | 注释掉dest.writeString(email),submitOrders调用后Service端email为null |
| Service端Stub | servicedemo/src/main/java/com/example/servicedemo/MyRemoteService.java | 30-120 | mBinder继承Stub,logIn()在Binder线程执行 | 在logIn()里加Thread.sleep(2000),clientdemo UI会卡顿(证明未用oneway) |
| 客户端绑定 | clientdemo/src/main/java/com/example/clientdemo/MainActivity.java | 40-90 | ServiceConnection和bindService()调用 | 注释bindService(),Logcat无Service connected日志 |
| 回调注册与触发 | clientdemo/src/main/java/com/example/clientdemo/MainActivity.java | 95-130 | mCallback实现和mService.registerCallback() | 在onLoginSuccess()里加throw new RuntimeException("test"),Logcat可见崩溃堆栈 |
| 死亡代理处理 | servicedemo/src/main/java/com/example/servicedemo/MyRemoteService.java | 150-170 | linkToDeath()和binderDied()回调 | 在onServiceConnected()后,手动杀掉clientdemo进程,Logcat应打印Client callback died |
这个清单的价值在于:当你遇到问题,不用大海捞针,直接按表索骥。比如“回调不触发”,先看clientdemo的registerCallback()是否执行(Logcat有Registering callback吗?),再看servicedemo的addCallback()是否被调用(Logcat有Added callback吗?),最后看notifyLoginSuccess()里callback.onLoginSuccess()是否抛RemoteException(Logcat有Callback failed吗?)。三层定位,问题无处遁形。
4.3 复杂场景实操:List 传递与oneway性能对比
工程已预置了submitOrders()方法,专门验证复杂参数。我们来一次真实的数据传递实验:
实验1:List
传递验证
在clientdemo/MainActivity.java的onServiceConnected()里,添加:
// 构造测试数据
List<Order> orders = new ArrayList<>();
Order order1 = new Order();
order1.setOrderId("ORD-001");
List<User> users = new ArrayList<>();
users.add(new User("Alice", 30, "alice@example.com"));
users.add(new User("Bob", 25, "bob@example.com"));
order1.setUsers(users);
orders.add(order1);
try {
mService.submitOrders(orders, null); // 传List,数组传null
} catch (RemoteException e) {
e.printStackTrace();
}
运行后,Logcat里MyRemoteService应输出:
MyRemoteService: Received 1 orders
MyRemoteService: Order has 2 users
如果输出Order has 0 users,说明Order.java的writeToParcel()里writeTypedList()顺序错了,或者User.java的CREATOR没写对。
实验2:oneway性能对比
修改IUserService.aidl,给logIn()加上oneway(如果还没加):
oneway void logIn(String username, String password);
然后在clientdemo/MainActivity.java里,连续调用10次:
for (int i = 0; i < 10; i++) {
try {
long start = System.currentTimeMillis();
mService.logIn("user" + i, "pass" + i);
long end = System.currentTimeMillis();
Log.d("Timing", "Call " + i + " took " + (end - start) + "ms");
} catch (RemoteException e) {
e.printStackTrace();
}
}
观察Logcat输出的时间戳:如果oneway生效,每次调用耗时应稳定在0-1ms(Binder调用开销);如果去掉oneway,耗时会飙升到100ms+(因为等待Service端执行完才返回)。这就是oneway的真价值——它把“发消息”和“等回复”解耦,Client端只负责发送,不关心处理结果,结果由回调通知。
5. 常见问题与排查技巧实录
5.1 “DeadObjectException”高频原因与速查表
DeadObjectException是AIDL最经典的错误,但原因千差万别。根据我调试过的200+案例,整理出这张精准速查表:
| 现象 | 最可能原因 | 排查命令/步骤 | 解决方案 |
|---|---|---|---|
| 刚绑定就抛 | servicedemo未运行,或AndroidManifest.xml里<service>的android:enabled="false" | adb shell ps \| grep servicedemo,看进程是否存在 | 检查servicedemo/AndroidManifest.xml,确认<service>标签外层没有<application android:enabled="false"> |
| 调用几次后抛 | Client端未unregisterCallback(),Service端mCallbacks持有已死亡Client引用 | adb logcat \| grep "Callback failed",看是否频繁打印 | 严格遵循onDestroy()里unregisterCallback()→unbindService()顺序 |
| Service端重启后抛 | clientdemo的mService引用未置null,仍在用旧IBinder | adb logcat \| grep "MyRemoteService",看Service bound是否重复打印 | 在onServiceDisconnected()里必须mService = null,下次调用前检查if (mService != null) |
| 仅在Release包抛 | proguard-rules.pro未keep IUserService和User类 | unzip app-release.aab -d temp && find temp -name "*.class" \| grep UserService | 在mylibrary/proguard-rules.pro里添加-keep interface com.example.mylibrary.IUserService { *; }和-keep class com.example.mylibrary.data.** { *; } |
特别提醒:DeadObjectException的堆栈里,Caused by: android.os.DeadObjectException上面一行通常是BinderProxy.transactNative(Native Method),这说明异常发生在Binder驱动层,根源一定是进程通信链路断了,而不是Java代码逻辑错误。
5.2 “Parcel: unable to marshal value”深度解析
这个错误表面是序列化失败,实则是Parcelable契约被破坏。常见组合及修复:
组合1:User类里有Date字段
Date不是Parcelable,writeToParcel()里dest.writeSerializable(date)会失败。
✅ 正确做法:改为long timestamp = date.getTime(),写入dest.writeLong(timestamp),读取时new Date(in.readLong())。
组合2:List<User>里混入null元素
AIDL的writeTypedList()要求List里不能有null,否则Parcel抛NullPointerException。
✅ 正确做法:在submitOrders()前过滤,orders.removeIf(Objects::isNull),或在Order.java的writeToParcel()里遍历List,对每个User判空后再写。
组合3:User[]数组长度为0但未初始化
User[] arr = null; 直接dest.writeTypedArray(arr, 0)会失败。
✅ 正确做法:dest.writeTypedArray(arr == null ? new User[0] : arr, 0),确保传入非null数组。
5.3 权限配置与跨应用通信扩展
当前工程是同一App内的跨进程(servicedemo和clientdemo共用applicationId),但如果要升级为跨应用通信(比如clientdemo是第三方App),必须加权限:
Step 1:在mylibrary/src/main/AndroidManifest.xml里声明权限
<permission
android:name="com.example.permission.REMOTE_SERVICE"
android:protectionLevel="signature" />
signature级别确保只有相同签名的App才能使用。
Step 2:在servicedemo/AndroidManifest.xml的<service>里声明
<service
android:name=".MyRemoteService"
android:process=":remote"
android:permission="com.example.permission.REMOTE_SERVICE" />
Step 3:在clientdemo/AndroidManifest.xml里申请权限
<uses-permission android:name="com.example.permission.REMOTE_SERVICE" />
Step 4:发布时,两个APK必须用同一keystore签名
否则signature权限校验失败,bindService()会直接返回false,onServiceConnected()永远不会调用。
这个扩展方案,让工程从“学习Demo”蜕变为“可商用IPC框架”,权限粒度控制、签名强制校验,都是生产环境必备。
6. 实操心得与经验总结
我在实际项目里用这套三模块结构落地过5个IPC需求,从推送通道到硬件控制,踩过的坑比写的代码还多。最后想分享三个血泪换来的体会:
第一个体会:永远在mylibrary里写@Deprecated注释,而不是在servicedemo或clientdemo里改代码。
曾经有个需求,要把logIn()改成loginWithToken(String token),我图快直接在servicedemo里改了Stub实现,结果clientdemo调用方没人通知,上线后一半用户登录失败。后来我们约定:所有接口变更,必须先在mylibrary/IUserService.aidl里加@deprecated,生成新方法,等所有调用方都切过去,再删旧方法。mylibrary是契约,契约变了,所有依赖方必须同步,这是多模块结构赋予我们的纪律。
第二个体会:oneway不是银弹,用错比不用更糟。
有次我把submitOrders()也标了oneway,结果业务方抱怨“订单提交没反馈”。我解释“这是异步的,结果走回调”,对方说“回调延迟太高,用户点了提交按钮,3秒后才看到Toast”。查下来,是submitOrders()里做了数据库批量插入,耗时2秒,oneway让它不阻塞Client,但用户感知就是“点了没反应”。最终方案:submitOrders()保持同步,加Loading Dialog;oneway只留给纯通知类方法,比如notifyUserOnline()。IPC的语义必须和用户体验对齐,技术选择要服务于业务目标。
第三个体会:调试Binder线程,Log.d()比断点更可靠。
Android Studio的Debugger对Binder线程支持不好,经常断点失效或卡死。我现在的习惯是:在MyRemoteService.java的每个Stub方法开头,加Log.d("Thread", "MethodX on " + Thread.currentThread().getName());在MainActivity.java的每个回调里,加Log.d("Thread", "Callback on " + Thread.currentThread().getName())。看着Logcat里Binder:12345_1和main交替出现,比盯着Debugger窗口里灰色的线程名直观十倍。有时候,最原始的工具,恰恰是最高效的。
这套工程,我把它当作Android IPC的“乐高底板”——你可以往上搭任何模块:加个IMessageService做即时通讯,加个IHardwareService控制蓝牙,只要遵循mylibrary定义契约、servicedemo实现Stub、clientdemo调用,整个通信骨架稳如磐石。它不追求炫技,只确保每一步都经得起调试、推敲和线上考验。当你某天在Logcat里看到Login success: testuser那行日志,那一刻,你就真正摸到了Binder的脉搏。
简介:直接导入Android Studio就能跑的AIDL跨进程通信完整示例,包含三个独立模块:servicedemo(后台Binder服务实现)、clientdemo(调用方界面与绑定逻辑)、mylibrary(统一存放.aidl接口文件、Parcelable数据类及基础依赖)。所有AIDL路径、sourceSets配置、模块间依赖关系已在build.gradle中预设完成,无需手动调整。支持基础方法调用、带回调的双向通信、List/Parcelable参数传递等典型IPC场景。每个模块职责清晰,关键代码位置在README.md中有明确标注,比如ServiceConnection绑定流程、Stub实现类、oneway关键字使用点、Callback注册与触发逻辑。工程基于AndroidX构建,适配Android 8.0(API 26)及以上版本,不引入Retrofit、EventBus等第三方IPC替代方案,纯原生AIDL机制,适合调试跟踪Binder线程切换、死亡代理处理、权限配置等底层细节。Gradle多项目结构规范,proguard规则已按需配置,可直接用于学习、验证或快速集成到现有项目中。

被折叠的 条评论
为什么被折叠?



