AIDL使用详解及原理(附加例子)

AIDL是什么?

AIDL(Android Interface Define Language ——Android接口定义语言)是一种IPC通信方式,可以利用它来定义两个进程相互通信的接口,与 Service 进行跨应用、跨进程通信的一种机制,高效、灵活,使用方便。它的本质是C/S架构的,需要一个服务器端,一个客户端。既然一个服务器端一个客户端,那么开始撸码实现。

开始AIDL的实现

服务端步骤如下:

  1. 创建一个安卓工程
  2. 生成aidl文件,在aidl文件中写入接口方法
  3. 实现接口,并向客户端放开接口

客户端步骤如下:

  1. 创建一个安卓工程
  2. 新建aidl目录
  3. 绑定服务,调试服务端接口

创建一个安卓工程(服务端)

   首先在Android Studio中创建一个Andorid工程,作为aidl的服务端,包名为com.lu.aidlserver。

生成aidl文件

   利用AS自带的插件,在src目录中右键生成一个aidl文件(new——>AIDL——>AIDL File——>输入文件名) 。


从图中可以看出生成了一个IMyAidlInterface.aidl文件。打开文件看到如下内容。

package com.lu.aidlserver;
// Declare any non-default types here with import statements

interface IMyAidlInterface {
    /**
         * Demonstrates some basic types that you can use as parameters
         * and return values in AIDL.
         * 从注解可以看得出 basicTypes()方法中的参数都是 aidl定义可以用的参数类型
         */
    //    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
    //            double aDouble, String aString);

           String getName();//获取一个名称
           int countSum(int a ,int b);//计算a+b
}

看到aidl的语法跟java是一样的,也很好理解,声明了一个接口,里面定义了aidl服务器端暴露给客户端调用的方法,方法的参数是一些可支持的数据类型,还能支持别的数据类型?这里先放一放,下面会讲到。从注解可以看得出 basicTypes()方法中的参数都是 aidl定义可以用的参数类型。当然可以根据自己的需求定义接口。在里面的就定义了两个接口方法。完成这部分操作之后还没有结束,我们需要手动编译程序,生成aidl对应的Java代码。


通过实现接口,并向客户端放开接口

/**
 * Author: 安仔夏天勤奋
 * Date: 2019/6/18
 * Desc: 服务
 */
public class MyAidlService extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();//返回给客户端的Binder
    }

    //获取aidl的Binder
    private class MyBinder extends IMyAidlInterface.Stub{
        @Override
        public String getName() throws RemoteException {
            return "我是AIDL";
        }

        @Override
        public int countSum(int a, int b) throws RemoteException {
            return a+b;
        }
    }
}

创建一个安卓工程(客户端)

  客户端和服务端一样的,就不多创建一个工程了,与服务端一个工程下,创建一个客户端module,包名为com.lu.aidlclient


客户端新建aidl目录

  将服务器端的aidl拷贝到客户端,特别要注意,拷贝后的客户端的aidl文件包目录必须与服务器端保持一致,拷贝完后同样时编译工程,让客户端也生成对应的java文件。

绑定服务,调试服务端接口

  在MainActivity中调试服务端的接口打打印。

public class MainActivity extends AppCompatActivity {

    private com.lu.aidlserver.IMyAidlInterface binder = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //android5.0之前都可以通过配置在manifest里service 的action来启动。android5.0之后都必须使用显示intent。
//        //启动aidl 的服务
//        Intent intent = new Intent();
//        intent.setComponent(new ComponentName("com.lu.aidlserver",
//                "com.lu.aidlserver.MyAidlService"));
//        //第一个参数是包名,第二个是类名

        Intent service = new Intent("com.lu.aidlserver.MyAidlService");
        // 绑定AIDL 隐式
        bindService(service, connection, BIND_AUTO_CREATE );
    }

    // 创建远程调用对象
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 从远程service中获得AIDL实例化对象
            //这里不能强制转换,因为虽然类的内容是一样的,但是却不是同一个。(每个app能访问到的毕竟是自己的类)
            binder = com.lu.aidlserver.IMyAidlInterface.Stub.asInterface(service);
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            binder = null;
        }
    };

    public void onClickTest(View v){
        try {

            Log.e("lu","Name : "+binder == null ? "null" :
                    binder.getName()+"      计算a+b="+binder.countSum(9,1));
            }

        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}
运行结果
06-19 00:44:32.389 4314-4314/com.lu.aidlclient E/lu: 我是AIDL      计算a+b=10

打印出来结果,说明远程调用成功了,aidl的整个流程就走完了。做到这一步起码你对AIDL有一定的认知了。下面讲一下上面提到的的问题,除了AS生成aidl文件给出的数据类型还能用其他数据类型作解答。

AIDL可使用的参数类型

  官方的数据类型就是只有这几种了。

void basicTypes(int anInt, long aLong,boolean aBoolean, float aFloat, double aDouble, String aString);

  我们知道Java有8中基本数据类型,分别为byte、char、short、int、long、float、double、boolean,这些类型是否都能作为aidl的参数进行传递呢?我们可以在aild中尝试下

void basicTypes(byte aByte, char aChar, short aShort, int anInt, long aLong, float aFloat,
            double aDouble, boolean aBoolean);

定义好后,我们手动去编译,发现无法成功编译,经过筛查发现aidl并不能支持short的数据类型,至于为什么呢,可以看一看Android中的Parcel,Parcel是不支持short的。更具体还没有查阅资料,有兴趣的可以自行查阅。官方文档

AIDL引用数据类型

引用数据类型根据官方文档介绍,可以使用String、CharSequence、List、Map。这样的话我们也可以使用自定义数据类型。

自定义数据类型

  话不多说,代码先行。在服务端创建一个学生类,并添加一些属性。


QQ图片20190619215714.png
public class Student implements Parcelable {
    private int age;
    private String name;
    private String sex;
    public Student(int age, String name, String sex) {
        this.age = age;
        this.name = name;
        this.sex = sex;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    public String toString() {
        return "\n名字="+this.name+"\t\t性别="+this.sex+"\t\t年龄="+this.age;
    }

    /**
     * Parcelable对数据进行分解/编组的时候必须使用相同的顺序,
     * 字段以什么顺序分解的,编组时就以什么顺序读取数据,不然会有问题
     * @param in Parcel
     */
    protected Student(Parcel in) {
        this.age = in.readInt();
        this.name = in.readString();
        this.sex = in.readString();
    }

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

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

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

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(age);
        dest.writeString(name);
        dest.writeString(sex);
    }
}

自定义数据类型,用于进程间通信的话,用Parcelable接口实现,Parcelable是类似于Java中的Serializable,Android中定义了Parcelable,用于进程间数据传递,对传输数据进行分解,编组的工作,相对于Serializable,Parcelable对于进程间通信更加高效。创建完Student实体后,还需要创建一个aidl文件,来定义一下我们的Student,否则Student在aidl中无法识别,在服务端的aidl文件中创建一个Student的aidl文件。



Student.aidl文件就两个话。

// Student.aidl
package com.lu.aidlserver;
parcelable Student;

在之前的服务器端IMyAidlInterface.aidl文件中添加addStudent方法。

// IMyAidlInterface.aidl
package com.lu.aidlserver;
// Declare any non-default types here with import statements
import com.lu.aidlserver.Student;

interface IMyAidlInterface {
    /**
         * Demonstrates some basic types that you can use as parameters
         * and return values in AIDL.
         * 从注解可以看得出 basicTypes()方法中的参数都是 aidl定义可以用的参数类型
         */
    //    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
    //            double aDouble, String aString);

           String getName();//获取一个名称
           int countSum(int a ,int b);//计算a+b
           List<Student> addStudent(in Student student);
}

在MyAidlService中实现添加的addStudent()。

public class MyAidlService extends Service {

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new MyBinder();//返回给客户端的Binder
    }

    //获取aidl的Binder
    private class MyBinder extends IMyAidlInterface.Stub{
        @Override
        public String getName() throws RemoteException {
            return "我是AIDL";
        }

        @Override
        public int countSum(int a, int b) throws RemoteException {
            return a+b;
        }

        @Override
        public List<Student> addStudent(Student student) throws RemoteException {
            List<Student> students=new ArrayList<>();
            students.add(student);
            return students;
        }
    }
}

服务端一切都准备好了,记得重新手动编译。下面进行客户端操作。

将服务端的两个aidl文件复制到客户端,包结构必须一致,aidl文件发生变化不要忘记重新编译代码。


接着将Student实体也复制到客户端,并且包结构一致。

最后在客户端的MainActivity中的点击事件方法中调用服务端的addStudent()。

public void onClickTest(View v){
        try {

            Log.e("lu","Name : "+binder == null ? "null" :
                    binder.getName()+"      计算a+b="+binder.countSum(9,1));

            if(binder!=null){
                List<Student> students = new ArrayList<>();
                students.addAll(binder.addStudent(new Student(18,"小卢","男")));
                students.addAll(binder.addStudent(new Student(19,"张三","男")));
                students.addAll(binder.addStudent(new Student(20,"李四","男")));
                Log.e("lu","List<Student> : "+students.toString());
            }

        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

运行服务端与客户端App,点击测试,输出以下日志,说明调用成功。

06-19 22:29:04.609 5323-5323/com.lu.aidlclient E/lu: 我是AIDL      计算a+b=10
06-19 22:29:04.610 5323-5323/com.lu.aidlclient E/lu: List<Student> : [
    名字=小卢       性别=男        年龄=18, 
    名字=张三       性别=男        年龄=19, 
    名字=李四       性别=男        年龄=20]

AIDL的demo搭建完成并能正常运行。下面就聊聊AIDL原理。

AIDL原理

  在上面的服务端的例子手动编译时,在路径为aidlserver\build\generated\aidl_source_output_dir\debug\compileDebugAidl\out\com\lu\aidlserver\目录下生成了一个IMyAidlInterface.java文件


要了解aidl原理,我们需要看一下根据aidl生成的对应的java代码(IMyAidlInterface.java),一段一段进行分析。

package com.lu.aidlserver;
public interface IMyAidlInterface extends android.os.IInterface{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.lu.aidlserver.IMyAidlInterface
{...}
public java.lang.String getName() throws android.os.RemoteException;
public int countSum(int a, int b) throws android.os.RemoteException;
//计算a+b
public java.util.List<com.lu.aidlserver.Student> addStudent(com.lu.aidlserver.Student student) throws android.os.RemoteException;
}

从生成的代码结构来看很简单,一个静态抽象类Stub,以及aidl中定义的方法。我们也不难看出Stub就是AIDL的核心代码,Stub类的结构如下图。


Stub的目录结构也不复杂,Stub继承系统中的Binder并实现了我们定义的aidl接口。一个构造函数,一个asInterface方法,一个asBinder方法,一个onTransact方法,一个Proxy代理类,这边Proxy与Stub同时实现了我们定义的aidl,且Proxy中实现了我们在aidl中定义的getName、countSum、addStudent方法,下面三个int为告诉系统的方法Id。

在客户端,我们绑定服务的时候通过Stub.asInterface()回去aidl对象,代码如下:

// 创建远程调用对象
    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 从远程service中获得AIDL实例化对象
            //这里不能强制转换,因为虽然类的内容是一样的,但是却不是同一个。(每个app能访问到的毕竟是自己的类)
            binder = com.lu.aidlserver.IMyAidlInterface.Stub.asInterface(service);
        }
        @Override
        public void onServiceDisconnected(ComponentName name) {
            binder = null;
        }
    };

也不难发现,客户端获取到的是一个远程服务的代理Stub.Proxy。查看asInterface源码。

public static com.lu.aidlserver.IMyAidlInterface asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.lu.aidlserver.IMyAidlInterface))) {
return ((com.lu.aidlserver.IMyAidlInterface)iin);
}
return new com.lu.aidlserver.IMyAidlInterface.Stub.Proxy(obj);
}

客户端调用getName、countSum、addStudent方法其实调用的时Stub.Proxy中实现的getName、countSum、addStudent,在Stub.Proxy中可以看到这几句核心代码。

//getName()
mRemote.transact(Stub.TRANSACTION_getName, _data, _reply, 0);
//countSum()
mRemote.transact(Stub.TRANSACTION_countSum, _data, _reply, 0);
//addStudent()
mRemote.transact(Stub.TRANSACTION_addStudent, _data, _reply, 0);

mRemote其实就是IMyAidlInterface.Stub,随意mRemote.transact传递到了IMyAidlInterface.Stub.OnTransact, onTransact中执行了getName、countSum、addStudent,回调到了我们在服务端定义MyAidlService中声明MyBidner时重写的getName、countSum、addStudent,这就是AIDL的整个流程。

private class MyBinder extends IMyAidlInterface.Stub{
        @Override
        public String getName() throws RemoteException {
            return "我是AIDL";
        }

        @Override
        public int countSum(int a, int b) throws RemoteException {
            return a+b;
        }

        @Override
        public List<Student> addStudent(Student student) throws RemoteException {
            List<Student> students=new ArrayList<>();
            students.add(student);
            return students;
        }
    }

总结

  • 服务端编写完时或AIDL文件发生变化不要忘记重新编译代码
  • 客户端复制AIDL文件时,一定要注意包名一致
  • 自定义数据类型时,Parcelable对数据进行分解/编组的时候必须使用相同的顺序
  • 客户端android5.0之前都可以通过配置在manifest里service 的action来启动。android5.0之后都必须使用显示intent。
AIDL原理流程图

AIDLdemo代码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容