news 2026/9/26 14:03:05

Android AIDL跨进程通信全解析:从原理到实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Android AIDL跨进程通信全解析:从原理到实战

做Android跨进程通信这些年,绕不开的一个东西就是AIDL。我刚入行那会儿,被Binder和AIDL搞得一头雾水,光是搞清楚in、out、inout的区别就花了好几天。后来在项目里做过音乐播放器、消息推送、后台定位,几乎每个涉及到Service通信的场景都要跟AIDL打交道,踩过的坑多了,才慢慢摸清它的脾气。这篇文章就把我对AIDL的理解、实战经验、还有那些文档里不会明说的细节,一次性讲清楚。不管你是刚接触Android的初学者,还是做了几年开发想系统梳理一下这块知识的老手,这篇文章应该都能给你一些参考。

1. AIDL到底解决了什么问题

1.1 进程隔离与通信的本质需求

先聊一个最基础的问题:为什么需要AIDL?

Android系统里,每个App默认运行在独立的进程里,进程之间是相互隔离的。这就好比两个人在不同的房间,各自关着门,想递个东西过去,不能直接伸手递,得通过一个“窗口”来传递。这个“窗口”在Android里就是Binder机制,而AIDL就是我们描述这个“窗口”上应该有哪些动作的一种语言。

之所以需要跨进程,通常有这几个场景:

  • 同一个App里,某些组件(比如后台定位Service)单独跑在一个进程里,主进程需要跟它通信。
  • 不同App之间需要共享数据或调用能力,比如微信支付跳过去再回调结果。
  • 系统级服务,比如ActivityManager、WindowManager,它们运行在系统进程里,App要调用系统功能,本质上也是跨进程通信。

如果没有跨进程通信,以上这些场景全都做不了。而AIDL,就是Android官方提供的一套标准化的跨进程通信接口定义工具。

1.2 为什么是AIDL而不是其他方案

很多人会问:Android跨进程通信的方式不止AIDL一种,还有广播(BroadcastReceiver)、ContentProvider、Socket、Messenger,为什么很多场景偏偏选AIDL?

我个人的理解是这样的:

  • Intent + Bundle:只能携带有限类型的Bundle数据,适合简单传参,做不到高频率、双向的、方法级的调用。
  • 广播:是单向的、松耦合的,适合事件通知,不适合一问一答式的请求响应。而且广播性能一般,全局广播还容易被别的高耗电应用监听,导致安全问题。
  • ContentProvider:主要解决数据共享的问题,面向的是SQLite或者文件这类数据源,接口也比较固定(query、insert、update、delete),不适合传递自定义的复杂业务方法。
  • Messenger:底层其实封装的也是AIDL,但它只能支持Message这种格式的消息传递,发过去发回来都是消息,处理复杂逻辑就要自己拼协议,比较繁琐。
  • Socket:通用性强,但性能开销大,在Android的进程通信里属于“重武器”,平时不太用得上。

所以,当你需要跨进程调用另一个进程里的“方法”,并且希望调用方式跟本地调用差不多,参数和返回值都可以是自定义对象,同时还要有同步/异步、双向通信这些能力的时候,AIDL就是最合适的选择。

1.3 AIDL与Binder的关系

这里必须把AIDL和Binder的关系理清楚,否则后面看任何代码都会一头雾水。

Binder是Android系统底层的进程通信机制,它负责数据在两个进程之间真正地传输。而AIDL是一套“接口描述语言”,它本身不参与数据传输,它的作用是帮你生成一堆Java代码,这堆代码封装好了Binder通信的所有细节。

你可以把Binder理解为一条“管道”,AIDL就是管道的“图纸”。我们做的事情其实是:先画一张图纸(写.aidl文件),然后交给构建工具去施工(生成Java代码),最后拿着施工好的管道(Binder对象)去连接两个进程。

提示:AIDL文件编译后生成的代码,通常在build/generated/aidl_source_output_dir目录下。如果找不到生成的文件,多半是构建没有成功,或者.aidl文件放错了位置。这个目录可以作为排查问题的第一个突破口。

2. AIDL的核心语法与数据类型

2.1 支持的数据类型清单

AIDL不是什么数据类型都能用,它有一套自己的“白名单”,这一点很多初学者第一次写的时候就栽了跟头。基础支持的类型如下:

  • Java基本类型:int、long、boolean、float、double、byte、char、short。
  • String。
  • CharSequence。
  • List:里面的元素必须是AIDL支持的类型,可以是泛型声明,比如List<String>。
  • Map:同样要求key和value都是AIDL支持的类型。
  • Parcelable对象:必须是实现了Parcelable接口的类,并且在AIDL文件里要通过import引入。
  • IBinder对象:可以用来传递Binder引用,实现把另一个Binder对象传给对端。

不支持的类型包括:Integer、Long这些包装类型虽然在方法里可以用,但AIDL文件里写的时候要用基本类型;java.util.Date、JSONObject这类对象直接传是跑不起来的,需要自己转成Parcelable或者序列化成字符串再传。File、Bitmap这些类,也不能直接用,要么转成byte数组,要么实现Parcelable。

2.2 in、out、inout方向标记及其原理

这一块是面试最爱考的点,也是实际开发中特别容易出问题的地方。AIDL方法里的非基本类型参数,必须指定方向标记,三种标记的含义分别是:

  • in:客户端把数据传过去给服务端。数据只是单向流动,服务端对参数做的修改不会回传给客户端。
  • out:客户端不传数据给服务端,而是服务端负责填充数据,调用结束后客户端可以拿到服务端填好的值。相当于服务端“向外输出”。
  • inout:双向的。客户端传数据过去,服务端可以修改,修改后的结果会传回客户端。

原理上,in方向的参数会调用Parcel.writeToParcel()把你传入的对象完整写进Parcel里,服务端拿到的是一个全新的反序列化对象;out方向的参数则对应数据从服务端写入Parcel返回回来;inout就是两头都写,代价是数据拷贝两次,效率最低。

实操中我的建议是:能少用就少用inout,尤其是大对象。inout会让系统做两次序列化和反序列化,性能开销直接翻倍。大部分业务场景其实in就够了。如果服务端需要返回结果,直接写在返回值里更清晰。

2.3 oneway关键字与异步调用

在接口方法前面加上oneway关键字,表示这个调用是异步的。什么意思呢?客户端调用这个方法时,不会阻塞等待服务端返回,而是立刻返回。这个方法适合用在那些耗时操作、或者你根本不关心返回结果的通知型调用上。

需要注意两点:

  • oneway方法不能有返回值,也不能有out或inout方向的参数,因为客户端不等结果,就没法接收返回的东西。
  • oneway只是让客户端不等待,但服务端处理该请求仍然是在Binder线程池中执行的,不意味着服务端会开启新线程。它只解决客户端阻塞的问题。

场景举例:音乐播放器里,通知播放服务“暂停”“下一曲”这种操作,就不需要阻塞,用oneway再合适不过。但如果要查询当前播放进度,那就必须用同步方法,等着返回值。

3. 从零到一:手写一个完整的AIDL通信Demo

3.1 定义接口与Parcelable对象

空谈原理没意思,直接跑一个Demo。我这里的场景是做一个简单的音乐播放助手:客户端(比如一个Activity)向播放服务发起播放请求,服务端执行播放、上报进度、还可以接收客户端的主动查询。

第一步,新建一个MusicData类,实现Parcelable接口。这个类用来封装歌曲信息,比如歌名、歌手、时长、当前进度。注意,AIDL跨进程传输对象时,只能是Parcelable,不能是Serializable。为什么?因为Binder底层本身就是基于Parcel进行内存共享和拷贝的,Parcelable在效率和设计上更契合这套机制。

public class MusicData implements Parcelable { private String songName; private String artist; private int duration; private int progress; public MusicData(String songName, String artist, int duration) { this.songName = songName; this.artist = artist; this.duration = duration; } protected MusicData(Parcel in) { songName = in.readString(); artist = in.readString(); duration = in.readInt(); progress = in.readInt(); } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeString(songName); dest.writeString(artist); dest.writeInt(duration); dest.writeInt(progress); } @Override public int describeContents() { return 0; } public static final Creator<MusicData> CREATOR = new Creator<MusicData>() { @Override public MusicData createFromParcel(Parcel in) { return new MusicData(in); } @Override public MusicData[] newArray(int size) { return new MusicData[size]; } }; // getter和setter省略 }

字段的写入和读取顺序必须完全一致,writeString对应readString,writeInt对应readInt,顺序错了,数据全乱。

第二步,创建AIDL文件。这个文件的位置有讲究:在Android Studio里,默认应该放在src/main/aidl目录下,包名必须跟MusicData所在包名一致,这样才能在AIDL文件中通过import找到它。

创建IMusicAidlInterface.aidl:

// IMusicAidlInterface.aidl package com.example.musicdemo; import com.example.musicdemo.MusicData; interface IMusicAidlInterface { void playMusic(MusicData music); void pauseMusic(); void stopMusic(); MusicData queryMusicStatus(); void registerCallback(IMusicCallback callback); void unregisterCallback(IMusicCallback callback); }

这里我故意加了一个回调——跨进程双向通信。客户端注册一个回调对象,服务端播放状态变化时主动通知客户端。这个能力在真实项目中非常常用,协议上就是AIDL支持把Binder对象作为参数传进接口方法。

同步创建一个IMusicCallback.aidl:

// IMusicCallback.aidl package com.example.musicdemo; interface IMusicCallback { void onMusicProgressChanged(int progress); void onMusicPlayStateChanged(boolean isPlaying); }

注意回调接口里没有MusicData,所以不需要import,直接定义即可。

3.2 服务端实现与注册

服务端是一个Service,核心逻辑是把AIDL接口定义的Stub实现出来,然后通过onBind返回给客户端。

重建一个MusicService:

public class MusicService extends Service { private boolean isPlaying = false; private int currentProgress = 0; private final CopyOnWriteArrayList<IMusicCallback> callbackList = new CopyOnWriteArrayList<>(); private final IMusicAidlInterface.Stub binder = new IMusicAidlInterface.Stub() { @Override public void playMusic(MusicData music) throws RemoteException { // 模拟播放逻辑 isPlaying = true; currentProgress = 0; new Thread(new Runnable() { @Override public void run() { try { while (isPlaying && currentProgress < music.getDuration()) { Thread.sleep(1000); currentProgress += 1000; for (IMusicCallback callback : callbackList) { callback.onMusicProgressChanged(currentProgress); } } isPlaying = false; for (IMusicCallback callback : callbackList) { callback.onMusicPlayStateChanged(false); } } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); } @Override public void pauseMusic() { isPlaying = false; } @Override public void stopMusic() { isPlaying = false; currentProgress = 0; } @Override public MusicData queryMusicStatus() throws RemoteException { MusicData data = new MusicData("Current Song", "Unknown Artist", 240000); data.setProgress(currentProgress); return data; } @Override public void registerCallback(IMusicCallback callback) { if (callback != null && !callbackList.contains(callback)) { callbackList.add(callback); } } @Override public void unregisterCallback(IMusicCallback callback) { if (callback != null) { callbackList.remove(callback); } } }; @Override public IBinder onBind(Intent intent) { return binder; } }

这里我用了一个CopyOnWriteArrayList来存储回调对象,因为可能有多个客户端注册回调,而且Binder回调是在Binder线程池中触发的,涉及线程安全。CopyOnWriteArrayList在写少读多的场景下性能不错,同时迭代时不会抛ConcurrentModificationException。

然后在AndroidManifest.xml里注册Service,并跨App访问时设置android:exported="true"(如果只是本App内部使用,建议false):

<service android:name=".MusicService" android:exported="true" android:process=":remote"> <intent-filter> <action android:name="com.example.musicdemo.MusicService" /> </intent-filter> </service>

指定android:process=":remote",是为了让Service跑在独立进程里,这样才是真正意义上的跨进程通信。如果不单独指定进程,默认跑在App主进程里,虽然也能用AIDL,但就失去了跨进程的意义。

3.3 客户端绑定与调用

客户端绑定Service的标准流程:先bindService,然后在ServiceConnection的回调里拿到IBinder,再通过IMusicAidlInterface.Stub.asInterface()转成接口对象。

public class MainActivity extends AppCompatActivity { private IMusicAidlInterface musicService; private IMusicCallback callback = new IMusicCallback.Stub() { @Override public void onMusicProgressChanged(int progress) { runOnUiThread(new Runnable() { @Override public void run() { // 更新UI进度条 progressBar.setProgress(progress); } }); } @Override public void onMusicPlayStateChanged(boolean isPlaying) { runOnUiThread(new Runnable() { @Override public void run() { // 更新播放按钮状态 } }); } }; private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { musicService = IMusicAidlInterface.Stub.asInterface(service); try { musicService.registerCallback(callback); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { musicService = null; } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 绑定服务 Intent intent = new Intent("com.example.musicdemo.MusicService"); intent.setPackage(getPackageName()); bindService(intent, connection, Context.BIND_AUTO_CREATE); } @Override protected void onDestroy() { super.onDestroy(); if (musicService != null) { try { musicService.unregisterCallback(callback); } catch (RemoteException e) { e.printStackTrace(); } } unbindService(connection); } }

回调对象为什么放在客户端而不是直接匿名创建?因为回调跟Service连接是绑定的,而且需要保证在unregisterCallback时传的是同一个对象。如果你在registerCallback里new了一个匿名对象,然后在unregisterCallback里又new一个,服务端的CopyOnWriteArrayList里找不到相等的对象,就会移除失败。这是新手小白最容易踩的坑。

另外需要注意,从Android 8.0(API 26)开始,bindService时如果使用的是隐式Intent,必须同时设置setPackage(),否则会报IllegalArgumentException。Android 5.0以上,隐式绑定Service还需要在Intent里明确指定包名或组件名。

3.4 AIDL文件生成失败与编译异常的排查

这个主题在开发中简直太常见了。我遇到过几种典型情况,这里专门列出来给读者排查:

  • AIDL文件里的包名与import路径不匹配。AIDL里package声明、import路径、以及Parcelable对象实际所在的包名,三者必须完全一致。如果不一致,构建报错,生成目录里看不到任何以Stub结尾的Java文件。
  • AIDL文件没有放在src/main/aidl目录下。老版本AS支持放java目录旁边,新版本推荐放aidl目录。如果放错了,Gradle不会扫描到,导致编译过但运行时找不到类,或者直接编译失败。
  • Parcelable类没有实现Parcelable接口,或者CREATOR字段缺失。AIDL生成的代码会依赖这个字段来反序列化对象。
  • 方法里使用了不支持的类型,比如传了Integer(用包装类型)、JSONObject、ArrayList<Object>。编译时会报“unsupported type”之类的错误。
  • Android Studio缓存问题:改了.aidl文件但编译结果没有变化,尝试Build -> Clean Project,再Build -> Rebuild Project。如果还不行,关掉AS,删除.gradle和build目录,重新打开再编译。
  • Kotlin项目里的AIDL:如果项目用了Kotlin,AIDL生成的是Java代码,不影响调用。但在Kotlin里引用Java生成的静态字段如Stub.asInterface,语法上不用额外处理,直接调用即可。

这些坑,每一条我都在实际项目中踩过,报错信息五花八门,根源多半是路径或类型问题。建议写了AIDL文件之后,先把项目整个Rebuild一遍,确认生成目录里出现了对应的Java文件,再接下去写逻辑,能省很多莫名其妙的调试时间。

4. AIDL的进阶用法:连接池、死亡回调与权限校验

4.1 Binder连接池(BinderPool)

随着业务变大,AIDL接口会越来越多。如果每个接口都对应一个Service,那资源开销太大了。更合理的做法是使用Binder连接池模式:一个Service管理多个Binder对象,客户端通过一个统一的query接口,传入业务标识,取出对应的Binder,再转换成对应的AIDL接口。

我举个例子:一个App里有下载模块和支付模块。下载需要跨进程AIDL,支付也需要跨进程AIDL。如果不做连接池,就要建两个Service、绑定两次。做成连接池,则只需要一个Service,它内部维护各个模块的Binder,客户端根据binderCode查询即可。

核心实现大致是这样的:

public class BinderPoolService extends Service { public static final int BINDER_CODE_MUSIC = 1; public static final int BINDER_CODE_DOWNLOAD = 2; private final IBinder binderPool = new BinderPool.Stub() { @Override public IBinder queryBinder(int binderCode) { if (binderCode == BINDER_CODE_MUSIC) { return new MusicBinder(); } else if (binderCode == BINDER_CODE_DOWNLOAD) { return new DownloadBinder(); } return null; } }; @Override public IBinder onBind(Intent intent) { return binderPool; } }

queryBinder的返回值是IBinder,这其实是把Binder对象直接暴露给了客户端,客户端再通过Stub.asInterface()转成具体接口。这样做的好处是Service的数量始终是一个,而且每个业务模块的AIDL编写方式保持不变,只是入口收敛了。

实现连接池的时候,客户端侧最好也做一个单例管理,避免每次都去bindService。绑定一次Service之后,用SparseArray<IBinder>缓存查询到的Binder对象,后续直接用缓存的接口调用。同时要处理Service连接断开的情况,重连之后清空缓存重新绑定。

4.2 客户端与服务端死亡回调(DeathRecipient)

跨进程通信有一个很现实的问题:进程会因为各种原因死掉。当服务端进程挂了,客户端还持有一个无效的Binder引用,这个时候调用任何方法都会抛DeadObjectException,应用甚至可能直接崩溃。

正确的做法是给Binder注册一个DeathRecipient,当Binder连接的进程死亡时,系统会回调binderDied()。在这个回调里,你可以重新绑定服务,或者通知UI刷新状态。

代码示例:

private IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { if (musicService == null) return; musicService.asBinder().unlinkToDeath(deathRecipient, 0); musicService = null; // 重新绑定服务 bindService(new Intent("com.example.musicdemo.MusicService"), connection, Context.BIND_AUTO_CREATE); } }; // 绑定成功后注册 @Override public void onServiceConnected(ComponentName name, IBinder service) { musicService = IMusicAidlInterface.Stub.asInterface(service); try { service.linkToDeath(deathRecipient, 0); musicService.registerCallback(callback); } catch (RemoteException e) { e.printStackTrace(); } }

linkToDeath注册后,一定要记得在合适的时机unlinkToDeath,否则会造成内存泄露,而且重复绑定后回调可能被触发多次。

4.3 服务端权限校验

AIDL接口默认情况下是开放的,任何App只要知道Service的action和包名,就能绑定并调用。如果不是希望对外暴露的服务,一定要做权限校验。

常用的校验方式有两种:

  • 自定义权限:在AndroidManifest.xml里定义权限,服务端onBind时检查调用方的权限。但自定义权限比较麻烦,需要客户端也声明相应权限,部署成本较高。
  • UID校验:getCallingUid()拿到调用方进程的UID,然后在服务端通过PackageManager查询该UID对应的包名,跟白名单比对。这是最常用也最简单的做法。

示例:

String[] allowedPackages = {"com.example.musicdemo"}; @Override public IBinder onBind(Intent intent) { int callingUid = Binder.getCallingUid(); String callingPackage = getPackageManager().getNameForUid(callingUid); for (String pkg : allowedPackages) { if (pkg.equals(callingPackage)) { return binder; } } return null; }

getCallingUid()很关键:它获取的是真正发起调用的那个进程的UID,而不是当前Service进程自己的UID。如果不区分,权限校验就是形同虚设。

5. 实战中的高频问题与排查技巧

5.1 跨进程传递大数据对象时的性能瓶颈

AIDL底层的数据传递,本质上是把对象序列化到Parcel里,然后在内核空间和用户空间之间进行拷贝。传递几十KB的小对象问题不大,但如果要传几MB的Bitmap或者长字符串,那就会非常慢,甚至导致TransactionTooLargeException。

这个异常是AIDL开发中最让人头疼的问题之一。Binder事务的缓冲区在Android系统里是有上限的,通常在1MB左右,但实际安全范围更小。如果你往AIDL方法里塞了大对象,比如一张高分辨率图片的byte数组,调用时八成会崩在这里。

我的经验是,大数据的跨进程传输,尽量不要走AIDL方法参数这条路。替代方案可以是:

  • 先把大数据写到App私有目录或缓存文件里,然后通过AIDL传递文件路径,对端自己读取文件。
  • 用MemoryFile或者SharedMemory共享内存来传递大块数据,但实现复杂,一般场景用不上。

5.2 多线程并发与Binder线程池的竞争问题

服务端AIDL方法的调用并不是在主线程执行,而是在Binder线程池里的多个线程中执行。这意味着,如果你在AIDL方法里直接修改了共享的成员变量(比如播放器的状态),就可能出现线程安全问题。

解决思路:

  • 对共享状态加锁,比如用synchronized或者ReentrantLock。
  • 状态尽量放在线程安全的容器里,比如ConcurrentHashMap、CopyOnWriteArrayList。
  • 不要在AIDL接口方法里做耗时操作。虽然是Binder线程池,但它有上限(通常默认是16个线程),方法阻塞时间过长,会耗尽线程池,导致后续调用排队。

客户端这边也要注意:调用同步的AIDL方法时,主线程会被阻塞,直到服务端返回。如果服务端方法耗时超过5秒,还可能导致系统弹ANR。因此,耗时接口在客户端也应该放到子线程去调用。

5.3 自定义Parcelable类如何放到独立库模块

工程复杂度上来之后,很多团队会把数据模型抽到独立的Library模块,AIDL接口也可能会分散在不同模块里。这时候需要注意:

  • AIDL文件必须与其引用的Parcelable类在编译期是可见的。如果Parcelable类在Library模块里,AIDL文件要么也放在那个模块,要么确认该模块以api方式被主模块依赖,使得类对主模块的AIDL编译器可见。
  • 多个模块都定义了相同包名的AIDL接口,会导致duplicate class冲突。建议每个模块用不同的包名后缀来隔离。

5.4 常见问题速查表

我在下面整理了一张速查表,方便你在实际开发时快速定位问题。

现象可能原因排查与解决
编译报AIDL文件找不到类型包名不一致、import路径错误、Parcelable类不在编译路径检查.aidl里的package和import,与类的实际包名逐一比对
生成目录下没有Stub类AIDL文件没放在src/main/aidl移到正确目录,Build -> Rebuild
运行时抛DeadObjectException服务端进程挂了,客户端还持有Binder引用注册DeathRecipient,死亡后重新绑定
调用AIDL方法卡死服务端方法耗时太长,Binder线程池被打满避免在服务端执行耗时操作,或者换成oneway异步接口
传大对象直接崩溃超过Binder事务大小上限换成文件路径传递,或者用共享内存方案
客户端与服务端数据的字段乱掉Parcelable的写入顺序和读取顺序不一致检查writeToParcel和Parcel构造函数里的读写顺序

6. 从使用到原理:AIDL生成代码的核心机制

6.1 生成的Stub、Proxy、asInterface分别是什么

很多读者用AIDL很熟练,但没看过生成的代码长什么样,这会导致面试或者排查复杂问题时心里没底。实际上,.aidl文件编译后,会生成一个同名的Java文件,里面有三个核心组成部分:

  • Stub:继承Binder的抽象类,里面实现了onTransact()方法。服务端继承Stub并实现业务方法。onTransact()根据方法ID分发到对应的业务方法。
  • Proxy:内部类,是客户端调用时的代理。Proxy持有IBinder引用,每次调用方法时,把参数写入Parcel,然后调用mRemote.transact(),等待服务端返回。
  • asInterface():静态方法,根据返回的IBinder是本地进程还是远程进程,决定直接返回Stub对象还是Proxy对象。如果Binder对象在同一个进程,直接强转返回,省去代理这一层。

理解这个机制后,你就明白“进程内调用AIDL接口没走Binder传输,但代码路径跟跨进程不一样”这句话的含义了。

6.2 Binder驱动的数据拷贝过程

Binder通信的底层机制,相比传统IPC省了一次内存拷贝。传统IPC(比如管道、Socket)通常需要两次拷贝:用户空间到内核空间,再从内核空间到另一个用户空间。Binder利用内核里的映射机制,只做一次拷贝。

一次完整的数据流程是这样的:

  1. 客户端通过Proxy把方法调用数据写到Parcel,然后调用transact()。
  2. 系统调用binder_ioctl进入内核态,Binder驱动把客户端进程里的数据拷贝到内核缓冲区。
  3. 服务端进程通过mmap映射同一块物理内存,直接读取到数据,不需要再从这个缓冲区拷贝到自己的用户空间(这里准确说是一侧拷贝,另一侧通过映射读取)。
  4. 服务端处理完毕,返回结果时同理。

这也是为什么AIDL在Android里会成为跨进程通信首选的原因之一:性能足够好。

6.3 手动实现Binder接口而不写AIDL

AIDL不是必须的。你可以手动定义类,直接继承Binder,然后重写onTransact(),实现服务端;客户端继承BinderProxy或者使用Binder的引用直接调用transact()。

但现实是,手动实现容易出错,尤其是Parcel的读写顺序、事务码的管理、异步事务的支持,都要自己维护。AIDL帮你把所有这些逻辑生成了代码,等于官方把最笨重、最容易出错的部分替你写好了。除非是系统级开发或者特殊性能要求的场景,否则我都建议直接使用AIDL。

7. 关于AIDL的一些心得与实用建议

这几年做Android,越来越觉得AIDL其实是一门被低估的“基本功”。很多人觉得它只是面试题,实际项目中都用LiveData、协程,跨进程通信好像用不到了。但只要你做插件化、后台保活、多进程架构、系统级开发,或者对接硬件厂商的SDK,AIDL就是绕不开的底层能力。

给各位几个建议:

第一,写AIDL文件时一定要先想好方向标记。in、out、inout不只是语法标记,它直接影响底层数据拷贝的次数和性能。默认能用in就用in,别图省事全都写inout。

第二,回调接口务必处理好反注册。客户端销毁时,如果忘了unregisterCallback,服务端持有的回调对象会一直存在,造成服务端内存泄漏,严重时客户端进程都被服务端强引用无法释放。

第三,调试AIDL的时候多利用生成代码。遇到奇怪问题,直接去看build/generated下的Stub类,逐步推演数据的读写流程,往往比瞎猜效率高得多。

第四,权限校验不要省。即便你的Service声明了exported=false,在部分系统版本上,通过隐式Intent仍可能被恶意应用探测。内部服务也建议在onBind里用UID校验一下调用方包名。

AIDL这套东西,表面上是写好一个.aidl文件,编译,然后Stub.asInterface()一调就完事了。但深层的工作量都在细节里,比如序列化性能、并发安全、生命周期管理、权限控制。把这些问题都处理好了,你的跨进程通信模块才算是真正稳了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 14:01:39

WeChatAppEx.exe与xweb_elf.dll故障深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/26 14:00:03

QtCipher实战:SQLite数据库文件加密与常见坑

简介&#xff1a;Sqlite加密插件QtCipher是一份面向Qt开发者的SQLite加密工程资源&#xff0c;基于sqlitecipher库为SQLite数据库提供文件级加密能力&#xff0c;帮助在桌面、移动或嵌入式应用中保护敏感数据&#xff0c;同时维持SQLite的轻量特性。压缩包共23个文件&#xff0…

作者头像 李华
网站建设 2026/9/26 14:00:01

AI大模型在数字营销与视频场景的实战:从流程拆解到工程落地

1. 从标题到落地&#xff1a;AI大模型在数字营销与视频场景的真实切入点“AI大模型在数字营销技术和视频类的应用实战”这个标题&#xff0c;乍看像是一份行业白皮书的目录&#xff0c;但我更愿意把它理解成一个一线操盘手在真实项目里反复折腾之后&#xff0c;沉淀下来的经验集…

作者头像 李华
网站建设 2026/9/26 13:59:59

组合模式实战:用统一接口优雅处理树形数据与菜单递归

做后端开发这些年&#xff0c;只要一聊到树形数据&#xff0c;组合模式&#xff08;Composite&#xff09;就一定会被拉出来。仔细想想&#xff0c;我们日常接触的商品分类、权限菜单、组织架构、目录文件&#xff0c;哪一个不是天然的多叉树&#xff1f;可问题是&#xff0c;很…

作者头像 李华