作者:夏至 欢饮转载,也请保留这段申明
http://blog.csdn.net/u011418943/article/details/64121628
最近做到一个项目,需要在两个进程之间通信,考虑到频繁通信和数据安全的问题,这里采用AIDL来为两个进程进行通信。
这里讲解的是如何操作,如何想要了解原理什么的,可以参照AIDL的官方中文文档:
https://developer.android.com/guide/components/aidl.html,
这里采用Android studio 开发,新建一个工程,和一个module,一个作为服务端,一个作为客户端。
一、AIDL简单通信
其实 AIDL 的通信,说白就是服务器提供了接口,让客户端去绑定这个服务,从而让两个进程之间相互通信
1.1 服务端
在Android studio中,新建一个AIDL 的文件夹,直接 new :
然后新建一个AIDL文件:
其中 IReremoSerice.aidl 为给其他进程绑定的服务,编写一个简单的方法,比如两数相加
interface IRemoteService {
int add(int num1,int num2);
}
此时先编译一下module,不然找不到 IRemoteService 的; 这样,其实我们的 AIDL 已经弄好了。
但是,其他用户怎么知道这个 AIDL 服务呢?
这里,我们可以创建一个 Service ,并把 AIDL 的binder 赋值给 Service ,如下:
public class RemoteService extends Service {
private static final String TAG = "RemoteService";
public RemoteService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
return mBinder;
}
//AIDL 的服务,
IRemoteService.Stub mBinder = new IRemoteService.Stub() {
@Override
public int add(int num1, int num2) throws RemoteException {
/**
* 这里为具体的实现方法,比如直接返回两数之和
*/
Log.d(TAG, "zsr add: 接收到客户端传递的两个数字: "+num1+" "+num2);
return (num1 + num2);
}
};
}
好了,服务端的代码就编写好了。
1.2 客户端
我们先看官方文档的说法:
即,客户端如果想要获取访问这个服务的权限,则必须在同级目录下,和服务端一样的接口 aidl文件。
那新建一个 aidl 文件夹,并新建包名与服务端一直 com.example.ipcdemo,然后把服务端的代码复制过来:
然后,我们在 Client 的客户端中,去绑定服务端的 RemoteService 即可,如下:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private IRemoteService mBinder;
private RemoteService mRemoteService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent();
//判定 AIDL 服务
intent.setClassName("com.example.ipcdemo","com.example.ipcdemo.service.RemoteService");
mRemoteService = new RemoteService();
bindService(intent,mRemoteService, Service.BIND_AUTO_CREATE);
}
class RemoteService implements ServiceConnection{
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBinder = IRemoteService.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
}
public void testAidl(View view) {
try {
if (mBinder != null) {
int num = mBinder.add(10, 10);
Log.d(TAG, "zsr testAidl: 服务端返回的两数之和为: "+num);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mRemoteService != null) {
unbindService(mRemoteService);
}
}
代码很简单,就是通过包名和类型绑定服务,然后在点击事件中,拿到两数之和,最后别忘记解绑服务;
先启动服务端,然后启动客户端,打印如下:
二、自定义数据,复杂数据传输
在AIDL的文档中,支持的数据类型如下:
- 基本数据类型(int,long,char,boolean 等)
- String 和 CharSequence
- List,只支持 ArrayList,里面的元素必须被 AIDL 支持
- Map,只支持 HashMap,里面的元素必须被 AIDL 支持,包括 key 和 value
- Parcelable,实现了 Parcelable接口的对象
- AIDL ,所有的 AIDL 接口本身也可以在 AIDL 文件中使用
这里,我们说一下 自定义的 Bean 数据。
很多时候,我们需要把数据封装到一个类中,如一个 Person类,然后里面有 name, age, sex等等,这种的话,我们就没法传递过去了,但是我们让这个类 继承 Parcleable ,把它转换成基本类型,然后传递过去;
举个栗子,比如,我们要把一个 TaskInfo 这个类的数据,在服务端和客户端之间相互传递,如果一个对象要在进程之间传递,这里可以让它继承 Parcelable 接口,方便对象序列化。
新建一个 TaskInfo类,注意需要在 com.example.ipcdemo (需要和你的AIDL同个包名)下,不然会提示找不到:
public class TaskInfo implements Parcelable {
public int id;
public String url;
public int progress;
public TaskInfo() {
}
protected TaskInfo(Parcel in) {
id = in.readInt();
url = in.readString();
progress = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(url);
dest.writeInt(progress);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<TaskInfo> CREATOR = new Creator<TaskInfo>() {
@Override
public TaskInfo createFromParcel(Parcel in) {
return new TaskInfo(in);
}
@Override
public TaskInfo[] newArray(int size) {
return new TaskInfo[size];
}
};
@Override
public String toString() {
return "TaskInfo{" +
"id=" + id +
", url='" + url + '\'' +
", progress=" + progress +
'}';
}
}
2.1、编写 bean 对应的 aidl 文件
添加一个 TaskInfo的aidl 文件,用 parcelable 修饰,这点官方文档也说得比较清楚了。
所以,当我们使用了 Parcelable 对象,就必须新建一个和它同名的 AIDL 文件,比如 TaskInfo.aidl,用parcelable修饰:
这个时候,我们对外公布的接口也要修改,比如我们改成添加一个任务:
// IRemoteService.aidl
package com.example.ipcdemo;
//记得导入 TaskInfo.aidl 的包
import com.example.ipcdemo.TaskInfo;
interface IRemoteService {
//两数之和
int add(int num1,int num2);
//添加一个任务
TaskInfo addTask(in TaskInfo info);
}
至于为什么前面有个 “in” 呢?原因在于我们需要对输入的参数添加它的属性,"in"表示参数由输入,’’'out"则为输出,“inout” 表示输入输出型参数。除了基本类型,其他类型都需要表上方向。
接着 build 一下,就会提示我们重写 addTask 方法了:
//AIDL 的服务,
IRemoteService.Stub mBinder = new IRemoteService.Stub() {
@Override
public int add(int num1, int num2) throws RemoteException {
/**
* 这里为具体的实现方法,比如直接返回两数之和
*/
Log.d(TAG, "zsr add: 接收到客户端传递的两个数字: "+num1+" "+num2);
return (num1 + num2);
}
@Override
public TaskInfo addTask(TaskInfo info) throws RemoteException {
Log.d("zsr", "收到客户端的信息: "+info);
info.id = 0;
info.progress = 50;
return info;
}
};
我们把收到的数据修改一下再返回去。
2.2、客户端的编写
把 aidl 的文件都拷贝过去,初一 IRemoteService 也需要:
然后,需要注意的是,需要新建一个服务端一模一样的包名,且把 TaskInfo 拷贝过去,如果 TaskInfo 有变动,服务端和客户端要同步:
注意,一定要是同级目录。这样就可以,当然,我们在onclick那里也修改一下:
public void test(View view){
try {
if (mBinder != null){
TaskInfo taskInfo = new TaskInfo(0,"www.baidu.com",0);
TaskInfo info = mBinder.addTask(taskInfo);
Log.d(TAG, "收到服务端返回的数据: "+info);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
收到的信息如下:
这样就可以了,就收到了自定义的数据了。
三、进程间的回调
进件的通信,比较麻烦的是,无法知道服务端那边的情况,或者实时去获取通信。
比如我的一个下载任务,我需要实时获取的它下载进度?
幸运的是,官方提供了 RemoteCallbackList 这个类,专门用于接口列表的繁琐工作,特别是用于 service 于 client 之间的回调,具体来说:
- 跟踪一组已注册的IInterface回调,注意他们的底层需要时 binder 通信的,比如 aidl 的服务
- 通过 Binder.DeathRecipien 绑定已注册的接口,当进程清除时,也能清除列表
- 通过锁定底层接口从而处理多线程,以及一种线程安全的方式来迭代列表的快照而不持有其锁。
下面来实战一下:
3.1、实现服务端接口
首先,为了知道下载任务的当前进度,我们新建一个共其他进程回调的 AIDL 接口,如下:
import com.example.ipcdemo.TaskInfo;
interface IDownCallback {
void download(in TaskInfo taskInfo);
}
可以看到,添加了一个 download方法,里面就是 TaskInfo 的信息,后面用来反馈任务进度。
接着,我们应该把这个接口,添加到 IRemoteService ,以方便注册和取消注册:
然后,先build 一下,接着,就要使用 RemoteCallbackList 了,它的泛型,我们传入 IDownCallback,在RemoteService 中重写 registerCallback 和 unRegisterCallback 分别注册和取消:
当然,别忘记在 onDestory 那里,取消 ReniteCallbackList :
为了方便模拟任务下载,这里当添加一个任务时,就启动 Handler ,1s 更新一下下载进度:
@Override
public TaskInfo addTask(TaskInfo info) throws RemoteException {
Log.d("zsr", "收到客户端的信息: "+info);
info.id = 0;
info.progress = 50;
mHandler.sendEmptyMessage(1);
mTaskInfo = info;
return info;
}
可以看到,当添加了任务,就启动 Handler,且把当前 taskinfo 赋值给 mTaskInfo,Handler 的逻辑如下:
/**
* 模仿下载进度
*/
private Handler mHandler = new Handler(Looper.getMainLooper()){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
if (msg.what == 1){
int i = mRemoteCallbackList.beginBroadcast();
while (i > 0){
i --;
mTaskInfo.progress +=10;
if (mTaskInfo.progress >=100){
mTaskInfo.progress = 100;
}else{
//1s 发一次
mHandler.sendEmptyMessageDelayed(1,1000);
}
try {
//把数据回调在接口上,download 为 IDownCallback 的方法
mRemoteCallbackList.getBroadcastItem(i).download(mTaskInfo);
} catch (RemoteException e) {
e.printStackTrace();
}
}
//beginBroadcact 和 finishBroadcast 是必须一起使用的
mRemoteCallbackList.finishBroadcast();
}
}
};
beginBroadcact 和 finishBroadcast 是必须一起使用的;而且速度更新不能过快,执行速度过快时,会出现
beginBroadcast() called while already in a broadcast
beginBroadcast() called while already in a broadcast
所以这里用1s更新一次,这样主线程不会太吃力。
3.2、实现客户端逻辑
首先把服务端的IRemoteService 和 IDownCallback 文件拷贝过来,build 一下。
接着,我们需要在 绑定服务端的 service 的时候,把下载监听接口也注册一下,当退出这个任务的时候,取消监听。
所以,先实现 IDownload 的接口实例:
IDownCallback.Stub DownCallback = new IDownCallback.Stub() {
@Override
public void download(TaskInfo taskInfo) throws RemoteException {
Log.d(TAG, "zsr download: "+taskInfo.progress);
}
};
很简单,就打印数据,接着在绑定服务时注册接口,取消绑定时取消接口:
点击事件哪里不用变,直接点击即可:
public void testAidl(View view) {
try {
if (mBinder != null) {
TaskInfo info = new TaskInfo();
info.url = "www.google.com";
TaskInfo taskInfo = mBinder.addTask(info);
Log.d(TAG, "zsr testAidl: 获取到服务端数据: "+taskInfo);
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
log 打印如下:
四. Binder 意外死亡
上面中,我们看到程序已经正常运行了,但是如果服务端因为某种原因意外终止了,这个我们的服务端的 Binder 连接断裂 (称之为 Binder 死亡),会导致我们连接失败。为了解决这个问题,Binder 提供了两个配对方法,linkToDeath 和 unlinkToDeath ,通过linkToDeath 给 Binder 设置一个死亡代理,当Binder 死亡时,该方法就会被调用。如下:
IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
Log.d(TAG, "zsr binderDied: ");
if (mBinder != null) {
mBinder.asBinder().unlinkToDeath(mDeathRecipient,0);
mBinder = null;
//todo 执行重新绑定service 的逻辑
}
}
};
在绑定的时候,添加一个死亡代理:
mBinder = IRemoteService.Stub.asInterface(service);
try {
mBinder.asBinder().linkToDeath(mDeathRecipient,0);
} catch (RemoteException e) {
e.printStackTrace();
}
接着,我们把服务端的进程去掉,就会发现这个死亡回调了:
记得在 onDestory 的时候,解绑死亡监听:
@Override
protected void onDestroy() {
super.onDestroy();
if (mBinder != null) {
mBinder.asBinder().unlinkToDeath(mDeathRecipient,0);
}
}
五. Binder 连接池
从上面看,当项目变得越来越大,如果有多个业务需要用到 AIDL 来进行进程间通信时,那该怎么办呢?你可能说,该建多少个就建多少个呗;那假如有100个业务呢?难道建 100 个吗?
这肯定是不行,Service 身为四大组件之一,本身就是消耗资源的,而且Service过大多,对apk 和用户来说都不太友好。所以,这里,我们应该使用一个 Service 来管理所有的 AIDL ,通过设计一个 queryBinder 方法,来实现不同 Binder 的切换。
从上面的分析看来,Binder 连接池的主要作用就是将每个业务模块的 Binder 请求同意转发到远程 Service 去执行,从而避免了重复创建 Service 的过程。如下图
接着,我们一起来实现吧,假设现在有个业务,一个打印字符串,一个是两数相加,所以,分别创建好 AIDL:
interface ISay {
void speak(String msg);
}
interface INums {
int add(int num1,int num2);
}
5.1 创建连接池
接着 ,应该创建好连接池的 AIDL,通过不同的 code id,来识别不同的业务,所以,创建一个 AIDL文件,返回 IBinder:
interface IBinderPool {
//通过 code 查找 IBinder
IBinder queryBinder(int binderCode);
}
接着,创建连接池的实现类,这个类中,应该包含服务端的代码和客户端的代码:
public class BinderPool {
private static final String TAG = "BinderPool";
public static final int BINDER_NUM = 1;
public static final int BINDER_SAY = 2;
//=======================================================
// 服务端应该调用的方法
//=======================================================
/**
* Binder 服务,通过 binderCode 来查询当前的 Binder
*/
public static class BinderPoolImpl extends IBinderPool.Stub{
@Override
public IBinder queryBinder(int binderCode) throws RemoteException {
IBinder binder = null;
//todo 这里不用每次都new,可以用 map 来优化
switch (binderCode){
case BINDER_NUM:
binder = new NumImpl();
break;
case BINDER_SAY:
binder = new SayImpl();
break;
default:break;
}
return binder;
}
}
//=======================================================
// 不同的业务
//=======================================================
private static class SayImpl extends ISay.Stub{
@Override
public void speak(String msg) throws RemoteException {
Log.d(TAG, "zsr speak: "+msg);
}
}
private static class NumImpl extends INums.Stub{
@Override
public int add(int num1, int num2) throws RemoteException {
return num1+num2;
}
}
}
可以看到,我们实现了不同业务的 AIDL 方法,并通过 BinderPoolImpl 的 queryBinder 方法,来筛选不同的业务 AIDL 。这样,我们只需要在 Service 这样写就可以了:
public class PoolService extends Service {
private IBinder mBinder = new BinderPool.BinderPoolImpl();
public PoolService() {
}
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
return mBinder;
}
}
这样,服务端的代码就写好了。
5.2 客户端
首先,从服务端复制 AIDL 文件到客户端中,把 BinderPool 也复制过来,接下来,客户端的绑定也是要通过 BinderPool 来实现的,所以,它应该添加以下代码:
//=======================================================
// 客户端应该调用的方法
//=======================================================
private Context mContext;
private static BinderPool mBinderPool;
private RemoteService mRemoteService;
private IBinderPool mBinder;
private BinderPool(Context context){
mContext = context;
}
public static BinderPool getInstance(Context context){
if (mBinderPool == null){
synchronized (BinderPool.class){
if (mBinderPool == null){
mBinderPool = new BinderPool(context);
}
}
}
return mBinderPool;
}
/**
* 连接远程服务,考虑在多线程的问题
*/
public synchronized void connectService(){
/**
* 绑定服务
*/
Intent intent = new Intent();
//绑定 服务
intent.setClassName("com.example.ipcdemo","com.example.ipcdemo.service.PoolService");
mRemoteService = new RemoteService();
mContext.bindService(intent,mRemoteService, Service.BIND_AUTO_CREATE);
}
class RemoteService implements ServiceConnection {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBinder = IBinderPool.Stub.asInterface(service);
//添加 Binder 意外死亡的监听
try {
mBinder.asBinder().linkToDeath(mDeathRecipient,0);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mBinder = null;
}
}
/**
* 通过 binderCode 查询 Binder
* @param binderCode
* @return
*/
public synchronized IBinder queryBinder(int binderCode){
IBinder binder = null;
if (mBinder != null) {
try {
binder = mBinder.queryBinder(binderCode);
} catch (RemoteException e) {
e.printStackTrace();
}
}
return binder;
}
/**
* 添加一个Binder意外死亡的监听
*/
IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
mBinder.asBinder().unlinkToDeath(mDeathRecipient,0);
mBinder = null;
connectService();
}
};
/**
* 释放资源
*/
public void onRelease(){
if (mRemoteService != null) {
mContext.unbindService(mRemoteService);
}
}
首先,它的说明步骤如下:
- 创建一个单例,传入 context,记得使用 context.getApplication()
- 通过 context 绑定远程服务,并注册意外断开的接口;并拿到 IBinderPool.Stub 的 mBinder
- 创建 queryBinder(),里面由 mBinder.queryBinder() 拿到服务端的不同业务的 Binder
- 创建 onRelease 方法,解绑服务
可以客户端的 onCreate 方法中,初始化单例,并绑定服务:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pool_test);
mInstance = BinderPool.getInstance(this);
mInstance.connectService();
}
在不同的点击事件中,实现不同的 AIDL 的功能:
public void say(View view) {
IBinder binder = mInstance.queryBinder(BinderPool.BINDER_SAY);
ISay iSay = ISay.Stub.asInterface(binder);
if (iSay != null) {
try {
iSay.speak("我是测试1");
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
public void num(View view) {
IBinder binder = mInstance.queryBinder(BinderPool.BINDER_NUM);
INums nums = INums.Stub.asInterface(binder);
if (nums != null) {
try {
int num = nums.add(2,3);
Log.d(TAG, "zsr num: "+num);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
mInstance.onRelease();
}
这样,我们的 AIDL 就学习完了。
总的来说,AIDL 的原理就是,其中一个进程设置好服务,里面要提供给外部的数据或方法。另外一个进程如果想要访问和通信,就绑定我这个服务,用 binder 的方式去通信即可。