Android IPC 之AIDL应用(下)

前言

IPC 系列文章: 建议按顺序阅读。

Android IPC 之Service 还可以这么理解
Android IPC 之Binder基础
Android IPC 之Binder应用
Android IPC 之AIDL应用(上)
Android IPC 之AIDL应用(下)
Android IPC 之Messenger 原理及应用
Android IPC 之服务端回调
Android IPC 之获取服务(IBinder)

上篇文章分析了AIDL原理及其基本使用,本篇文章将继续深入分析AIDL其它用法及其注意事项。
通过本篇文章,你将了解到:

1、AIDL 传递非基本数据类型
2、AIDL 数据流方向

1、AIDL 传递非基本数据类型

在上篇文章中定义AIDL文件时,方法形参都是使用基本参数,实际需求里不仅仅只传递基本参数。比如客户端想从服务端获取学生信息,包括姓名、年龄等。

自定义数据类型

public class Student implements Parcelable {
    private String name;
    private int age;

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

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

    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 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;
    }
}
复制代码

声明了Student类,该类里有学生的信息:姓名、年龄。
跨进程传递对象需要序列化数据,因此采用Parcelable 进行序列化,实现Parcelable需要实现其方法: describeContents()与writeToParcel(xx),并且还需要添加静态类:CREATOR,用来反序列化数据。
Parcelable序列化都是标准样式,实际上就做了两件事:

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

AIDL 使用自定义数据类型

准备好了数据类型,接着来看看如何使用它。

package com.fish.myapplication;

import com.fish.myapplication.service.Student;//----------------(1)
interface IMyServer {
    void getStudentInfo(int age, in Student student);//------------(2)
}
复制代码

(1)
与平时一致,引入一个新的类型,要将其类名import 出来。

(2)
getStudentInfo(xx)有个形参类型为:Student student,并且前边还有个"in" 标记(这个后续说)

自定义数据类型关联的AIDL

上面的代码是无法编译通过的,还需要在AIDL里声明自定义数据类型关联的AIDL。
新建名为:Student 的AIDL 文件,默认内容如下:

package com.fish.myapplication.service;
interface Student {
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}
复制代码

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

package com.fish.myapplication.service;
parcelable Student;
复制代码

这么改造后,Student.aidl生成的Student.java 文件内容为空。
修改后,编译成功。

注意事项

包名类名一致
Student.aidl和自定义数据类型Student.java 需要保持包名类名一致。

image.png

如上图,Student.java 包名为:com.fish.myapplication.service

再看Student.aidl 结构:

image.png

如上图,Student.aidl 包名为:com.fish.myapplication.service

可以看出,两者包名一致。

解决类重复问题
编写过程中可能会遇到类重复问题:
先定义了Student.java,当再定义Student.aidl 时,若两者处于同一包下,那么将无法创建Student.aidl文件。
分两种方法解决:

第一种:先定义Student.aidl,并将其内容改造,最后定义Student.java。
第二种:先定义Student.java 在与Student.aidl不同的包名下,然后再定义Student.aidl,并改造内容,最后将Student.aidl 移动至与Student.java 同一包名下。

客户端/服务端处理自定义数据类型

服务端业务

public class MyService extends Service {

    private final String TAG = "IPC";

    //构造内部类
    private IMyServer.Stub stub = new IMyServer.Stub() {
        @Override
        public void getStudentInfo(int age, Student student) throws RemoteException {
            Log.d(TAG, student.getName() + " in server");
        }
    };

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        //Stub 继承自Binder,因此是IBinder类型
        return stub;
    }
}
复制代码

获取传递过来的Student,并打印其姓名。

客户端业务

    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IMyServer iMyServer = IMyServer.Stub.asInterface(service);
            try {
                iMyServer.getStudentInfo(2, new Student("xiaoming", 18));
            } catch (Exception e) {

            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
复制代码

构造Student对象,并传递给服务端。
运行结果如下:

image.png

总结AIDL 使用自定义数据类型步骤

1、构造自定义数据类型同名.aidl文件
2、构造自定义数据类型.java文件
3、在AIDL 接口里使用自定义数据类型

2、AIDL 数据流方向

什么是数据流

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

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

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

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

image.png

数据流具体使用

从上图可以看出,数据流方向有三种:

in
out
inout

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

package com.fish.myapplication;

import com.fish.myapplication.service.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,不用刻意标注。

服务端实现方法:

    private IMyServer.Stub stub = new IMyServer.Stub() {
        @Override
        public void getStudentInfoIn(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 getStudentInfoOut(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 getStudentInfoInout(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) {
            IMyServer iMyServer = IMyServer.Stub.asInterface(service);
            try {
                Student student = new Student("xiaoming", 18);
                Log.d(TAG, "student name:" + student.getName() + " in client before getStudentInfoIn");
                iMyServer.getStudentInfoIn(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.getStudentInfoOut(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.getStudentInfoInout(2, student3);
                Log.d(TAG, "student name:" + student3.getName() + " in client after getStudentInfoInout");

            } catch (Exception e) {

            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
        }
    };
复制代码

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

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

运行后结果如下:

image.png

总结一下规律:

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 的结合。

接下来将分析Messenger。

本文基于Android 10.0。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营学习Android

猜你喜欢

转载自juejin.im/post/7031773320756330504