Android AIDL跨进程通信三模块工程:服务端、客户端与公共接口库一体化实现

该文章已生成可运行项目,

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接导入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传输”不是摆设:工程里定义了UserOrder两个嵌套Parcelable类,Order里包含List<User>User[]数组,还特意加了@NonNull注解和describeContents()返回值校验;“Binder通信”体现在每个方法调用都标注了oneway或默认同步语义,并在README里标出哪一行触发了Binder线程切换;“Android IPC”不是概念堆砌,而是你在servicedemoMyRemoteService.java里能看到完整的onBind()返回mBindermBinder继承自StubStub内部如何反序列化参数并分发到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里要keep IUserServiceUser,但如果它们散落在不同目录,规则容易漏写,Release包一混淆就崩。

三模块结构正是为彻底规避这些问题而生:
- mylibrary 是纯Java/Kotlin库模块(apply plugin: 'com.android.library'),不带任何Android组件,只放.aidlParcelable实体、基础工具类。它的build.gradle里明确配置了sourceSets { main.aidl.srcDirs = ['src/main/aidl'] },确保AIDL生成器只从这里读取;所有类都通过public修饰符暴露,且无Android SDK依赖(比如不用ContextActivity),这样它能被任意Android Module安全依赖;
- servicedemocom.android.application模块,声明了<service>组件,进程名设为:remote,意味着它会在独立Linux进程里运行。它依赖mylibrary的方式是implementation project(':mylibrary'),因此IUserService.StubUser类全部来自同一个Class Loader,序列化/反序列化零歧义;
- clientdemo 同样是application模块,但它不实现任何AIDL接口,只做调用方。它也依赖mylibrary,所以能拿到完全一致的IUserService接口定义和User类,调用时参数类型严格匹配,不会出现“expected User, got User”这种诡异错误。

这种设计让“跨进程”真正回归本质:进程隔离是Linux内核级的,而类定义一致性是构建系统保障的。你不需要记住“要把AIDL放哪个目录”,因为mylibrarybuild.gradle已经锁死路径;你不需要担心混淆规则漏写,因为mylibraryconsumer-rules.pro会自动合并到依赖它的Module中;你甚至不需要手动sync,Android Studio检测到settings.gradleinclude ':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才正式支持异步调用语义(之前虽能编译,但实际仍是同步阻塞),而ParcelabledescribeContents()返回值在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.gradleclientdemo/build.gradle(App级)
两者配置高度对称,核心差异在applicationIdAndroidManifest.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')这行,才是让IUserServiceUser类真正可用的命脉。

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对两者序列化方式不同——ListwriteTypedList()ArraywriteTypedArray(),工程里Order.javawriteToParcel()方法必须同时实现两种写法,否则任一参数都会为空。

最关键的registerCallbackunregisterCallback,体现了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.javaOrder.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再写nameUser(Parcel in)里就必须先in.readInt()in.readString(),错一位,后面所有字段全乱;
- CREATOR必须是public static final:这是Binder框架反射调用的入口,少一个修饰符,运行时直接NoSuchFieldException
- 无参构造函数不可省略:虽然User(Parcel in)是protected,但Binder在反序列化时会先调用无参构造创建对象,再调用CREATOR.createFromParcel()填充字段,没有它,ParcelreadValue()会失败;
- 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会崩溃。而ArraywriteTypedArray()内部已处理null,所以不用额外标记。

3.2 Service端Stub实现的线程模型与死亡代理

servicedemo/src/main/java/com/example/servicedemo/MyRemoteService.java是真正的Binder服务端,它的onBind()返回mBinder,而mBinderIUserService.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线程并发读写(addCallbackregisterCallback里,notifyLoginSuccesslogIn里),普通ArrayListConcurrentModificationException,而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.gradlesettings.gradle,第一反应可能是“这么多Gradle文件,怎么导入?”别慌,这是多项目结构的标准形态。以下是精确到点击步骤的操作指南:

第一步:启动Android Studio(推荐Flamingo或更高版本)
关闭所有已打开的项目,进入欢迎界面。

第二步:导入工程(不是“Open”)
点击 Open or Import → 在文件选择器中,选中解压后的根目录文件夹(即包含gradlew.batsettings.gradlemylibrary/等的文件夹),不要选里面的某个子文件夹。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视图中展开 servicedemobuildgeneratedaidl_source_output_dirdebugout,你应该能看到IUserService.javaICallback.javaUser.java等文件。如果看不到,说明mylibrarysourceSets没生效,检查mylibrary/build.gradle里是否有sourceSets.main.aidl.srcDirs = ['src/main/aidl']

第五步:运行servicedemo(先起服务)
在工具栏选择 servicedemo 作为启动Module → 点击绿色三角形 ▶️ 运行。AS会安装APK并启动一个空白Activity(servicedemoMainActivity是空的,只为拉起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全文onewayList<Order>ICallback参数均在此定义修改logInvoid logIn(String u),Sync后clientdemo里调用处会报红
Parcelable实现mylibrary/src/main/java/com/example/mylibrary/data/User.java45-70writeToParcel()CREATOR必须严格匹配注释掉dest.writeString(email)submitOrders调用后Service端email为null
Service端Stubservicedemo/src/main/java/com/example/servicedemo/MyRemoteService.java30-120mBinder继承StublogIn()在Binder线程执行logIn()里加Thread.sleep(2000),clientdemo UI会卡顿(证明未用oneway
客户端绑定clientdemo/src/main/java/com/example/clientdemo/MainActivity.java40-90ServiceConnectionbindService()调用注释bindService(),Logcat无Service connected日志
回调注册与触发clientdemo/src/main/java/com/example/clientdemo/MainActivity.java95-130mCallback实现和mService.registerCallback()onLoginSuccess()里加throw new RuntimeException("test"),Logcat可见崩溃堆栈
死亡代理处理servicedemo/src/main/java/com/example/servicedemo/MyRemoteService.java150-170linkToDeath()binderDied()回调onServiceConnected()后,手动杀掉clientdemo进程,Logcat应打印Client callback died

这个清单的价值在于:当你遇到问题,不用大海捞针,直接按表索骥。比如“回调不触发”,先看clientdemoregisterCallback()是否执行(Logcat有Registering callback吗?),再看servicedemoaddCallback()是否被调用(Logcat有Added callback吗?),最后看notifyLoginSuccess()callback.onLoginSuccess()是否抛RemoteException(Logcat有Callback failed吗?)。三层定位,问题无处遁形。

4.3 复杂场景实操:List 传递与oneway性能对比

工程已预置了submitOrders()方法,专门验证复杂参数。我们来一次真实的数据传递实验:

实验1:List 传递验证
clientdemo/MainActivity.javaonServiceConnected()里,添加:

// 构造测试数据
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.javawriteToParcel()writeTypedList()顺序错了,或者User.javaCREATOR没写对。

实验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端重启后抛clientdemomService引用未置null,仍在用旧IBinderadb logcat \| grep "MyRemoteService",看Service bound是否重复打印onServiceDisconnected()里必须mService = null,下次调用前检查if (mService != null)
仅在Release包抛proguard-rules.pro未keep IUserServiceUserunzip app-release.aab -d temp && find temp -name "*.class" \| grep UserServicemylibrary/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,否则ParcelNullPointerException
✅ 正确做法:在submitOrders()前过滤,orders.removeIf(Objects::isNull),或在Order.javawriteToParcel()里遍历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内的跨进程(servicedemoclientdemo共用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()会直接返回falseonServiceConnected()永远不会调用。

这个扩展方案,让工程从“学习Demo”蜕变为“可商用IPC框架”,权限粒度控制、签名强制校验,都是生产环境必备。

6. 实操心得与经验总结

我在实际项目里用这套三模块结构落地过5个IPC需求,从推送通道到硬件控制,踩过的坑比写的代码还多。最后想分享三个血泪换来的体会:

第一个体会:永远在mylibrary里写@Deprecated注释,而不是在servicedemoclientdemo里改代码
曾经有个需求,要把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_1main交替出现,比盯着Debugger窗口里灰色的线程名直观十倍。有时候,最原始的工具,恰恰是最高效的。

这套工程,我把它当作Android IPC的“乐高底板”——你可以往上搭任何模块:加个IMessageService做即时通讯,加个IHardwareService控制蓝牙,只要遵循mylibrary定义契约、servicedemo实现Stub、clientdemo调用,整个通信骨架稳如磐石。它不追求炫技,只确保每一步都经得起调试、推敲和线上考验。当你某天在Logcat里看到Login success: testuser那行日志,那一刻,你就真正摸到了Binder的脉搏。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接导入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规则已按需配置,可直接用于学习、验证或快速集成到现有项目中。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

本文章已经生成可运行项目
源码链接: https://pan.quark.cn/s/fa13cd6c6c8d Chrome浏览器作为一款备受青睐的网页浏览器,凭借其出色的稳定性和运行速度获得了广泛认可。 然而出于安全考量,Chrome系统默认不兼容ActiveX插件,因为ActiveX技术主要应用于Internet Explorer,它赋予网页内容用户本地系统交互的能力,但同时也可能引发潜在的安全隐患。 不过在某些特定工作场景下,比如在企业内部网络环境或需要老旧应用程序整合时,可能仍需在Chrome中启用ActiveX控件。 为此我们必须掌握在Chrome浏览器下加载和运用ActiveX的方法。 首先需要明确ActiveX的本质。 ActiveX是由微软设计的一种技术框架,旨在开发可在网页环境中运行的控件,这些控件能够完成多种功能,包括视频播放、应用程序组件运行或硬件设备通信等。 ActiveX控件多以OCX(OLE控件)格式发布。 在Chrome浏览器中启用ActiveX需要采取额外措施,因为该浏览器本身并不支持此项技术。 以下是几种常见的解决方案: 1. **应用Chrome的兼容性设置**:部分Chrome版本提供了" --enable-internal-activex"命令行参数,可通过此参数使浏览器具备加载ActiveX控件的能力。 用户可在启动Chrome时,于快捷方式的目标路径后附加该参数来激活此功能。 例如:"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe" --enable-internal-activex。 2. **安装第方插件**:市面上存在一些第方插件,例如"IE Tab"或"ActiveX Con...
标题SpringBoot微信小程序结合的健康饮食平台研究AI更换标题第1章引言介绍健康饮食平台的研究背景、意义、国内外研究现状、论文方法及创新点。1.1研究背景意义阐述健康饮食平台在当前社会的重要性及其市场需求。1.2国内外研究现状分析国内外健康饮食平台的发展现状及趋势。1.3研究方法及创新点概述本文采用的研究方法和技术创新点。第2章相关理论总结健康饮食、SpringBoot及微信小程序的相关理论。2.1健康饮食理论介绍健康饮食的基本原则和营养学知识。2.2SpringBoot框架阐述SpringBoot框架的特点、优势及在项目中的应用。2.3微信小程序技术介绍微信小程序的开发技术、特点及其用户群体。第3章健康饮食平台设计详细介绍健康饮食平台的设计方案,包括前端和后端设计。3.1平台架构设计给出平台的整体架构、模块划分及交互流程。3.2数据设计介绍数据的设计思路、表结构及数据关系。3.3前后端交互设计阐述前后端数据交互的方式、接口设计及安全性考虑。第4章微信小程序实现介绍微信小程序的具体实现过程,包括页面设计、功能实现等。4.1页面设计布局给出微信小程序的页面设计思路、布局及交互效果。4.2功能实现测试详细介绍微信小程序各项功能的实现过程及测试方法。4.3用户体验优化阐述如何提升微信小程序的用户体验,包括界面优化、性能优化等。第5章平台测试优化对健康饮食平台进行测试,并根据测试结果进行优化。5.1测试环境数据介绍测试环境、测试数据及测试方法。5.2测试结果分析从功能、性能、用户体验等方面对测试结果进行详细分析。5.3平台优化策略根据测试结果提出平台优化策略,包括代码优化、功能改进等。第6章结论展望总结本文的研究成果,并展望未来的研究方向。6.1研究结论概括本文的主要研究结论和平台实现效果。6.2展望指出本文研究的不足之处以及未来研究的方向和改进点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值