分享

Android Study - 关于AIDL的使用学习

 codingSmart 2021-10-22
code小生,一个专注 Android 领域的技术平台
公众号回复 Android 加入我的安卓技术群

作者:UsherChen
声明:本文已获
UsherChen投稿发表,转发等请联系原作者授权

Android Study - 关于AIDL的使用学习


  • 关于Android中多进程的了解

  • 使用AIDL进行多进程通信

  • 自定义代码模拟AIDL的工作流程

  • 其他说明


关于Android中多进程的了解

进程:一般指一个执行单元,在平台上指一个程序或一个应用;

线程:CPU调度的最小单位,也是一种资源,一个应用可包含多个线程;

Android多进程通信通常指两种现象:

  • 一个应用的组件

    可在清单文件 AndroidManifest.xml 中指定 android:process 属性名称,一个应用内多个线程通信的现象
  • 多个应用对应多个进程间通信的现象

以上两种现象本质上没有区别,本质上一个进程对应了一块内存区域,彼此之间需要通过一些特殊的方式来进行通信,不可直接通信。


使用AIDL进行多进程通信

上文说道,多进程彼此之间不可直接通信,需借助一些特殊的方式,AIDL是Android系统提供的一种多进程通信的方式。

AIDL:Android Interface Deifinition Laguage 安卓接口定义语言,本质就是一个接口文件,但是是用 .aild 后缀结尾的;

使用AIDL进行进程间通信牵扯到几个类:Service、Binder、IBinder、Intent、Parce 这里先不管这几个类是什么东西,下面会说明;

然后还牵扯到两个概念:服务端与客户端;

从业务层面理解以上元素的作用就是,客户端(含有进程)调用服务端(含有进程)中的方法业务,然后客户端会给一些数据到服务端,服务端再返回一些数据给客户端,如果不需要数据的话就不给或者不返回即可。

看起来似乎有一些抽象, 以下图例模拟一下进程间通信的原理:

两个应用(进程)之间的通信是通过Binder作为桥梁来完成的,Binder是Android提供的一种用于进程通信的方式,这里把它简单理解为一个类即可。

这里模拟两个app来演示AIDL的工作原理,服务端的app提供了三个方法:求和、求最大值、求最小值三个方法;客户端的app需要调用服务端里的这三个方法,并且获取到这三个方法的返回值。

服务端app

我们需要创建一个组件来保持服务常驻后台,并提供业务方法。首先需要创建一个AIDL文件,用来定义需要提供的业务方法,这里在AS中右键单击app module,选择创建一个AIDL文命名为ICalculateAIDL.aidl

// ICalculateAIDL.aidl
package com.usherchen.demo.server;

interface ICalculateAIDL {

    // 计算x与y的和
    int add(int x, int y);

    // 对比x与y的最大值
    int max(int x, int y);

    // 对比x与y的最小值
    int min(int x, int y);

}

系统生成的那个方法直接删掉即可,由于AIDL文件本质是一个接口文件,那么在这里定义三个抽象方法用来在其他代码中实现具体业务。需要注意的是,AIDL文件并不会在AS中出现代码提示,所有的代码需要手动敲上去,代码量比较少。

然后点击Build - Make Project,会发现,在app的build - generated - aidl_source_output_dir - debug 这个路径(路径可能会更长)下有一个 ICalculateAIDL.java 文件。有意思的事情来了,原来点了Make Project以后AS自动帮我们实现了ICalculateAIDL.aidl中的接口,可以看到 ICalculateAIDL.java 这个文件里的代码超级复杂,我们先不管这个文件,继续去编写我们其他的代码。

为了使应用置于后台仍旧可以继续工作,这里我们写一个ICalculateService继承Service用于提供服务:

public class ICalculateService extends Service {

    private static final String TAG = "usherchen_server";

    /**
     * 这是一个实现了AIDL接口方法的IBinder实例
     */

    private ICalculateAIDL.Stub mIBinder = new ICalculateAIDL.Stub() {

        /**
         * 实现了x与y求和的逻辑
         */

        @Override
        public int add(int x, int y) throws RemoteException {
            return x + y;
        }

        /**
         * 实现了x与y求最大值的逻辑
         */

        @Override
        public int max(int x, int y) throws RemoteException {
            if (x > y) {
                return x;
            }
            return y;
        }

        /**
         * 实现了x与y求最小值的逻辑
         */

        @Override
        public int min(int x, int y) throws RemoteException {
            if (x < y) {
                return x;
            }
            return y;
        }
    };

    @Override
    public void onCreate() {
        Log.i(TAG, "onCreate: ");
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand: ");
        return super.onStartCommand(intent, flags, startId);
    }

    /**
     * @param intent intent
     * @return 这里返回了上面实现了AIDL内部方法的实例
     */

    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind: ");
        return mIBinder;
    }

    @Override
    public void onRebind(Intent intent) {
        Log.i(TAG, "onRebind: ");
        super.onRebind(intent);
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.i(TAG, "onUnbind: ");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        Log.i(TAG, "onDestroy: ");
        super.onDestroy();
    }
}

代码不是很长,首先这里写了一个ICalculateAIDL.Stub的实例并实现了内部的方法。看这三个方法就知道这里是实现AIDL文件中抽象方法的业务的地方。然后我们点击Stub类,可以发现,这个类的类名如下所示:

public static abstract class Stub extends android.os.Binder implements com.usherchen.demo.server.ICalculateAIDL

这里可以看到Stub这个类继承了Binder,而Binder点进去可以看到:

public class Binder implements IBinder

因此Stub也是IBinder的字类,所以可以在Service的onBind方法中直接返回,onBind这个方法返回了一个IBinder的实例,是在启动这个Service的组件中用到的。这里不再解释怎么使用Service。接下来去清单文件中注册ICalculateService,如下所示:

        <!--这里注册自定义binder的service用于提供服务-->
        <service
            android:name=".service.ICalculateService"
            tools:ignore="ExportedService">

            <intent-filter>
                <action android:name="com.usherchen.demo.aidl.custom" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </service>

这里一目了然,对CustomICalculateService进行,因为我们需要在另一个app(另一个进程)中启动这个服务,因此这里需要采用隐式启动的方式对其进行属性声明。以上编码完成后,直接在设备跑起来,然后将这个app置于后台即可,接下来进行客户端的代码编写。

客户端app

对于客户端而言,我们需要调用服务端里面的方法,如果凭空什么都没有的话是肯定无法调用另一个app里面的方法的。所以我们需要把服务端的AIDL文件拷贝出来一份,即ICalculateAIDL.aidl这个文件,包含其所在路径,一起拷贝到客户端app的 src - main 文件夹下,然后点击Build - Make Project ,等待完成后,同样在客户端app的build - generated - aidl_source_output_dir - debug(路径可能更长)路径下看到有一个 ICalculateAIDL.java 文件,你会发现这个文件的内容与服务端的 ICalculateAIDL.java 文件中的代码一模一样,不用管他,我们继续写其他的代码。

这里我们写一个页面出来,有两个输入框,三个按钮,页面代码与样式如下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas./apk/res/android"
    xmlns:tools="http://schemas./tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">


    <EditText
        android:id="@+id/et_number_x"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:background="@android:drawable/edit_text"
        android:gravity="center"
        android:inputType="number"
        tools:ignore="UnusedAttribute" />


    <EditText
        android:id="@+id/et_number_y"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:background="@android:drawable/edit_text"
        android:gravity="center"
        android:inputType="number"
        tools:ignore="UnusedAttribute" />


    <Button
        android:id="@+id/btn_add"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="add" />


    <Button
        android:id="@+id/btn_max"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="max" />


    <Button
        android:id="@+id/btn_min"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="min" />


</LinearLayout>

然后在Activity里面去编写逻辑:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "usherchen_client";

    private EditText mEtNumberX;
    private EditText mEtNumberY;
    private Button mBtnAdd;
    private Button mBtnMax;
    private Button mBtnMin;

    ////////////////////////////////////////////////////////////////////////////////

    private ICalculateAIDL mICalculateAIDLBinder;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mICalculateAIDLBinder = ICalculateAIDL.Stub.asInterface(service);
            Log.i(TAG, "onServiceConnected: mICalculateAIDLBinder " + mICalculateAIDLBinder.hashCode());
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mICalculateAIDLBinder = null;
            Log.i(TAG, "onServiceDisconnected: mICalculateAIDLBinder null");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        mEtNumberX = findViewById(R.id.et_number_x);
        mEtNumberY = findViewById(R.id.et_number_y);
        mBtnAdd = findViewById(R.id.btn_add);
        mBtnMax = findViewById(R.id.btn_max);
        mBtnMin = findViewById(R.id.btn_min);
        mBtnAdd.setOnClickListener(this);
        mBtnMax.setOnClickListener(this);
        mBtnMin.setOnClickListener(this);
        intentService();
    }

    private void intentService() {
        Intent i = new Intent();
        i.setPackage("com.usherchen.demo.server");
        i.setAction("com.usherchen.demo.aidl.custom");
        bindService(
                i,
                mConnection,
                BIND_AUTO_CREATE
        );
    }

    @Override
    public void onClick(View v) {
        try {
            int id = v.getId();
            int x = Integer.parseInt(mEtNumberX.getText().toString());
            int y = Integer.parseInt(mEtNumberY.getText().toString());
            if (id == mBtnAdd.getId()) {
                Log.i(TAG, "onClick: add" + mICalculateAIDLBinder.add(x, y));
            } else if (id == mBtnMax.getId()) {
                Log.i(TAG, "onClick: max" + mICalculateAIDLBinder.max(x, y));
            } else if (id == mBtnMin.getId()) {
                Log.i(TAG, "onClick: min" + mICalculateAIDLBinder.min(x, y));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

这里的代码也很简单,首先创建一个 ICalculateAIDL 类的实例,然后创建一个ServiceConnection实例去实现内部类的方法,这是使用 bindService 方法去绑定一个服务的前提。当 ServiceConnection 绑定成功的时候,会接收到一个 IBinder 实例,这个实例内部实现了 ICalculateAIDL 的抽象方法,也就是add、max、min这三个方法。

在输入框中输入数字1和2,点击三个按钮可以看到打印的日志结果是3、2、1,说明我们确实是成功的调用了另一个进程中的方法业务,并且获取到了返回值。那么这一过程是怎么实现的,这里需要看一下 ICalculateAIDL.java 这个文件。

总结分析

这个文件的内容很多,我们选择性的来看两个地方,第一个地方是 Stub 字类中的 onTransact 方法:

        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            String descriptor = DESCRIPTOR;
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_add: {
                    data.enforceInterface(descriptor);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.add(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                case TRANSACTION_max: {
                    data.enforceInterface(descriptor);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.max(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                case TRANSACTION_min: {
                    data.enforceInterface(descriptor);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.min(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                default: {
                    return super.onTransact(code, data, reply, flags);
                }
            }
        }

这个方法看名字翻译成中文叫做传输,那么它里面肯定作了一些传输性的工作,我们来解释一下这个方法。首先它有四个入参:

  • code - 用于区分使用哪个方法;

  • data - 客户端传过来的数据;

  • reply - 从服务端返回的数据;

  • flags - 表明是否有返回值,0 - 有,1 - 无;

同时还有一个 DESCRIPTOR 常量,对于这个常量,我个人对其理解是,用于服务端和客户端对接的一个标识,后面我们自定义代码去实现以上功能的时候会说到。

然后我们选择一个 case TRANSACTION_add 去看一下里面的代码:

                    data.enforceInterface(descriptor);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.add(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);

这里一行行来看,第一行:data.enforceInterface(descriptor); 这里的作用即将 DESCRIPTOR 传入,表示一个认证的动作。

然后下面是定义了两个 int 值 _arg0 _arg1传入了 add 方法里,并返回了一个 _result,然后 reply 执行了两行代码,reply.writeNoException(); 表示这里没有异常发生,然后将 _result 写入,返回出去。

所以,当客户端调用 mICalculateAIDLBinder.add(x, y) 这行代码的时候,本质上是服务端的 ICalculateAIDL.java 的 Stub 的 onTransact 方法执行,然后将返回值递给了客户端。

从以上分析可以看出,数据的传输是集中在了 Stub 类里面的 onTransact 方法里,那么我们是不是也可以自己来写一个Binder来实现进程间的通信呢?


自定义代码模拟AIDL的工作流程

AIDL文件实现服务端app与客户端app通信的根本是Binder,那么我们模拟系统帮我们生成的 ICalculateAIDL.java 文件,自己去写代码模拟一下这个过程。

服务端app

自定义一个新的服务命名为CustomICalculateService,编写代码如下:

public class CustomICalculateService extends Service {

    private static final String TAG = "usherchen_server";

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.i(TAG, "onBind: ");
        return mCustomBinder;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
    }

    @Override
    public boolean onUnbind(Intent intent) {
        return super.onUnbind(intent);
    }

    @Override
    public void onRebind(Intent intent) {
        super.onRebind(intent);
    }

    ////////////////////////////////////////////////////////////////////////////////

    private static final String DESCRIPTOR = "com.usherchen.demo.descriptor";

    /**
     * 自定义一个binder实例用于提供服务
     */

    private Binder mCustomBinder = new Binder() {

        @Override
        protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            switch (code) {
                case 1:
                    data.enforceInterface(DESCRIPTOR);
                    int x_1 = data.readInt();
                    int y_1 = data.readInt();
                    int s_1 = x_1 + y_1;
                    reply.writeNoException();
                    reply.writeInt(s_1);
                    return true;
                case 2:
                    data.enforceInterface(DESCRIPTOR);
                    int x_2 = data.readInt();
                    int y_2 = data.readInt();
                    int s_2;
                    if (x_2 > y_2) {
                        s_2 = x_2;
                    } else {
                        s_2 = y_2;
                    }
                    reply.writeNoException();
                    reply.writeInt(s_2);
                    return true;
                case 3:
                    data.enforceInterface(DESCRIPTOR);
                    int x_3 = data.readInt();
                    int y_3 = data.readInt();
                    int s_3;
                    if (x_3 < y_3) {
                        s_3 = x_3;
                    } else {
                        s_3 = y_3;
                    }
                    reply.writeNoException();
                    reply.writeInt(s_3);
                    return true;
                default:
                    return super.onTransact(code, data, reply, flags);
            }
        }
    };

}

这里我没有用AIDL去让系统帮我生成java文件,而是直接写了一个Binder示例实现了onTransact方法,然后我们自己定义好 DESCRIPTOR 常量,code定义1 - add、2 - max、3 - min,然后在清单文件中注册

<!--这里注册自定义binder的service用于提供服务-->
        <service
            android:name=".service.CustomICalculateService"
            tools:ignore="ExportedService">

            <intent-filter>
                <action android:name="com.usherchen.demo.aidl.custom" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </service>

服务端的代码编写完毕。

客户端app

这里页面就沿用之前的页面,然后需要对Activity里面的代码进行重新编写,如下所示:

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "usherchen_client";

    private EditText mEtNumberX;
    private EditText mEtNumberY;
    private Button mBtnAdd;
    private Button mBtnMax;
    private Button mBtnMin;

    ////////////////////////////////////////////////////////////////////////////////

    private IBinder mIBinder;

    private ServiceConnection mCustomBinderConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            mIBinder = service;
        }

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_home);
        mEtNumberX = findViewById(R.id.et_number_x);
        mEtNumberY = findViewById(R.id.et_number_y);
        mBtnAdd = findViewById(R.id.btn_add);
        mBtnMax = findViewById(R.id.btn_max);
        mBtnMin = findViewById(R.id.btn_min);
        mBtnAdd.setOnClickListener(this);
        mBtnMax.setOnClickListener(this);
        mBtnMin.setOnClickListener(this);
        initCustomService();
    }

    private void initCustomService() {
        Intent i = new Intent();
        i.setPackage("com.usherchen.demo.server");
        i.setAction("com.usherchen.demo.aidl.custom");
        bindService(
                i,
                mCustomBinderConnection,
                BIND_AUTO_CREATE
        );
    }

    @Override
    public void onClick(View v) {
        try {
            int id = v.getId();
            int x = Integer.parseInt(mEtNumberX.getText().toString());
            int y = Integer.parseInt(mEtNumberY.getText().toString());
            if (id == mBtnAdd.getId()) {
                add(x, y);
            } else if (id == mBtnMax.getId()) {
                max(x, y);
            } else if (id == mBtnMin.getId()) {
                min(x, y);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void showToast(String s) {
        Toast.makeText(this, s, Toast.LENGTH_SHORT).show();
    }

    private static final String DESCRIPTOR = "com.usherchen.demo.descriptor";

    private void add(int x, int y) {
        if (mIBinder == null) {
            return;
        }
        try {
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            data.writeInterfaceToken(DESCRIPTOR);
            data.writeInt(x);
            data.writeInt(y);
            mIBinder.transact(1, data, reply, 0);
            reply.readException();
            int s = reply.readInt();
            showToast(String.valueOf(s));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }


    private void max(int x, int y) {
        if (mIBinder == null) {
            return;
        }
        try {
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            data.writeInterfaceToken(DESCRIPTOR);
            data.writeInt(x);
            data.writeInt(y);
            mIBinder.transact(2, data, reply, 0);
            reply.readException();
            int s = reply.readInt();
            showToast(String.valueOf(s));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }


    private void min(int x, int y) {
        if (mIBinder == null) {
            return;
        }
        try {
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            data.writeInterfaceToken(DESCRIPTOR);
            data.writeInt(x);
            data.writeInt(y);
            mIBinder.transact(3, data, reply, 0);
            reply.readException();
            int s = reply.readInt();
            showToast(String.valueOf(s));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

}

代码稍微有点多,但相比之前的AIDL案例,并没有改动太多。首先创建一个IBinder实例,然后在ServiceConnection里面去获取对象。这个时候,我们去调用add方法,我们来看一下这里面的代码:

 private void add(int x, int y) {
        if (mIBinder == null) {
            return;
        }
        try {
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            data.writeInterfaceToken(DESCRIPTOR);
            data.writeInt(x);
            data.writeInt(y);
            mIBinder.transact(1, data, reply, 0);
            reply.readException();
            int s = reply.readInt();
            showToast(String.valueOf(s));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

首先我们获取到两个序列化对象(关于序列化不懂自行百度),一个data用于将客户端的数据传输出去,一个reply用于获取服务端返回的数据,然后这一行代码 data.writeInterfaceToken(DESCRIPTOR); 对应了AIDL案例中的 data.enforceInterface(DESCRIPTOR); 前文已经说明,这是一种认证行为,不再做过多解释。然后按照应有的顺序执行,不可随意更改代码顺序,否则会造成异常。这里需要说明 mIBinder.transact(1, data, reply, 0); 这行代码执行后,实际上就是调用了服务端的 onTransact 方法,那么我们接下来就可以获取到服务端执行业务后的结果了。

其他说明

另外AIDL需要注意的有以下几点:

  • 服务端与客户端均需要一份相同路径的XXX.aidl文件

  • AIDL的调用过程是同步的,但是它的内部会开启线程,这一点比较迷糊。(简单来说,客户端的ICalculateAIDL.java文件的Stub字类中还有一个类Proxy,这个类内部会调用transact方法,在这个时候,客户端就等待服务端的结果回来,那么这个时候实际上是处于主线程的,如果服务端处理的过程比较慢,那么客户端就有可能报ANR了)

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多