AIDL用法详解

AIDL用法详解

如何编写AIDL文件

AIDL 是Android Interface Definition Language (Android 接口定义语言)的缩写。只看这个定义其实也并不明白,其实它就是一个Android IPC通信的一个代码规范。并不是一个新的语言啥的。

分别介绍 在同一APP和不同APP通信的情况。

1.在同一APP内

1.1 编写AIDL文件

AS 提供右键提示创建AIDL的功能。

在main目录上右键 ,AIDL文件和Src目录统计

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. 重构构建下工程

在这里插入图片描述
在build目录下就会生成 对应的.java 文件
在这里插入图片描述
关于里面的内容,后面会分析

3. 编写Service,具体实现要暴露的接口

在这里插入图片描述

4. 客户端与服务端绑定

在这里插入图片描述

5. 执行结果

在这里插入图片描述

6. 注意事项
  1. 创建的service要在AndroidManifest.xml注册

     <service
            android:name=".binder.MyAIDLService"
            android:enabled="true"
            android:exported="true"
            
            android:process=":aidl"/>
    

    如果要让Service运行在不同的进程,添加process属性 android:process=“:hello” 表示当前进程名是包名+aidl 即 com.example.androidlearn:aidl。

2.不通APP内

先说在前面 : 我们需要保证,在客户端和服务端中都有我们需要用到的所有.aidl 文件和其中涉及到的 .java 文件,因此不管在哪一端写的这些东西,写完之后我们都要把这些文件复制到另一端去。

服务端创建形式和同一app内相同,不同的地方如下:

1. 客户端的创建方式。

在这里插入图片描述

让客户端和服务端的AIDL保持一致,重构之后,也会生成对应的java文件,这样是为了让客户端可以引用到服务端aidl文件的方法

2. 绑定服务端service
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_aidlclient_main);

        findViewById(R.id.aidl_client_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            
                Intent intent = new Intent();
                ComponentName componentName = new ComponentName("com.example.aidl_servier", "com.example.aidl_servier.MyAIDLService");
                intent.setComponent(componentName);
                
                bindService(intent,serviceConnection,BIND_AUTO_CREATE);
            }
        });
    }

由于服务端与客户端在不通的应用,要想绑定服务,使用intent的setComponment方法

3.先启动服务端,再启动客户端

自己误区的地方:在同一工程不同应用,通过setComponent可以启动工程的其他应用。以为其他Service不需要先运行,setComponent也能这样启动呢,其实对于Service不是这样。如果是这样,我不经过你允许就调起后台全部服务,岂不是崩溃了。
所以必须先启动服务端。

数据类型

支持的数据类型

  • Java中的八种基本数据类型,包括 byte,short,int,long,float,double,boolean,char。
  • String 类型。
  • CharSequence类型。
  • List类型:List中的所有元素必须是AIDL支持的类型之一,或者是一个其他AIDL生成的接口,或者是定义的parcelable(下文关于这个会有详解)。List可以使用泛型。
  • Map类型:Map中的所有元素必须是AIDL支持的类型之一,或者是一个其他AIDL生成的接口,或者是定义的parcelable。Map是不支持泛型的。

AIDL默认支持的数据类型都是可序列化的。

为什么AIDL的传递的数据都要支持序列化?

由于不同的进程有着不同的内存区域,并且它们只能访问自己的那一块内存区域,目标进程根本不能访问源进程的内存,所以我们必须将要传输的数据转化为能够在内存之间流通的形式。这个转化的过程就叫做序列化与反序列化。
简单来说是这样的:比如现在我们要将一个对象的数据从客户端传到服务端去,我们就可以在客户端对这个对象进行序列化的操作,将其中包含的数据转化为序列化流,然后将这个序列化流传输到服务端的内存中去,再在服务端对这个数据流进行反序列化的操作,从而还原其中包含的数据——通过这种方式,我们就达到了在一个进程中访问另一个进程的数据的目的。

自定义数据类型

如果想要自定义数据类型,进行AIDL通信,就需要对类型进行序列化,而通常,在我们通过AIDL进行跨进程通信的时候,选择的序列化方式是实现 Parcelable 接口。

默认实现Parcelable 接口代码如下:

public class Student implements Parcelable {

    protected Student(Parcel in) {
    }

    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) {
    }
}

但是请注意,这里有一个坑:上述默认生成的序列化类的对象只支持为 in 的定向 tag

为什么呢?因为默认生成的类里面只有 writeToParcel() 方法,而如果要支持为 out 或者 inout 的定向 tag 的话,还需要实现 readFromParcel() 方法——而这个方法其实并没有在 Parcelable 接口里面,所以需要我们自己写。后面数据流向有解释。

为Student类添加姓名和年龄信息。

完整的Student类兑现的代码是这样的:

package com.example.aidl_servier;

import android.os.Parcel;
import android.os.Parcelable;

public class Student implements Parcelable {

    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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



    protected Student(Parcel in) {

        // 从序列化里解析成员变量
        name = in.readString();
        age = in.readInt();

    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    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.writeString(name);
        dest.writeInt(age);
    }
    
    public void readFromParcel(Parcel parcel) {
        this.name = parcel.readString();
        this.age = parcel.readInt();
    }
}

Parcelable序列化都是标准样式,实际上就做了两件事:

1、将Student数据分别写入到序列化对象Parcel里
2、从序列化对象Parcel里构建出Student对象

至此,关于AIDL中非默认支持数据类型的序列化操作就完成了。

使用自定义数据类型

对于同一app下 AIDL使用自定义数据类型通信就不展示了。
主要针对不同app,这个看懂,同一app更不在话下。

服务端
1. 创建序列化数据类型

上一节形式创建Student序列化类。

2.创建非默认类型AIDL文件

同样右键的方式创建AIDL文件,

// Student.aidl
package com.example.aidl_servier;

// Declare any non-default types here with import statements

interface Student {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

将以上内容删除,改造成如下内容:

// Student.aidl
package com.example.aidl_servier;

// Declare any non-default types here with import statements

parcelable Student;

注意: 一定要确保自定义序列化类型数据和对应的aidl文件的包类名一致
在这里插入图片描述

解决类重复的问题

先定义了Student.java,当再定义Student.aidl 时,若两者处于同一包下,那么将无法创建Student.aidl文件。

分几种方法解决:

  1. 先定义Student.aidl,最后定义Student.java
  2. 先定义Student.java 在与Student.aidl不同的包名下,然后再定义Student.aidl,最后将Student.aidl 移动至与Student.java 同一包名下。
  3. 先不命名Student,比方命名hhh.java,等Student.aidl定义好了 再把hhh改为Student。
3.被其他aidl文件作为参数使用

创建IMyserver.aidl文件 使用自定义数据类型

// IMyServer.aidl
package com.example.aidl_servier;

import com.example.aidl_servier.Student; //----------------(1)
interface IMyServer {
    void getStudentInfo(int age, in Student student);//------------(2)
}
  1. 与平时一致,引入一个新的类型,要将其类名import 出来。(一定要导入)
  2. getStudentInfo(xx)有个形参类型为:Student student,并且前边还有个"in" 标记(这个后续说)
4.服务端Service

与之前创建Service逻辑相同

public class MyAIDLService extends Service {

    private static final String TAG = "MyAIDLService";

    IMyServer.Stub myBinder = new IMyServer.Stub(){
        
        @Override
        public void getStudentInfo(int age, Student student) throws RemoteException {
            Log.d(TAG, student.getName() + " in server");
        }
    };

    public MyAIDLService() {
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return myBinder;

    }
}
客户端

和之前说的相同,把所有的.aidl文件和涉及到的.java 文件从服务端复制到客户端去。都要保证这些文件的包类名和服务端的包类名保证相同。否则会会报 Symbol not found 错误。AIDL文件是整个复制过去的,所以aidl对应的包名是相同的。对于Student.java 客户端和服务端的包名是不同的,我们可以在客户端新建包名(和服务端相同),然后把服务端的Student.java赋值过去。

为什么这样做呢?
客户端的IMServer.aidl是从服务端复制过去的
在这里插入图片描述
构建之后,生成的.IMyServer.java如下
在这里插入图片描述
导入的是 com.example.aidl_servier.Student student
如果客户端没有定义Student,肯定会 Symbol not found 错误。所以涉及到.java文件也要复制过去。要和生成的.java文件引入保持一致,也就是包名相同嘛。

客户端业务和之前相同

package com.example.aidl_client;

import androidx.appcompat.app.AppCompatActivity;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;

import com.example.aidl_servier.IMyServer;
import com.example.aidl_servier.Student;

public class AIDLClientMainActivity extends AppCompatActivity {

    private static final String TAG = "AIDLClientMainActivity";

    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

            Log.d(TAG, "onServiceConnected: ");


            IMyServer iMyServer = IMyServer.Stub.asInterface(service);
            try {
                // iMyServer.say("hello server");
                iMyServer.getStudentInfo(2,new Student("xiaoHong",18));
                Log.d(TAG, "onServiceConnected: 成功通信");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

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

        findViewById(R.id.aidl_client_btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                ComponentName componentName = new ComponentName("com.example.aidl_servier", "com.example.aidl_servier.MyAIDLService");
                intent.setComponent(componentName);
                bindService(intent,serviceConnection,BIND_AUTO_CREATE);
            }
        });
    }
}

运行结果如下
在这里插入图片描述

数据流向

回顾一下常用的方法调用方式:

    public void getStudentInfo(int age, Student student) {
        student.setName("modify");
    }

形参为:int 类型;Student类型;
在同一进程里,当调用该方法时,传入Student引用,方法里对Student成员变量进行了更改,方法调用结束后,调用者持有的Student引用所指向的对象其内容已经更改了。而对于int 类型,方法里却无法更改。
上述涉及到了经典问题:传值与传址。

而对于不同的进程,当客户端调用getStudentInfo(xx)方法时,虽然看起来是直接调用服务端的方法,实际上是底层中转了数据,因此当初传入Student,返回来的已经不是同一个Student引用。
因此,AIDL 规定了数据流方向。
在这里插入图片描述

之前又说到 参数类型前面有in关键字。这个是定向tag,表示了在跨进程通信中数据的流向,数据流向是针对在客户端中的那个传入方法的对象而言的,有三种类型

  • in
    其中 in 表示数据只能由客户端流向服务端,表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;
  • out
    out 表示数据只能由服务端流向客户端,表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动。
  • inout
    inout 则表示数据可在服务端与客户端之间双向流通,nout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动

另外,Java 中的基本类型和 String ,CharSequence 的定向 tag 默认是 in,不用标注 。还有,请注意,请不要滥用定向 tag ,而是要根据需要选取合适的——要是不管三七二十一,全都一上来就用 inout ,等工程大了系统的开销就会大很多——因为排列整理参数的开销是很昂贵的。

为测试它们的差异,分别写三个方法:

package com.fish.myapplication;

import package com.example.aidl_servier.Student;
interface IMyServer {
    void getStudentInfo(int age, in Student student);
    void getStudentInfo2(int age, out Student student);
    void getStudentInfo3(int age, inout Student student);
}

基本数据类型如 int、String 默认是数据流类型是: in,不用刻意标注。

服务端实现方法:

    IMyServer.Stub myBinder = new IMyServer.Stub(){
        @Override
        public void say(String word) throws RemoteException {
            Log.d(TAG, "say: "+word);
        }

        @Override
        public void getStudentInfo(int age, Student student) throws RemoteException {
            Log.d(TAG, "student name:" + student.getName() + " in server in getStudentInfoIn");
            student.setName("change name getStudentInfoIn");

        }

        @Override
        public void getStudentInfo2(int age, Student student) throws RemoteException {
            Log.d(TAG, "student name:" + student.getName() + " in server in getStudentInfoOut");
            student.setName("change name getStudentInfoOut");

        }

        @Override
        public void getStudentInfo3(int age, Student student) throws RemoteException {
            Log.d(TAG, "student name:" + student.getName() + " in server in getStudentInfoInout");
            student.setName("change name getStudentInfoInout");
        }
    };

将Student name 打印出来,并更改name 内容。
客户端调用服务端方法


    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {

            Log.d(TAG, "onServiceConnected: ");


            IMyServer iMyServer = IMyServer.Stub.asInterface(service);
            try {
                // iMyServer.say("hello server");
                Student student = new Student("xiaoming", 18);
                Log.d(TAG, "student name:" + student.getName() + " in client before getStudentInfoIn");
                iMyServer.getStudentInfo(2, student);
                Log.d(TAG, "student name:" + student.getName() + " in client after getStudentInfoIn");


                Student student2 = new Student("xiaoming", 18);
                Log.d(TAG, "student name:" + student2.getName() + " in client before getStudentInfoOut");
                iMyServer.getStudentInfo2(2, student2);
                Log.d(TAG, "student name:" + student2.getName() + " in client after getStudentInfoOut");

                Student student3 = new Student("xiaoming", 18);
                Log.d(TAG, "student name:" + student3.getName() + " in client before getStudentInfoInout");
                iMyServer.getStudentInfo3(2, student3);
                Log.d(TAG, "student name:" + student3.getName() + " in client after getStudentInfoInout");

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

       

构造Student 对象,并分别打印调用服务端方法前后Student name名字。
当编译的时候,发现编译不过,还需要在Student.java 里添加方法:

    public Student() {}
    public void readFromParcel(Parcel parcel) {
        this.name = parcel.readString();
        this.age = parcel.readInt();
    }

运行结果如下:
在这里插入图片描述

总结一下规律:

1、使用 in 修饰Student,服务端收到Student的内容,更改name后,客户端收到Student,其name 并没有改变。表示数据流只能从客户端往服务端传递。
2、使用 out 修饰Student,服务端并没有收到Student的内容,更改name后,客户端收到Student,其name 已经改变。表示数据流只能从服务端往客户端传递。
3、使用 inout 修饰Student,服务端收到Student的内容,更改name后,客户端收到Student,其name 已经改变。表示数据流能在服务端和客户端间传递。

数据流在代码里的实现

AIDL 文件最终生成.java 文件,因此在该文件里找答案。
当使用 in 修饰时
对于客户端,将Student数据写入序列化对象。

          if ((student!=null)) {
            _data.writeInt(1);
            student.writeToParcel(_data, 0);
          }

对于服务端,并没有将Student写入回复的序列化对象。
当使用 out 修饰时
对于客户端,没有将Student数据写入序列化对象。
对于服务端,将Student写入回复的序列化对象。

          _arg1 = new com.fish.myapplication.service.Student();
          this.getStudentInfoOut(_arg0, _arg1);
          reply.writeNoException();
          if ((_arg1!=null)) {
            reply.writeInt(1);
            //写入reply
            _arg1.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
          }

当使用 inout 修饰时
实际上就是 in out 的结合。

AIDL生成对应Java内容解析

看这篇 Android IPC 之AIDL应用(上)

参考:

  • 10
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
Android AIDLAndroid Interface Definition Language)是一种用于定义客户端和服务之间通信接口的语言。AIDL 是一个 Android 特有的 RPC(远程过程调用)机制。 下面是使用 AIDL 的基本步骤: 1. 定义 AIDL 接口 在服务端创建一个 AIDL 文件,定义服务的接口方法。例如,创建一个名为 IMyService.aidl 的文件,其中包含以下内容: ``` interface IMyService { void sayHello(); } ``` 2. 实现 AIDL 接口 在服务端实现 AIDL 接口,例如: ``` public class MyService extends Service { private final IMyService.Stub binder = new IMyService.Stub() { @Override public void sayHello() throws RemoteException { Log.i("MyService", "Hello World"); } }; @Nullable @Override public IBinder onBind(Intent intent) { return binder; } } ``` 3. 绑定服务 在客户端绑定服务,例如: ``` private ServiceConnection connection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { IMyService myService = IMyService.Stub.asInterface(service); try { myService.sayHello(); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { } }; private void bindService() { Intent intent = new Intent(this, MyService.class); bindService(intent, connection, Context.BIND_AUTO_CREATE); } ``` 通过上述步骤,就可以实现客户端与服务端之间的通信。需要注意的是,AIDL 接口中定义的方法必须是可序列化的。如果方法参数或返回值类型不支持序列化,可以通过 Parcelable 接口实现序列化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值