案例解析
需求场景:client和server建立远程连接后,client主动调用server方法增加消息事件,server定时推送给client消息事件。
服务端server
创建AIDL文件,并在其中定义与客户端通信的内容(即方法),并编译(Make Project)。
TestMathAidl.aidl类
package com.jinzifu.myserver;
import com.jinzifu.myserver.MediumEvent;
import com.jinzifu.myserver.INewEventArrivedListener;
interface TestMathAidl {
List<MediumEvent> getMediumList();
boolean addEvent(in MediumEvent mediumEvent);
void registerListener(INewEventArrivedListener listener);
void unRegisterListener(INewEventArrivedListener listener);
}
INewEventArrivedListener.aidl类
package com.jinzifu.myserver;
import com.jinzifu.myserver.MediumEvent;
interface INewEventArrivedListener {
void onNewEventArrieved(in MediumEvent mediumEvent);
}
MediumEvent.aidl类
package com.jinzifu.myserver;
import com.jinzifu.myserver.MediumEvent;
parcelable MediumEvent;
注意:MediumEvent.java是属于Java类,应放在Java包下,其他aidl类应放在aidl包下。
AndroidManifest.xml类
//自定义服务权限
<permission android:name="com.jinzifu.myserver.permission.ACCESS_EVENT_SERVICE"
android:protectionLevel="normal"/>
<service
android:name=".TestServerService">
<intent-filter>
<action android:name="com.jinzifu.myserver.TestMathAidl" />
</intent-filter>
</service>
注意:宜将AIDL相关的文件都放到同一个包中,便于从server端复制到client端。
因为客户端需要反序列化服务端中和AIDL接口相关的所有文件,如果类的完整路径不一致,就无法反序列化成功。
TestServerService:通过IBinder的实现类实现远程业务方法。
public class TestServerService extends Service {
//① CopyOnWriteArrayList 队列支持并发读写,且最终以ArrayList的类型数据通信,同理ConcurrentHashMap。
CopyOnWriteArrayList<MediumEvent> mediumEvents =
new CopyOnWriteArrayList<>();
//② RemoteCallbackList 是系统专门提供的用于管理跨进程接口回调实例的,且在客户端进程终止后,能自动移除客户端注册的Listener,内部还实现了线程同步。
RemoteCallbackList<INewEventArrivedListener> INewEventArrivedListeners
= new RemoteCallbackList<>();
@Override
public void onCreate() {
super.onCreate();
//③ 模拟远程服务定时推送消息给客户端,会遍历RemoteCallbackList的listener接口,参考观察者模式。
new Thread(new Runnable() {
@Override
public void run() {
try {
while (true) {
Thread.sleep(5000);
MediumEvent mediumEvent = new MediumEvent("" + System.currentTimeMillis(), 10);
//beginBroadcast与finishBroadcast要配对使用
final int size = INewEventArrivedListeners.beginBroadcast();
for (int i = 0; i < size; i++) {
INewEventArrivedListener iNewEventArrivedListener = INewEventArrivedListeners.getBroadcastItem(i);
iNewEventArrivedListener.onNewEventArrieved(mediumEvent);
}
INewEventArrivedListeners.finishBroadcast();
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}).start();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new TestMathAidl.Stub() {
@Override
public List<MediumEvent> getMediumList() throws RemoteException {
return mediumEvents;
}
//④ 重写onTransact方法,对多进程/多应用的客户端连接申请权限验证
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
//⑤ server端自定义权限,client端注册权限后才能绑定成功
int result = checkCallingPermission(
"com.jinzifu.myserver.permission.ACCESS_EVENT_SERVICE");
if (result == PackageManager.PERMISSION_DENIED) {
Log.e("MainActivity", "远程服务权限校验=" + result);
return false;
}
return super.onTransact(code, data, reply, flags);
}
@Override
public boolean addEvent(MediumEvent mediumEvent) throws RemoteException {
return mediumEvents.add(mediumEvent);
}
@Override
public void registerListener(INewEventArrivedListener listener)
throws RemoteException {
INewEventArrivedListeners.register(listener);
}
@Override
public void unRegisterListener(INewEventArrivedListener listener)
throws RemoteException {
INewEventArrivedListeners.unregister(listener);
}
};
}
}
核心知识点:如上源码中注释位置
- CopyOnWriteArrayList 队列支持并发读写,且最终以ArrayList的类型数据通信,同理ConcurrentHashMap。
- RemoteCallbackList 是系统专门提供的用于管理跨进程接口回调实例的,且在客户端进程终止后,能自动移除客户端注册的Listener,内部还实现了线程同步。
- 模拟远程服务定时推送消息给客户端,会遍历RemoteCallbackList的listener接口,参考观察者模式。
- 重写onTransact方法,对多进程/多应用的客户端连接申请权限验证。
- server端自定义权限,client端注册权限后才能绑定成功。
客户端client
MainActivity类
...
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
//① onServiceConnected和onServiceDisconnected回调是在client的UI线程中的。
testMathAidl = TestMathAidl.Stub.asInterface(iBinder);
if (testMathAidl == null || !iBinder.isBinderAlive()) return;
Log.e("MainActivity", "远程服务连接成功");
//② 调用远程服务service的方法会阻塞client的UI线程,此须在子线程中调用。
new Thread() {
@Override
public void run() {
super.run();
try {
testMathAidl.registerListener(mINewEventArrivedListener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}.start();
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
//③ 正常解绑服务不会回调此方法,远程服务service失联后会回调此方法,可在此重连服务
Log.e("MainActivity", "远程服务失联");
}
};
INewEventArrivedListener mINewEventArrivedListener = new INewEventArrivedListener.Stub() {
@Override
public void onNewEventArrieved(MediumEvent mediumEvent) throws RemoteException {
//④ 接收远程服务端的回调,此处是在binder线程池中,更新UI需异步消息机制
Log.e("MainActivity", "新事件名字=" + mediumEvent.name);
}
};
@Override
protected void onDestroy() {
super.onDestroy();
unbindService();
}
private void unbindService() {
if (testMathAidl != null) {
try {
//⑤ 断开服务时,需要解注册listener并解绑service。
testMathAidl.unRegisterListener(mINewEventArrivedListener);
} catch (RemoteException e) {
e.printStackTrace();
}
unbindService(serviceConnection);
testMathAidl = null;
}
}
...
核心知识点:如上源码中注释位置
- onServiceConnected和onServiceDisconnected回调是在client的UI线程中的。
- 调用远程服务service的方法会阻塞client的UI线程,此须在子线程中调用。
- 正常解绑服务不会回调此方法,远程服务service失联后会回调此方法,可在此重连服务。
- 接收远程服务端的回调,此处是在binder线程池中,更新UI需异步消息机制。
- 断开服务时,需要解注册listener并解绑service。
Android Manifest.xml类
<uses-permission android:name="com.jinzifu.myserver.permission.ACCESS_EVENT_SERVICE"/>
LOG日志:
com.jinzifu.myclient E/MainActivity: 远程服务连接成功
com.jinzifu.myclient E/MainActivity: 事件个数=1
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330942898
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330947903
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330952911
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330957915
com.jinzifu.myclient E/MainActivity: 新事件名字=1567330962919
基于AIDL的IPC问题汇总
- 重复实例问题
Android为每个应用分配一个独立的虚拟机,或者为每个进程分配独立的虚拟机,不同的虚拟机在内存分配上有不同的地址,导致不同进程中,访问同一个类会有多个对象实例。
- 静态成员变量,单例模式失效;
- 依附于进程的线程同步机制失效;
- SharedPreferences持久化失效(同理读写文件并发);
- Application实例重复创建;
在进程之间通过共享内存的方式通信,都会失败的。
- 并发问题
AIDL方法是在服务端的Binder线程池中执行的,当多个客户端同时连接服务端时,会有并发读写的问题。CopyOnWriteArrayList 队列支持并发读写,且最终以ArrayList的类型数据通信,同理ConcurrentHashMap。
- 接口回调问题
客户端注册Listener对象实参传到服务端时,服务端收到的是一个新的Listener对象,导致在解注册时无法成功移除客户端Listener。
RemoteCallbackList是系统专门提供的用于管理跨进程接口回调实例的,且在客户端进程终止后,能自动移除客户端注册的Listener,内部还实现了线程同步。
对象实例是不能通过跨进程直接传输的,对象的跨进程本质是反序列的过程,传递的是值不是引用。
- 权限验证问题
- onTransact:onTransact方法返回值来做权限验证,避免任意进程调用远程服务。【推荐】
- 自定义Permission:在服务端的onBind()中校验自定义的permission,如果通过了校验,正常返回Binder对象,否则返回null。
常用的权限验证都要在server的清单文件中注册自定义权限。
- 线程挂起引起的ANR问题
客户端需在工作线程中调用服务端方法。客户端listener的回调方法是在客户端的binder线程池中,若更新UI需hander异步通信机制。
- onServiceConnnected和onServiceDisconnected方法运行在客户端UI线程中。
- 客户端调用远程服务方法时,客户端线程会被挂起,进入阻塞状态。
- 通过binder 调用对方的方法,对方的方法都是运行在自身的binder线程池中,耗时操作无需另开线程。
//判断是否是UI线程
Log.d("MainActivity", "" + (Looper.myLooper() == getMainLooper()));//true
- Binder死亡问题
Binder运行在服务端进程中,如果服务端异常终止,客户端与服务端的连接就会中断,即Binder死亡。
两种监听binder死亡的方法:
- Binder的两个方法linkToDeath和unlinkToDeath,通过linkToDeath为Binder设置死亡代理,当连接中断时客户端就会收到通知。
- 远程service异常关闭时才会调用onServiceDisconnected()方法,在此可执行服务的重连机制。【推荐】
linkToDeath的binderDied方法是在Binder线程池中被调用的,onServiceDisconnected()在客户端的UI线程中回调的。
linkToDeath会在service异常关闭时,移除连接信息,并回调onServiceDisconnected方法。