(一)Android IPC简介
1.IPC是什么?
进程间通信或者跨进程通信,两个进程间进行数据交互的一个过程。
2.进程与线程之间的关系?
线程是CPU调度的最小单元。
而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。
补充:进程的五种优先级
- 前台进程
- 即用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:
- 托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
- 托管某个 Service,该Service绑定到用户正在交互的 Activity
- 托管正在前台运行的 Service(服务已调用 startForeground())
- 托管正在执行生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
- 托管正执行其 onReceive() 方法的 BroadcastReceiver
- 通常,在任意给定时间前台进程都为数不多,只有在内存不足以支持它们同时继续运行这一情况下系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应
- 可见进程
- 没有任何前台组件,但仍会影响用户在屏幕上所见内容的进程。如果一个进程满足以下任一条件,即视为可见进程:
- 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个覆盖一部分屏幕的对话框时,就会出现这种情况
- 托管绑定到可见或前台Activity 的 Service
- 可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程
- 服务进程
- 正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别的进程。
- 尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态
- 后台进程
- 包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。
- 这些进程对用户体验没有直接影响,系统可能会随时终止它们以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态
- 空进程
- 不含任何活动应用组件的进程。
- 保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程
3.什么是ANR应用无响应?
主线程中去执行耗时任务,就会造成界面无法响应(ANR),严重影响用户体验。
解决方法:把一些耗时的任务放在子线程中即可。
4.不同平台的IPC机制。
Windows上:剪贴板、管道和邮槽等;
Linux上:命名管道,共享内容、信号量等;
Android:Binder实现进程间通信,Socket也可以实现任意两个终端之间的通信。
5.多进程通信的应用场景?
- ContentProvider;
- 多进程应用:单进程容易受到内存限制而OOM,部分模块比如WebView可以单独作为一个进程
(二)Android中的多进程模式
使用android:process属性可以轻易开启多进程模式;但多进程未必比单个进程好。
(1)开启多进程模式
Android中多进程是指一个应用中存在多个进程的情况,一般只有给四大组件(Activity、BroadcastReceiver、Service、Receiver)指定android:process一种方法。
2.1.1相关代码:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
<activity android:name=".Main2Activity"
android:process=":remote"/>
2.1.2 效果:
使用DDMS视图或者命令可以查看:adb shell ps | findstr com.example.hzk.myapplication1
前提是通信:
2.1.3 注意事项:
Main2Activity和Main3Activity的“:”和“.”的区别:
(1)“:”是简写;“.”需要使用全称,包括包名。
(2)”:”开头的属当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以”:”开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一进程中。
每个应用有一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程由是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。分享的信息包括:data目录、组件信息等。
(2)多进程模式的运行机制
2.2.2多进程存在的问题
- 静态成员和单例模式完全失效(每个进程都分配一个独立的虚拟机,有不同的地址空间,于是有不同的对象备份,而不是同一份);
- 线程同步机制完全失效(不是一块内存,所以不同进程锁住的不是同一个对象);
- SharedPreferences的可靠性下降(不支持两个进程同时执行写操作)。
- Application会多次创建(两个不同的虚拟机和Application的)。
2.2.3实现跨进程通信的方式
- Intent来传递数据,
- 共享文件和SharedPreferences,
- 基于Binder的Messenger和AIDL
- Socket
(三)IPC基础概念介绍
- Serializable和Parcelable可以完成对象的序列化过程,当我们需要通过Intent和Binder传输数据时,或者在对象持久化到存储设备上或者通过网络传输给其他客户端就需要使用序列化。
- 序列化是将对象的状态信息转换为可以存储或传输的二进制字节流形式的过程。
(1)Serializable接口
Serializable是java中提供的一个序列化接口,它是一个空接口,对象实现这个Serializable接口,就标记这个对象是可序列化的。
serialVersioUID作用:
工作机制:序列化的时候会把当前类的serialversionUID写进序列化的文件中,当反序列化的时候系统会去检测文件中serialversionUID,看它是否和当前类的serialversionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化;否则就说明当前类和序列化的类相比发生了某些变换,比如成员,类型可能发生了变化,这个时候是无法正常的反序列化的。
-
当版本升级后,我们可能删除了某个变量或者新增加了一些新的成员变量,只要serialVersionUID不变,这个时候我们的反向序列化过程仍然能够成功,程序仍然能够最大限度的恢复数据。
-
但如修改了类名,修改了成员常量的类型,即使serialVersionUID通过验证,也会反序列化失败。
-
如果没有手动指定serialVersionUID,系统也会根据当前类的结构生成它的hahs值。
-
需要注意的是:
(1)静态成员变量属于类不属于对象,所以不会参与序列化过程;
(2)transient关键字标记的成员变量不参与序列化过程。
(2)Parcelable接口
Parcelable是一个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。
3.2.1代码实现:
public class User implements Parcelable{
public int userId;
public String userName;
public boolean isMale;
protected User(int userId,String userName,boolean isMale) {
this.userId = userId;
this.userName = userName;
this.isMale = isMale;
}
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(userId);
out.writeString(userName);
out.writeInt(isMale?1:0);
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
public User(Parcel in) {
userId = in.readInt();
userName =in.readString();
isMale = in.readInt() ==1;
}
@Override
public int describeContents() {
return 0;
}
}
3.2.2每个实现方法的功能:
序列化功能由writeToParcel方法来完成,最终是通过Parcel中的一系列write方法来完成的,
反序列化功能由CREATOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel的一系列read方法来完成反序列化过程。User方法从序列化后的对象中创建原始对象。
在Book(Parcel in)方法中,如果有一个成员变量是另一个可序列化对象,在反序列化过程中需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
这里我推荐一个插件android-parcelable-intellij-plugin,专门用于自动生成这些重复样本代码。
Parcelable和Serializable如何选择?
- 两者最大的区别在于 存储媒介的不同,Serializable 使用 I/O 读写存储在硬盘上,而 Parcelable 是直接 在内存中读写。很明显,内存的读写速度通常大于 IO 读写,所以在 Android 中传递数据优先选择 Parcelable
- Serializable 会使用反射,序列化和反序列化过程需要大量 I/O 操作, Parcelable 自已实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在 Native 内存中,效率要快很多。
- Parcelable 的性能比 Serializable 好,在内存开销方面较小,所以在内存间数据传输时推荐使用 Parcelable(如 Activity 间传输数据)。
- 而 Serializable 可将数据持久化方便保存,所以在需要保存或网络传输数据时选择 Serializable,因为 Android 不同版本 Parcelable 可能不同,所以不推荐使用 Parcelable进行数据持久化.
2.3.3 Binder
Binder是Android中的一个类,实现了 IBinder 接口。从IPC角度说,Binder是Andoird的一种跨进程通讯方式,Binder还可以理解为一种虚拟物理设备,它的设备驱动是/dev/binder。从Android Framework角度来说,Binder是 ServiceManager 连接各种Manager( ActivityManager· 、 WindowManager )和相应 ManagerService 的桥梁。从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService时,服务端返回一个包含服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务器端提供的服务或者数据( 包括普通服务和基于AIDL的服务)。
Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。
图中的Client,Server,Service Manager之间交互都是虚线表示,是由于它们彼此之间不是直接交互的,而是都通过与Binder驱动进行交互的,从而实现IPC通信方式。其中Binder驱动位于内核空间,Client,Server,Service Manager位于用户空间。Binder驱动和Service Manager可以看做是Android平台的基础架构,而Client和Server是Android的应用层,开发人员只需自定义实现client、Server端,借助Android的基本平台架构便可以直接进行IPC通信。
Android中Binder主要用于 Service ,包括AIDL和Messenger。普通Service的Binder不涉及进程间通信,Messenger的底层其实是AIDL,所以下面通过AIDL分析Binder的工作机制。
由系统根据AIDL文件自动生成.java文件
- Book.java
表示图书信息的实体类,实现了Parcelable接口。
- Book.aidl
Book类在AIDL中的声明。
- IBookManager.aidl
定义的管理Book实体的一个接口,包含 getBookList 和 addBook 两个方法。尽管Book类和IBookManager位于相同的包中,但是在IBookManager仍然要导入Book类。
- IBookManager.java
系统为IBookManager.aidl生产的Binder类,在 gen 目录下。
IBookManager继承了 IInterface 接口,所有在Binder中传输的接口都需要继IInterface接口。结构如下:
- 声明了 getBookList 和 addBook 方法,还声明了两个整型id分别标识这两个方法,用于标识在 transact 过程中客户端请求的到底是哪个方法。
- 声明了一个内部类 Stub ,它继承自Binder,并且是一个抽象类,并没有实现getBookList 和 addBook 方法,这两个方法需要服务端new Stub的时候去实现。
- 当客户端和服务端位于同一进程时,方法调用不会走跨进程的 transact 。
- 当二者位于不同进程时,方法调用需要走 transact 过程,就会调用到 Stub 的内部代理类 Proxy中的方法
- Stub 的内部代理类 Proxy,由客户端来使用。它实现了IBookManager接口,并且实现了 getBookList 和 addBook 方法,但是里面只是把数据装进data这个Parcel对象,通过mRemote的transact方法发送给服务端,接着用reply这个Parcel对象等待服务端数据的返回,这一切都是通过mRemote这个IBinder对象进行,mRemote代表着Binder对象的本地代理,mRemote会通过Binder驱动来完成与远程服务端的Stub的通信。
- 这个接口的核心实现就是它的内部类 Stub 和 Stub 的内部代理类 Proxy 。
Stub和Proxy类的内部方法和定义
-
DESCRIPTOR
Binder的唯一标识,一般用Binder的类名表示。
-
asInterface(android.os.IBinder obj)
将服务端的Binder对象转换为客户端所需的AIDL接口类型的对象,如果C/S位于同一进
程,此方法返回就是服务端的Stub对象本身,否则返回的就是系统封装后的Stub.proxy对
象。所以,跨进程通信中客户端是使用代理对象来实现通信的。
-
asBinder
返回当前Binder对象。
-
onTransact
这个方法运行在服务端的Binder线程池中,由客户端发起跨进程请求时,远程请求会通过
系统底层封装后交由此方法来处理。该方法的原型是
java public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags)
- 服务端通过code确定客户端请求的目标方法是什么,
- 接着从data取出目标方法所需的参数,然后执行目标方法。
- 执行完毕后向reply写入返回值( 如果有返回值) 。
- 如果这个方法返回值为false,那么服务端的请求会失败,利用这个特性我们可以来做权限验证。
-
Proxy#getBookList 和Proxy#addBook
这两个方法运行在客户端,内部实现过程如下:
- 首先创建该方法所需要的输入型对象Parcel对象_data,输出型Parcel对象_reply和返回值对象List。
- 然后把该方法的参数信息写入_data( 如果有参数)_
- _接着调用transact方法发起RPC( 远程过程调用) ,同时当前线程挂起
- 然后服务端的onTransact方法会被调用知道RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply中的数据。
AIDL文件不是必须的,之所以提供AIDL文件,是为了方便系统为我们生成IBookManager.java,但我们完全可以自己写一个。
Binder的工作机制
- 由于当前线程会被挂起知道服务端进程返回数据,所以远程请求如果很耗时,则不能在UI线程在发起;
- Binder方法需要用同步的方法去实现,因为他已经运行在一个线程中了。
linkToDeath和unlinkToDeath
如果服务端进程异常终止,我们到服务端的Binder连接断裂。但是,如果我们不知道Binder连接已经断裂,那么客户端功能会受影响。通过linkTODeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知。
- 首先,声明一个 DeathRecipient 对象。 DeathRecipient 是一个接口,只有一个方法 binderDied ,当Binder死亡的时候,系统就会回调 binderDied 方法,然后我们就可以重新绑定远程服务。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){
@Override
public void binderDied(){
if(mBookManager == null){
return;
}
mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
mBookManager = null;
}
}
然后,在客户端绑定远程服务成功后,给binder设置死亡代理:
mService = IBookManager.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
- 另外,可以通过Binder的 isBinderAlive 判断Binder是否死亡。
2.4 Android中的IPC方式
主要有以下方式:
- Intent中附加extras
- 共享文件
- Binder
- ContentProvider
- Socket
2.4.1 使用Bundle
- 四大组件中的三大组件( Activity、Service、Receiver) 都支持在Intent中传递 Bundle 数据。
- Bundle实现了Parcelable接口,因此可以方便的在不同进程间传输。
- 当我们在一个进程中启动了另一个进程的Activity、Service、Receiver,可以再Bundle中附加我们需要传输给远程进程的消息并通过Intent发送出去。被传输的数据必须能够被序列化。
但是在Intent 传输数据的过程中,用到了 Binder,Intent中的数据,即Bundle数据,会作为 Parcel 存储在Binder 的事务缓冲区(Binder transaction buffer)中的对象进行传输,而这个 Binder 事务缓冲区具有一个有限的固定大小,约为1MB,而且这个1Mb并不是当前进程所独享,而是所有进程共享的,所以由于1Mb的限制,Bundle不能存放大量的数据,不然会报TransactionTooLargeException,并且Bundle中存放的数据也要求能够被序列化,所以Bundle只适用于数据量小和简单的进程间通信。
2.4.2 使用文件共享
- 我们可以序列化一个对象到文件系统中的同时从另一个进程中恢复这个对象。
- 通过 ObjectOutputStream / ObjectInputStream 序列化一个对象到文件中,或者在另一个进程从文件中反序列这个对象。注意:反序列化得到的对象只是内容上和序列化之前的对象一样,本质是两个对象。
- Android基于Linux,文件支持并发读写。而文件并发读写会导致读出的对象可能不是最新的 。所以文件共享方式适合对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写问题。
- SharedPreferences 底层实现采用XML文件来存储键值对。系统对它的读/写有一定的缓存策略,即在内存中会有一份 SharedPreferences 文件的缓存,因此在多进程模式下,系统对它的读/写变得不可靠,面对高并发读/写时 SharedPreferences 有很大几率丢失数据,因此不建议在IPC中使用 SharedPreferences 。
2.4.3 使用Messenger
- Messenger可以在不同进程间传递Message对象。是一种轻量级的IPC方案,底层实现是AIDL。它对AIDL进行了封装,使得我们可以更简便的进行IPC。
- 具体使用时,分为服务端和客户端:
- 服务端:创建一个Service来处理客户端请求,同时创建一个Handler并通过它来创建一个
Messenger,然后在Service的onBind中返回Messenger对象底层的Binder即可。
private final Messenger mMessenger = new Messenger (new xxxHandler());
- 客户端:绑定服务端的Sevice,利用服务端返回的IBinder对象来创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,消息类型是 Message 。如果需要服务端响应,则需要创建一个Handler并通过它来创建一个Messenger( 和服务端一样) ,并通过 Message 的 replyTo 参数传递给服务端。服务端通过Message的 replyTo 参数就可以回应客户端了。
- 总而言之,就是客户端和服务端 拿到对方的Messenger来发送 Message 。只不过客户端通过bindService 而服务端通过 message.replyTo 来获得对方的Messenger。
- Messenger在 Hanlder 以串行的方式处理队列中的消息,所以不存在并发执行,因此我们不用考虑线程同步的问题。
- 缺点:
- Messenger是串行处理消息的,不适合大量并发请求
- Messenger用来传递消息,无法跨进程调用服务端的方法。
2.4.4 使用AIDL
AIDL 意思即 Android Interface Definition Language,翻译过来就是Android接口定义语言,是用于定义服务端和客户端通信接口的一种描述语言,可以拿来生成用于 IPC 的代码。
使用步骤如下:
- 服务端需要创建Service来监听客户端请求,然后创建一个AIDL文件,将暴露给客户端的接口在AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
- 客户端首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
AIDL支持的数据类型:
- 基本数据类型、String、CharSequence
- List:只支持ArrayList,里面的每个元素必须被AIDL支持
- Map:只支持HashMap,里面的每个元素必须被AIDL支持
- Parcelable
- 所有的AIDL接口本身也可以在AIDL文件中使用
自定义的Parcelable对象和AIDL对象,不管它们与当前的AIDL文件是否位于同一个包,都必须显式import进来。
如果AIDL文件中使用了自定义的Parcelable对象,就必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
package com.ryg.chapter_2.aidl;
parcelable Book;
AIDL接口中的参数除了基本类型以外都必须表明方向in/out。
AIDL接口文件中只支持方法,不支持声明静态常量。
建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。
- AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,管理数据的集合直接采用 CopyOnWriteArrayList 来进行自动线程同步,只要这个集合类实现了List接口就可以使用,不一定非要是ArrayList,但是Binder中还是会形成一个ArrayList。类似的还有 ConcurrentHashMap 。
- 因为客户端的listener和服务端的listener不是同一个对象,所以 RecmoteCallbackList 是系统专门提供用于删除跨进程listener的接口,支持管理任意的AIDL接口。它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是 IBinder 类型,value是 Callback 类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。
- 客户端调用远程方法的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,如果很耗时,就会让客户端一直等待甚至ANR,所以客户端不能在主线程中去调用服务端的方法,要开启一个子线程。同理,服务端调用客户端的listener的方法时,调用的方法运行在客户端的Binder线程池中,所以不能在这里做UI操作,除非切换到UI线程。
- Binder可能会意外死亡,若意外停止,则重连服务。
- 方法一:给Binder设置DeathRecipient监听,Binder死亡,收到Binderdied回调,在binderDied中我们可以重连远程服务。
- 方法二:onServiceDisconnected中进行重连操作。
- 权限验证
默认情况下,我们的远程服务任何人都可以连接,我们必须加入权限验证功能,权限验证失败则无法调用服务中的方法。通常有两种验证方法:
- 在onBind中验证,验证不通过返回null。
验证方式比如permission验证,在AndroidManifest声明Android自定义权限和使用权限。这种方法也适用于Messager。
- 在服务端的onTransact中验证,验证不通过返回false。可以permission验证,还可以验证包名。
2.4.5 使用ContentProvider
- ContentProvider是四大组件之一,天生就是用来进程间通信。和Messenger一样,其底层实现是用Binder。
- 系统预置了许多ContentProvider,比如通讯录、日程表等。要RPC访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。
- 创建自定义的ContentProvider,只需继承ContentProvider类并实现 onCreate 、 query 、 update 、 insert 、 getType 六个抽象方法即可。getType用来返回一个Uri请求所对应的MIME类型,剩下四个方法对应于CRUD操作。这六个方法都运行在ContentProvider进程中,除了 onCreate 由系统回调并运行在主线程里,其他五个方法都由外界调用并运行在Binder线程池中。
- ContentProvider是通过Uri来区分外界要访问的数据集合,例如外界访问ContentProvider中的表,我们需要为它们定义单独的Uri和Uri_Code。根据Uri_Code,我们就知道要访问哪个表了。
- query、update、insert、delete四大方法存在多线程并发访问,因此方法内部要做好线程同步。
- 若采用SQLite并且只有一个SQLiteDatabase,SQLiteDatabase内部已经做了同步处理。若是多个SQLiteDatabase或是采用List作为底层数据集,就必须做线程同步。
2.4.6 使用Socket
- Socket也称为“套接字”,分为流式套接字和用户数据报套接字两种,分别对应于TCP和UDP协议。Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。。
- 在远程Service建立一个TCP服务,然后在主界面中连接TCP服务。服务端Service监听本地端口,客户端连接指定的端口,建立连接成功后,拿到 Socket 对象就可以向服务端发送消息或者接受服务端发送的消息。
- 除了采用TCP套接字,也可以用UDP套接字。实际上socket不仅能实现进程间的通信,还可以实现设备间的通信(只要设备之间的IP地址互相可见)。
- 但是它的传输效率也是非常的低
2.5 Binder连接池
前面提到AIDL的流程是:首先创建一个service和AIDL接口,接着创建一个类继承自AIDL接口中的Stub类并实现Stub中的抽象方法,客户端在Service的onBind方法中拿到这个类的对象,然后绑定这个service,建立连接后就可以通过这个Stub对象进行RPC。
那么如果项目庞大,有多个业务模块都需要使用AIDL进行IPC,随着AIDL数量的增加,我们不能无限制地增加Service,我们需要把所有AIDL放在同一个Service中去管理。
服务端只有一个Service,把所有AIDL放在一个Service中,不同业务模块之间不能有耦合
服务端提供一个 queryBinder 接口,这个接口能够根据业务模块的特征来返回响应的Binder对象给客户端
不同的业务模块拿到所需的Binder对象就可以进行RPC了
2.6 选用合适的IPC方式