Android开发Android开发经验谈

029 Android多进程-AIDL-使用

2021-01-16  本文已影响0人  凤邪摩羯

1、前言

IPC(interprocess communication)是指进程间通信,也就是在两个进程间进行数据交互。不同的操作系统都有他们自己的一套IPC机制。例如在Linux操作系统中可以通过管道、信号量、消息队列、内存共享、套接字等进行进程间通信。

那么在Android系统中我们可以通过Binder来进行进程间的通信,当然除了Binder我们还可以使用Socket和文件共享来进行进程间的通信。

前面几节已经学习了文件共享的方式、Socket方式和Binder中的其他几种方式,下面就来介绍BInder的另一种方式-AIDL。

2、AIDL介绍

2.1、AIDL定义

AIDL( Android Interface Definition Language),译为:android接口定义语言,是一种IDL语言,用于生成可以在Android设备上两个进程之间进行 IPC的代码。

如果在一个进程中(例如Activity)要调用另一个进程中(例如Service)对象的操作,就可以使用AIDL生成可序列化的参数。

2.2、AIDL语法规则

既然AIDL是一种语言,那么,他就有直接的语法规则,下面简单介绍一下几点规则。

默认情况下AIDL支持以下数据类型:

注意,对于序列化的数据,我们必须要显示import进来,即使他们在同一个包中。当我们使用自定义的对象时必须实现Parcelable接口,Parcelable为对象序列化接口,效率比实现Serializable接口高。并且新建一个与该类同名的AIDL文件,声明他为Parcelable类型。

我们定义AIDL接口还需要注意以下几点:

2.3、AIDL机制

AIDL IPC机制是面向接口的,像COM或Corba一样,但是更加轻量级。它是使用代理类在客户端和实现端传递数据。

AIDL规定了两个进程之间如何使用Binder来通信,AIDL文件就是接口和数据声明。Android利用我们写好的AIDL文件生成Java文件,真正在打包成apk文件时会把aidl文件生成的java代码打包进去,不会打包AIDL文件,那么很好理解,Android利用我们写好的AIDL文件生成基于Binder进程通信java代码。Binder进程通信是C/S和代理模式实现的,我们在下节课学习一下Binder。

image

3、AIDL使用流程

通过上面的DIDL数据通信流程图,我们知道,AIDL的核心是BInder驱动,所以,我们就很自然的把AIDL的用法分 为是三部分

binder实现、客户端调用、服务端调用

虽然这样划分没有上面毛病,但是,AIDL的实现过程还是稍显麻烦的,初用AIDL的童鞋可能会不知所措,因此,我整理了一个实现Aidl的基本流程,把最关键的Binder实现部分细分前后关联的6步,让人一看就清楚明了,流程如下:

1、定义数据实体类;
2、添加数据实体类的映射 aidl 文件;
3、添加AIDL接口描述文件;
4、编译生成Binder 的 Java 文件;
5、服务端 IUseAidlInterface.Stub;
6、客户端 Stub.asInterface(IBinder);

当然,如果我们只用到AIDL直接支持的数据,可以省掉前两步,但,这个与我们实际的开发并不吻合,因为,我们的开发都是需要自定义数据实体类来描述对象的属性的,这就告诉我们前两步在实际开发中也是必须的。

按照这个流程,我们就能非常顺利的使用AIDL进行进程间的通信了。

4、 AIDL用法详解

下面,我们就通过一个栗子,详细讲解一下流程以及需要注意的地方。

栗子的目的是,通过AIDL实现如下功能:
客户端调用服务端的远程接口向服务端添加音乐数据,并在需要时获取全部的音乐数据。

4.1、定义数据实体类

我们做开发的时候,正常来说都是需要自定义一个描述属性的数据类,里面包含各种对对象属性描述的数据,如,我们之前讲bundler的时候,就定义了一个MusicData的数据模型来描述一个歌曲的属性信息。

而前面已经说过,AIDL使用非支持数据(非基础数据类型和支持的JAVA数据类型)时,都需要数据类现实Parcelable或者Serializable接口,鉴于性能而且是Android的开发,所以,我们实现Parcelable接口。

public class Parameter implements Parcelable {

    private int param;

    public Parameter(int param) {
        this.param = param;
    }

    public int getParam() {
        return param;
    }

    public void setParam(int param) {
        this.param = param;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.param);
    }

    protected Parameter(Parcel in) {
        this.param = in.readInt();
    }

    public static final Creator<Parameter> CREATOR = new Creator<Parameter>() {
        @Override
        public Parameter createFromParcel(Parcel source) {
            return new Parameter(source);
        }

        @Override
        public Parameter[] newArray(int size) {
            return new Parameter[size];
        }
    };

}

4.2、添加数据实体类的映射 aidl 文件

添加好数据模型后,就可以添加对应的映射 aidl 文件了,

Android 目录结构直接提供了一个aidl文件夹,当然,当我们新添加aidl文件的时候才会显现,这个文件夹里可以根据不同的包实现不同的AIDL文件。

目前,无论是Eclipse还是Android Studio对aidl文件的编写都还是很不智能,就像是写txt文本文件,没有智能提示的。而且关键的是里面的package 包名和import导包都要十分小心,不能写错了,否则,会直接报错:无法找到类或者无法识别类。

另外,数据实体类及其映Aidl文件的类名、包名以及文件名要保持一致,否则,也会直接报错:无法找到类或者无法识别类。

所以,为了避免报错,必须要让他们保持一致。最好的方法是

我们这里添加的映射 aidl 文件的内容如下:

// Parameter.aidl
package com.example.process.h_aidl;

// Declare any non-default types here with import statements
//要和声明的实体类在一个包里
parcelable Parameter;

4.3、添加AIDL接口描述文件

经过前面两步,已经添加了AIDL接口描述文件所需要的数据类及其映射 aidl 文件,接下里就可以添加AIDL接口描述文件,定义接口使用数据了。

按照之前说的,为了避免报错,我们还是在包文件夹上,右键选择添加DID文件,自动添加后,就可以根据实际需要,修改AIDL文件了。

另外,再重复一下,要注意以下几点:

这里添加的接口文件,大致如下:

package com.example.process.h_aidl;

import com.example.process.h_aidl.Parameter;

interface IOperationManager {

    void operation(in Parameter parameter1 , in Parameter parameter2);


 }


4.4、编译生成Binder 的 Java 文件

OK,AIDL接口文件添加完毕后,我们重新编译一下项目,顺利编译通过的话,就会为我们生成复杂的Binder Java 文件。
binder 的 java 文件生成在以下目录:

build/generated/aidl_source_output_dir/debug/compileDebugAidl/out/

如果,没有顺利通过,就回去查查看,哪里有问题,问题一般出在类名、文件名和包名不一致。

通过这几步,AIDL相关的基础已经准备好,并且编译生成了对应的binder Java文件,接下来,我们就可以在服务端和客户端使用AIDL进行进程间的通信了。

4.5、服务端

经过前面几步,准备好了使用Binder,接下来,创建你喜欢的Service,并在其中创建上面生成的 Binder 对象实例(IUseAidlInterface.Stub),实现接口定义的方法;

当客户端与服务端绑定时,服务端是通过onBind()将 mIBinder 返回给客户端的,客户端拿到mIBinder就可以通过它远程调用服务端的方法,实现通讯了。

   /**
 * Description: IPC之AIDL方式,服务端
 */
package yb.demo.myProcesses.useAIDL;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import java.util.ArrayList;
import java.util.List;
import yb.demo.myProcesses.useAIDL.dataModel.MusicData;

/**
 * @ClassName: IPCAIDLClientActivity
 * @Description: IPC之AIDL方式,服务端
 * @Author: yufenfen
 * @Date: 2016/4/8 5:41 PM
 */

public class IPCAIDLService extends Service {

    @Override
    public IBinder onBind(Intent intent) {
        return mIBinder;
    }

    private List<MusicData> mData = new ArrayList<MusicData>();

    IBinder mIBinder = new IUseAidlInterface.Stub() {
        @Override
        public void addMusic(MusicData data) throws RemoteException {
            mData.add(data);
        }

        @Override
        public List<MusicData> getMusicList() throws RemoteException {
            return mData;
        }
    };

}

4.6、 客户端

一切准备就绪了,就差客户端调用这一步了,我们新建一个activity作为客户端,实现流程如下:

//6 客户端 (运行在主进程)
public class AIDLActivity extends AppCompatActivity implements View.OnClickListener {

    ActivityHAidlBinding mBinding;

    private IOperationManager iOperationManager;

    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iOperationManager = IOperationManager.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iOperationManager = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityHAidlBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        initView();
        initData();
    }

    private void initView() {
        mBinding.tvJisua.setOnClickListener(this);
    }

    private void initData() {
        bindService();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBinding = null;
        if (serviceConnection != null) {
            unbindService(serviceConnection);
        }
    }

    @Override
    public void onClick(View v) {

        //跨进程调用
        if (iOperationManager != null) {
            try {
                Parameter resultParameter = iOperationManager.operation(new Parameter(2), new Parameter(2));
                ToastUtils.showShort("运算结果: " + resultParameter.getParam());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }


    private void bindService() {
        Intent intent = new Intent();
        intent.setClassName("com.example.process.h_aidl", "com.example.process.h_aidl.AIDLService");
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }


}

OK,到这里,经过以上六步,相信你的demo已经顺利的跑起来了。

4.7、补充

4.7.1、服务端执行耗时操作

在上一节的例子里的运算操作只是将参数进行乘法操作,当然能够很快获得返回值,但如果是要进行耗时操作,那这种方式就不太合适了,所以可以以注册回调函数的方式来获取运算结果。即客户端向服务端注册一个回调函数用于接收运算结果,而不用傻乎乎地一直等待返回值

package com.example.process.h_aidl;

import com.example.process.h_aidl.Parameter;

interface IOnOperationCompletedListener {

    void onOperationCompleted(in Parameter result);

}

package com.example.process.h_aidl;

import com.example.process.h_aidl.Parameter;
import com.example.process.h_aidl.IOnOperationCompletedListener;


//3 定义一个向外暴露运算方法的 AIDL 接口,
interface IOperationManager {

  //定义接口的方法,运算并将运算结果返回给客户端

   void operation(in Parameter parameter1 , in Parameter parameter2);

   void registerListener(in IOnOperationCompletedListener listener);

   void unregisterListener(in IOnOperationCompletedListener listener);
 }

//4 AIDL服务端(运行在process2进程)
public class AIDLService extends Service {


    private static final String TAG = "AIDLService";

    private CopyOnWriteArrayList<IOnOperationCompletedListener> copyOnWriteArrayList;


    //
    private IBinder stub = new IOperationManager.Stub() {

        //在 Service 中进行实际的运算操作,并将运算结果返回
        @Override
        public void operation(Parameter parameter1, Parameter parameter2) throws RemoteException {
            LogUtils.e(TAG, "服务端 operation() 被调用");
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            int param1 = parameter1.getParam();
            int param2 = parameter2.getParam();
            Parameter result = new Parameter(param1 * param2);

            for (IOnOperationCompletedListener listener : copyOnWriteArrayList) {
                listener.onOperationCompleted(result);
            }
        }

        @Override
        public void registerListener(IOnOperationCompletedListener listener) throws RemoteException {
            LogUtils.e(TAG, "registerListener");
            if (!copyOnWriteArrayList.contains(listener)) {
                LogUtils.e(TAG, "注册回调成功");
                copyOnWriteArrayList.add(listener);
            } else {
                LogUtils.e(TAG, "回调之前已注册");
            }
        }

        @Override
        public void unregisterListener(IOnOperationCompletedListener listener) throws RemoteException {
            LogUtils.e(TAG, "unregisterListener");
            if (copyOnWriteArrayList.contains(listener)) {
                copyOnWriteArrayList.remove(listener);
                LogUtils.e(TAG, "解除注册回调成功");
            } else {
                LogUtils.e(TAG, "该回调没有被注册过");
            }
        }
    };

    public AIDLService() {
        copyOnWriteArrayList = new CopyOnWriteArrayList<>();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return stub;
    }

}
//6 客户端 (运行在主进程)
public class AIDLActivity extends AppCompatActivity implements View.OnClickListener {

    ActivityHAidlBinding mBinding;

    private static final String TAG = "AIDLActivity";

    private IOperationManager iOperationManager;

    private IOnOperationCompletedListener completedListener = new IOnOperationCompletedListener.Stub() {
        @Override
        public void onOperationCompleted(Parameter result) throws RemoteException {
            ToastUtils.showShort("运算结果: " + result.getParam());
        }
    };


    private ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            LogUtils.e(TAG, "onServiceConnected");
            iOperationManager = IOperationManager.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            LogUtils.e(TAG, "onServiceDisconnected");

            iOperationManager = null;
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mBinding = ActivityHAidlBinding.inflate(getLayoutInflater());
        setContentView(mBinding.getRoot());

        initView();
        initData();
    }

    private void initView() {
        mBinding.tvJisua.setOnClickListener(this);
        mBinding.tvRegister.setOnClickListener(this);
        mBinding.tvUnregister.setOnClickListener(this);
    }

    private void initData() {
        bindService();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mBinding = null;
        if (serviceConnection != null) {
            unbindService(serviceConnection);
        }
    }

    @Override
    public void onClick(View v) {
        int id = v.getId();
        switch (id) {
            case R.id.tv_register:
                if (iOperationManager != null) {
                    try {
                        iOperationManager.registerListener(completedListener);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case R.id.tv_unregister:
                if (iOperationManager != null) {
                    try {
                        iOperationManager.unregisterListener(completedListener);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                break;
            case R.id.tv_jisua:
                //跨进程调用
                if (iOperationManager != null) {
                    try {
                       iOperationManager.operation(new Parameter(2), new Parameter(2));
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                }
                break;
        }

    }


    private void bindService() {
        Intent intent = new Intent();
        intent.setClassName("com.example.process", "com.example.process.h_aidl.AIDLService");
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }


}

4.7.2 正确使用 AIDL 回调接口

在上面的代码中我提供了一个按钮用于解除回调函数,但当点击按钮时,Logcat 却会打印出如下信息

image.png

该回调没有被注册过?但在注册回调函数和解除回调函数时,使用的都是同个对象啊!其实,这是因为回调函数被序列化了的原因,Binder 会把客户端传过来的对象序列化后转为一个新的对象传给服务端,即使客户端使用的一直是同个对象,但对服务端来说前后两个回调函数其实都是两个完全不相关的对象,对象的跨进程传输本质上都是序列化与反序列化的过程

为了能够无误地注册和解除注册回调函数,系统为开发者提供了 RemoteCallbackList,RemoteCallbackList 是一个泛型类,系统专门提供用于删除跨进程回调函数,支持管理任意的 AIDL 接口,因为所有的 AIDL 接口都继承自 IInterface,而 RemoteCallbackList 对于泛型类型有限制

    public class RemoteCallbackList<E extends IInterface>

RemoteCallbackList 在内部有一个 ArrayMap 用于 保存所有的 AIDL 回调接口

    ArrayMap<IBinder, Callback> mCallbacks  = new ArrayMap<IBinder, Callback>();

其中 Callback 封装了真正的远程回调函数,因为即使回调函数经过序列化和反序列化后会生成不同的对象,但这些对象的底层 Binder 对象是同一个。利用这个特征就可以通过遍历 RemoteCallbackList 的方式删除注册的回调函数了 此外,当客户端进程终止后,RemoteCallbackList 会自动移除客户端所注册的回调接口。而且 RemoteCallbackList 内部自动实现了线程同步的功能,所以我们使用它来注册和解注册时,不需要进行线程同步

以下就来修改代码,改为用 RemoteCallbackList 来存储 AIDL 接口

    //声明
    private RemoteCallbackList<IOnOperationCompletedListener> callbackList;

注册接口和解除注册接口

        @Override
        public void registerListener(IOnOperationCompletedListener listener) throws RemoteException {
            callbackList.register(listener);
            Log.e(TAG, "registerListener 注册回调成功");
        }

        @Override
        public void unregisterListener(IOnOperationCompletedListener listener) throws RemoteException {
            callbackList.unregister(listener);
            Log.e(TAG, "unregisterListener 解除注册回调成功");
        }

遍历回调接口

            //在操作 RemoteCallbackList 前,必须先调用其 beginBroadcast 方法
            //此外,beginBroadcast 必须和 finishBroadcast配套使用
            int count = callbackList.beginBroadcast();
            for (int i = 0; i < count; i++) {
                IOnOperationCompletedListener listener = callbackList.getBroadcastItem(i);
                if (listener != null) {
                    listener.onOperationCompleted(result);
                }
            }
            callbackList.finishBroadcast();

按照上面的代码来修改后,客户端就可以正确地解除所注册的回调函数了

还有一个地方需要强调下,是关于远程方法调用时的线程问题。客户端在调用远程服务的方法时,被调用的方法是运行在服务端的 Binder 线程池中,同时客户端线程会被挂起,这时如果服务端方法执行比较耗时,就会导致客户端线程被堵塞。就如果上一节我为了模拟耗时计算,使线程休眠了五秒,当点击按钮时就可以明显看到按钮有一种被“卡住了”的反馈效果,这就是因为 UI 线程被堵塞了,这可能会导致 ANR。所以如果确定远程方法是耗时的,就要避免在 UI 线程中去调用远程方法。 所以,客户端调用远程方法 operation 的操作可以放到子线程中进行

    new Thread(new Runnable() {
        @Override
        public void run() {
            Parameter parameter1 = new Parameter(param1);
            Parameter parameter2 = new Parameter(param2);
            if (iOperationManager != null) {
                try {
                    iOperationManager.operation(parameter1, parameter2);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                }
            }
    }).start();

此外,客户端的 ServiceConnection对象的 onServiceConnectedonServiceDisconnected都是运行在 UI 线程中,所以也不能用于调用耗时的远程方法。而由于服务端的方法本身就运行在服务端的 Binder 线程池中,所以服务端方法本身就可以用于执行耗时方法,不必再在服务端方法中开线程去执行异步任务

同理,当服务端需要调用客户端的回调接口中的方法时,被调用的方法也运行在客户端的 Binder 线程池中,所以一样不可以在服务端中调用客户端的耗时方法

最后,我们还需要考虑一个问题,那就是安全问题。假设有人反编译了服务端应用的代码,取得了 AIDL 接口,知道了应用的包名以及 Service 路径名后,就可以直接通过 AIDL 直接调用服务端的远程方法了,这当然不是应用开发者所希望面对的,因此服务端就需要对请求连接的客户端进行权限验证了

4.7.3、服务端和客户端是两个APP的情况

将服务端的 AILD 文件以及 Parameter 类复制到客户端,保持文件路径(包名)不变

文件目录如下所示


image.png

五、结语

这一节还是比较费文字的,详细地介绍了 实现使用AIDL在进程间进行通信 的基本流程,结合实例讲解和提出注意点,比如文件名、类名以及文件所在包的路径不统一……,旨在把AIDL的使用描述清楚,相信看到这里,你已经把握了它的用法。

但是我们还是有很多疑问的,比如:
流程中,第四步,编译生成Binder 的 Java 文件,它的内容是怎么样的,他是如何生成的?
还有,这一节讲的AIDL以及之前讲的 Messenger、Bundle 和 Content Provider 都是基于Binder实现的,那么BInder又是什么呢?
……
其实,我们都还是停留在怎么用,而没有深入了解为什么这样用,也就是知其然还要知其所以然。

上一篇下一篇

猜你喜欢

热点阅读