Android IPC 之AIDL应用(下)

1,756 阅读6分钟

前言

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

Android IPC 之Service 还可以这么理解
Android IPC 之Binder基础
Android IPC 之Binder应用
Android IPC 之AIDL应用(上)
Android IPC 之AIDL应用(下)
Android IPC 之Messenger 原理及应用
Android IPC 之服务端回调
Android IPC 之获取服务(IBinder)
Android Binder 原理换个姿势就顿悟了(图文版)

上篇文章分析了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