AIDL使用详解及进程回调

作者:夏至 欢饮转载,也请保留这段申明
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);
        }
    }

首先,它的说明步骤如下:

  1. 创建一个单例,传入 context,记得使用 context.getApplication()
  2. 通过 context 绑定远程服务,并注册意外断开的接口;并拿到 IBinderPool.Stub 的 mBinder
  3. 创建 queryBinder(),里面由 mBinder.queryBinder() 拿到服务端的不同业务的 Binder
  4. 创建 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 的方式去通信即可。

  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值