曾几何时给自己立过一个 FLAG,那就是想抽空将自己对一些技术的理解以书面的形式记录下来,因为这样不仅能增加自己的记忆,而且也会方便自己查阅,当然如果还能有机会帮到一些有需要的人,那么估计会是我莫大的荣幸。
BUT
这个 FLAG 好难实现啊,感觉很多业余时间都没有利用起来,让它白白的浪费掉了,今天也是好不容易抽空静下来才能写这篇遍博客,所以这里咱也不好高骛远,仅当笔记使用,如有错误缺失,望能留言提示!
(看完这篇博客你们要是还不会AIDL我让你们连着打三天,还是不带还手的那种)
在讲AIDL之前,其实我们不得不提及另外一个概念,那就是IPC(Inter-Process Communication),它的意思就是进程间通信,主要用来形容两个进程之间进行数据交换的过程。那什么又叫做进程呢?它和线程又有什么区别?简单概述一下,因为进程(Process)和线程(Thread)其实是两个截然不同的概念,大家还是需要有一个比较清晰的认识的。
线程 我们使用的比较频繁,在操作系统中,它是CPU调度的最小单元,是一种有限的系统资源。而 进程 就较为不同了,它指的则是一个执行单元,在PC端和移动设备上则用来形容的是一个程序或者一个应用。其中一个进程可以包含多个线程,在Android中,最简单的情况下,一个进程也会包含一个线程,即主线程,也就是我们通常说的UI线程。
而在 Android 中 IPC 则可以理解为是应用和应用之间的通信,当然这里提到的的应用我们假定为都是单进程的,大家可能也了解过,Android程序虽然也是运行在虚拟机上的,只是与JAVA程序中只能存在一个JVM实例的情况不同,在Android上面,每一个应用程序默认就对应了一个进程,而每一个进程都有自己的Dalvik VM或者ART虚拟机实例。它们分别拥有着自己的单独的内存地址,同时每个进程也只能处理自己内部的数据,在进程与进程之间是不允许进行普通形式的数据访问的(注意:普通形式)。这样做的好处就是保证了进程间数据的安全性,同时也保证了在运行时,一个应用程序奔溃了也不会导致其他程序的奔溃,提高用户的的使用体验。
这里是我画的一个进程的内存分布图,很简陋,但也能表示出一些问题了,其中每一个小圆点就代表了一个进程,同样也可以代表一个Dalvik VM实例,它们各自使用着自身独有的内存地址,普通情况下两个进程间之间是没办法进行数据交流的,但是Google在堵死一扇门的同时也留给我们一扇窗,就像上面这张图一样,虽然我将每个内存之间都是单独分割开了,看起来各不相关,但是我还是采用了一条虚线将它们连接了起来,那是因为 Android 系统中支持我们以一种比较 “绕” 的方式来实现进程间的数据通信。
说到 “绕” 我们首先会想到的一定会是 共享内存 了,比如File或者SD卡中创建SQLite等等方式,利用外部存储技术,实现数据通信。但是这里我们先不说通过共享内存来实现进程通信是否方便,而是因为这种方式那怕在同进程中使用其实也会造成几个不可避免的影响,如下:
所以为了解决这些问题,系统还提供了其他的IPC通信方式让我们选择,目前我了解到的方式大致有:
上面几种方法,有些是比较容易理解的,如,消息队列,共享内存,网络Socket这几个,而且它们其实在Linux c中就已经存在了,算是元老级别的技术了,而Bundle其实就是利用Intent意图,在完成跳转界面时候携带参数实现数据的传递。这里我们需要特别注意的应该是Binder机制,它相对而言算是Android中一个比较核心,同时也是一个比较难以理解的技术点了,如果有机会,我争取单独去写一遍关于它的博客,这里我只是简单的去概括一下它在Android中的作用。
Binder是一个实现了 IBinder 接口的类。而IBinder接口定义了与远程对象的交互协议。通常在进行跨进程通信时,不需要去专门实例化IBinder接口,直接从Binder派生即可。
由于跨进程通信是需要内核空间支持的,在Android系统中,这个运行在内核空间,负责各个进程进行通信的模块就是Binder类,所以我们可以把它简单理解为一种“虚拟”的物理设备,可以当成是一个驱动,或者是一个桥梁。它是ServiceManger连接各种Manager,如ActivityManager,WindowManager等等管理类的主要通道。
它涉及到了四大组件的启动与传值的整个流程,比如当我们使用Service时,Binder同样可以理解为是客户端和服务端进行通信的媒介,当bindservice的时候,服务端就会通过onBind()方法返回一个Binder对象,客户端就可以通过它去获取服务端提供的方法和数据了,这里的服务包括普通服务和基于AIDL的服务。
因此AIDL其实也是一种IPC的通信机制。主要用于进程间的通信,下面我将会带着大家一步一步实现一个通过AIDL技术来达到两个进程间通信的Demo。
闲语:本来我写博客不是很喜欢粘贴图片的,我觉得这样会导致映入眼帘的都是一堆五颜六色的代码截图,看起来很是混乱,至少我不是很喜欢这样风格的博客。但是由于在使用AIDL时,我发现它对于文件的目录层级结构要求极其严格,所以这里为了能够让大家觉得更加清晰,我不得不多放几张文件的路径截图,这里面就不可避免的可能会包含一些代码的截图,从而会造成博客中的代码是有所缺失的,但是大家不用担心,在博客的末尾我会将完整的demo放入链接中的,供大家下载验证。
大致了解了一下什么叫做IPC,下面我们就要进入这篇博客的主题了,但是在说我对AIDL的理解之前,还是想先放一张图上来。(老弟,把刀先放下,别急啊,心急吃不了热西施不是)
这张图是什么呢,其实就是官网针对AIDL的解释,各位看官千万不要跟我以前一样傻傻的认为官方文档没有用,或者它是英文的原因就不想去看它,我这里要郑重的告诫大家一下,这文档,有时间了一定要看,毕竟这里才是Android最权威的东西。
那上面是什么意思呢,其实我也是看不懂的!(笑哭,小老弟,你咋又去提你的刀了,放下,放下,我这话不是没说完呢),但是看不懂,不怕啊,咱可以利用有道词典进行翻译,只有这样对照着看,印象才会更加深刻一些,下面我会上面的一些翻译,同时也加上自己的理解,概述一下AIDL的定义。
AIDL 是 Android interface Definition Language 的简称,是一种针对与 Android 平台设计的一种 IDL 语言,被称为 Android开发接口定义语言 ,在进行 Android 开发时,可以利用它定义多进程之间相互通信时都认可的编程接口。因为在 Android 中,一个进程通常无法访问另一个进程的内存,如果想要传递数据,那么就需要将想要传输的对象分解成操作系统能够认识的特定规格的形式才可以传输,但是由于编写这一组操作需要涉及到Binder机制,相较于技术层面来说是比较底层部分的,如果是让Android开发人员编写这一组操作是比较繁琐的工作,所以为了简化 开发人员的工作,Goolge就提供了一种语言规范接口,它更像是一种模板,其中定义了两个进程间调用方法的规范,只要我们按照特定的方式书写 .aidl文件,然后执行 sdk\build-tools\verson\aidl.exe, 就会由此自动生成一个 Interface.java 的实例代码,而在以后通信的过程中这些 aidl 文件将不在起作用,所有的代码逻辑其实都是Interface.java中实现。
注:如果跨进程通信是在同步情况的下,还是建议使用Messager来实现,但由于Messenger是通过hander 和 message 实现的,内部存在一个MessagerQueue队列,处理消息时有这一定的顺序延迟,所以在多线程并发的情况下就不适用了,这时才会考虑支持多线程,多并发的AIDL,所以因为是适用于多线程的情况,AIDL 接口的实现必须是完全线程安全。而其中涉及到一个oneway关键字用于修改远程调用的行为。使用该关键字时,远程调用不会阻塞;
在默认情况下,AIDL支持一些数据类型,在使用这些数据类型的时候并不需要特意的去进行导包操作,同时也不需要去关注它的定向TAG(这个我会放在下面去讲解,就是一种数据流向的识别标签),这里我给大家罗列一下:
八种基本数据类型:byte、char、short、int、long、float、double、boolean
String,CharSequence
List
List 内部承载的数据必须是AIDL支持的类型,或者是其它声明过的对象。可选择将 List 用作“通用”类(例如,List)。另一端实际接收的具体类始终是 ArrayList,但生成的方法使用的是 List 接口。
Map
Map 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或声明过的可打包类型。 不支持通用 Map(如 Map
但是这里有一个坑需要大家特别注意一下,虽然官网上面在支持类型时说的是 :“All primitive types in the Java programming language (such as int, long, char, boolean, and so on)”,但是这里不得不提醒大家一句,在使用过程中,BaseTypes 中 short 类明显是编译不通过的,如果存在 short 类型的参数,在编译时就会抛出 unknown type 的错误,而原因是 AIDL 在传输数据时需要使用到序列化和 IO 操作实现数据流的读写,但是 short 类型明显不支持读写操作,所以就没有办法使用了。
除了上面列出的类型之外,其他的各种自定义类型,都需要继承 Parcelable 接口,这里的支持 Parcelable 很重要,Android系统可以通过它将对象分解成可编组到各个进程之间的流对象。Parcelable 也是一种序列化的手段,跟 Serializable 类似,但是由于 Serializable 是 Java 中的序列化接口,在 Java 可能很适用,使用也简单,但是由于它的开销很大,在序列化时会产生大量的临时文件,从而导致频繁的GC,所以在 Android 中就重新编写了一种名为 Parcelable 的方式来完成序列化和反序列化过程中需要大量的I/O操作,它更适用于 Android 系统,使用时可能会麻烦许多,但是它的效率很高,序列化的对象字节序列会直接保存到本地文件中,实现永久化。
如需创建支持 Parcelable 协议的类,您必须执行以下操作:
而如果是在传输集成了 Parcelable 接口的封装对象数据时,我们还需要利用 import 进行导包的动作,那怕它和使用它的类存放在同一包下,下面在写代码时,我会给大家详细演示一遍。
编写环境 : Android studio 3.4 Android Gradle Plugin Version 3.0.0 Gradle Version 4.1
首先我们需要在 main 文件下面和 java 目录同级的地方创建一个 aidl 文件夹,然后在其下创建我们需要的 AIDL 接口文件。但是由于我这里使用的AS,它支持直接创建以 .aidl 为后缀名的文件,所以我这里直接右击 main 目录,就可以创建了。
是不是很方便啊,我发现最近几个版本的Android studio比之前的已经稳定很多了,至少给了我更新到最新版本的动力了,哈哈,废话不多说啊,我们先说说aidl文件。
关于 .aidl 文件,我习惯按照它们不同的功能将其区分为两种类型,虽然都是以后缀名 .aidl 结尾,但它们的内部形式和起到的作用是完全不同的。
定义对象
: 这种 .adil 文件主要是在传递那些实现了 Parcelable 的自定义封装对象时才会编写,它主要起到了一个声明对象的作用,因为在 AIDL 中只有通过了 parcelable(注意是小写) + 类名 声明过的非默认对象才能通过 aidl.exe 的编译。
定义接口
: 这种 .adil 文件主要是用来声明服务接口的,也就是在跨进程通信时,服务端可以提供哪些方法,需要注意的是:
1:方法可携带零个或多个参数,同时也可以有返回值或者没有。
2:所有非默认支持的参数类型,都需要添加表明数据流向的方向标记,也就是 定向TAG,可以是in 、out或者inout。
3:只支持方法,不能公开AIDL中的静态字段。
这里我创建一个名为 aidlservice 的项目,包名就叫做 yya.godinsec.service ,同时我想在进程间传输一个名为 Computer 的自定义类型。毕竟传输非默认支持数据类型的情况相较于默认支持类型肯定会复杂些,所以这里我们直接演示复杂的。如果大家复杂都能够理解了,那么简单的就自然而然的理解了。
接着利用AS的选择项直接创建一个 Computer.aidl,由于是非默认支持类型,需要利用 parcelable 关键字先进行类型的声明。
路径是在 main 下面,与 java 同级目录,而我们看到的 yya.godinsec.service 这级目录其实是创建时AS就帮我们自动添加的,只有最后一个 entitybean 是后来创建的,然后我将 Computer.aidl 文件移到了里面,这里需要提醒大家一句的是,由于 Computer.aidl 的路径已经确定,那么在接下来需要创建 Computer.java (映射文件)的路径也就被限定了,因为在 AIDL 使用规则中,entitybean.adil 和 entitybean.java 必须分别放在 aidl 和 java 下同样的路径中,这样才能够在编译时相互映射。
接着就是需要创建定义服务接口的 .aidl类 了,我这里命名为 IComputerInterface.aidl,并在其中添加了三个方法,尽量的做到简洁易懂,一个添加电脑的方法,一个更改价格的方法,同时还有一个获取所有电脑集合的方法。
上面用红色框框标注的地方,就是层级目录,希望给大家一个直观的印象,IComputerInterface.aidl 我并没有将它跟 Computer.aidl 放到一个包名下面,所以有些童鞋会认为我利用import进行导包动作很正常,但是需要注意的是,那怕我将它们两个放在一个目录下,其实这一步仍然没办法省略,而且如果细心的同学还可以看到我在 AddComputer(out Computer computer) 添加了一个 out 的 定向TAG。所以我们得到一个结论:
如果在使用 AIDL 时,需要传递非默认的数据类型作为参数时,最少要有两个 aidl 文件,一个是要利用 parcelable 关键字,进行类型声明的,另外就是一个接口类,用来定义接口服务的,而且在定义接口服务的 .aidl 文件中,如果在方法参数中使用到了非默认类型作为参数,那么无论如何都需要通过 import 进行导包的动作,同时一定要显示添加定向TAG做修饰,否则就会报错,我这里选用了 out 。
IComputerInterface.aidl 它看起来像是一个接口,起到了一种规范服务端的行为动作的作用,但是在 AIDL 中,它仅仅是给 aidl.exe 程序提供了一个用来生成名为 IComputerInterface.java 接口类的模板而已。
不信?那我们点击一下Rebuild试试,额,报错了,说是找不到 Computer.java 类,好吧,是忘了这一步了。那我们就赶紧把 Computer.java 先给创建出来。
前面我们已经创建了 Computer.aidl 文件,它的路径为 yya.godinsec.service.entitybean,刚刚有说到过,这个路径其实已经是被限定了,它限定的就是 entitybean.java 文件的路径,我们只能在 java 下面的同级目录下创建一个 Computer.java 对象,而且由于它是非默认支持类型,必须实现 Parcelable 接口,我用的是AS,它内部直接提供了 Parcelable 的模板,所以当我创建好 Computer.java 后直接实现 Parcelable 接口,它会报错,然后按照AS的要求点击,就可以很快捷的创建出来一个 Parcelable 的实体类了。
public class Computer implements Parcelable {
private String name; //电脑的名字
private int price; //电脑的价格
// public Computer() {
// }
public Computer(String name, int price) {
this.name = name;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public Computer(Parcel in) {
name = in.readString();
price = in.readInt();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
dest.writeInt(price);
}
// public void readFromParcel(Parcel dest) {
// //读值顺序一定要和writeToParcel()方法中一致的,否则在序列化的时候会出错
// name = dest.readString();
// price = dest.readInt();
// }
@Override
public int describeContents() {
return 0;
}
public static final Creator<Computer> CREATOR = new Creator<Computer>() {
@Override
public Computer createFromParcel(Parcel in) {
return new Computer(in);
}
@Override
public Computer[] newArray(int size) {
return new Computer[size];
}
};
@Override
public String toString() {
return "Computer{" +
"name='" + name + '\'' +
", price=" + price +
'}';
}
}
上面的代码就是就是一个实现了 Parcelable 的 Computer.java 了,路径为 yya.godinsec.service.entitybean ,和 Computer.aidl 保持一致。
但是在代码中,我有将两个方法给注释掉了,一个是 Computer() 无参构造 ,还有一个就是 readFromParcel() ,这里主要是想讲一下我在说 Parcelable 时的坑了 。
单独注释掉,那是因为在通过AS模板自动生成的代码中并不包含这两个方法,这里特意注释掉只是希望给大家留个更深刻的印象,主要是想告诉大家如果以后有机会使用AIDL时,最好添加加上这两个方法,注意啊,不是一定要添加,而是当你将 定向TAG定义为 out 或者 inout 时,才必须添加,原因就是在利用AS生成的代码下,它只默认支持 定向TAG为in 的情况,还记得我上面在 AddComputer(out Computer computer) 中,我特意将 定向TAG定义为out 类型了吧,如果在注释掉的情况下我们点击 Rebuild Project ,就会获取一个大大 Error。说是在执行 aidl.exe 的时候因为获取了一个非零返回值而导致强制退出,原因就显示在下图中:
无参那个我们就略过了,添加上去就好,但是这个 readFromParcel() 是什么?为什么我把 定向TAG 设置为 in 的时候就没报错呢?这是给大家解释一下:readFromParcel() 其实就是一个跟 writeToParcel 方法恰好相反的方法,从词意我们就可以猜测出它们各自的意思,一个是写入的作用,另外一个就是读取的作用了。简单来说就是一个序列化和反序列化的过程。
而为什么在 out 的时候就必须需要添加这个方法呢?这个问题我放到下面第四节详细讲解 定向TAG 的时候再给大家好好分析一下。Computer.java 完整的代码就是将上面注释的方法都放开之后的了。
这是一个小知识点,但是有的时候确实会弄的很多人无厘头。这些IDL类型的语言虽然是为了简化程序员的工作的,确是需要我们严格按照它的规定来书写,灵活性小很多,也算是各有利弊了吧。
前面两步,说起来很是麻烦,但其实也就是一个定义传输类型,定义服务接口的步骤而已,这时让我们重新点击 Rebuild Project 按钮,这是就可以很顺利的编译通过了。接下来AS就会自动去调用 aidl.exe ,并且几乎是立刻就在 aidlservice\build\generated\source\aidl\debug 目录下面生成一个 IComputerInterface.java 的文件了。它就是我们以后编写代码时会用到的对象了,而前面编写的那两个.aidl文件主要就是为了生成它的,这时我们甚至可以 “大言不惭” 的说一句不在需要那两个 .aidl 文件了(当然,打个比喻而已,笑哭)。
这个文件中的代码在初次看到时估计会很是头疼的,确实很乱,也没有排版,我们先不去深究内部的具体含义,这里我们只需要把它当成是一个接口使用就好,接口回调 想必大家都会用吧,没错,那你现在就把它当成里面的那个接口就好了,这是我当时粗浅理解 AIDL 时的想法,虽然不正确,但有助于我们去理解它的整体流程,内部包含了我们在IComputerInterface.aidl 中定义的三个方法就好。如下:
package yya.godinsec.service;
public interface IComputerInterface extends android.os.IInterface {
public static abstract class Stub extends android.os.Binder implements yya.godinsec.service.IComputerInterface{
//扩展方法***********
}
public java.util.List<Computer> getComputers() ;
public void AddComputer(Computer computer) ;
public void setComputerPrice(String name, int price) ;
}
我将所有扩展类的代码都删除后,只留下上面的这一点点代码后,各位是不是有些神经气爽的感觉,它就是一个 interface 中包含了一个继承自 Binder 的抽象扩展类Stub,并且实现了当前 interface ,由于是抽象的,所以那怕没有强制重写三方法,但是它内部仍然存在我们在IComputerInterface.adil 中定义的三个方法的引用。由于Binder可以用于服务之间的传输,所以在我们编写Service的时候可以将这个Stub存根类的实例传递出去,用于调用下方的三个方法。这样看来是不是觉得也很简单了。
下面就到了正式编写服务器代码的时候了。这里会涉及到一些Service的知识点,如果有同学对这块都不熟悉的话可以先去巩固一下Service的知识,再返回看这篇博客,这里我就直接放代码了啊。
public class ComputerService extends Service {
//一个存放电脑的集合
private List<Computer> mComputers;
//返回Stub 因为Stub继承了Binder,而Binder实现了IBinder接口
@Override
public IBinder onBind(Intent intent) {
LogUtils.e("远程服务开始绑定");
return mStub;
}
@Override
public void onCreate() {
super.onCreate();
LogUtils.e("启动远程服务");
mComputers = new ArrayList<>();
}
private final IComputerInterface.Stub mStub = new IComputerInterface.Stub() {
@Override
public List<Computer> getComputers() throws RemoteException {
return mComputers;
}
@Override
public void AddComputer(Computer computer) throws RemoteException {
if (mComputers == null) return;
mComputers.add(computer);
LogUtils.e("添加了一个电脑 "+computer.toString()+" 到集合中");
}
@Override
public void setComputerPrice(String name, int price) throws RemoteException {
if (mComputers == null) return;
Iterator<Computer> iterator = mComputers.iterator();
if (iterator.hasNext()) {
Computer computer = iterator.next();
if (name.equals(computer.getName())) {
computer.setPrice(price);
}
LogUtils.e("更改后的电脑价格为 :" + computer.toString());
}
}
};
@Override
public boolean onUnbind(Intent intent) {
LogUtils.e("解除绑定远程服务");
return super.onUnbind(intent);
}
}
这里就是写了一个 Service,在 onCreate 时实例化集合,由于在 onBind() 方法中需要回传一个 IBinder 对象,由于上面的那个 IComputerInterface.java 中正好有一个叫做 Stub 的类是继承了 Binder 类的,而 Binder 是实现了 IBinder 对象的,所以我们这里其实可以将 Stub 的实例传递出去。至于其中底层的逻辑解析,我会放到下一遍博客再详细讲解一下,大家不用太过疑惑,这篇博客的主要目的是教会大家如何使用,和使用的时候需要注意那些内容就好了,要不然篇幅太长了。
我们继续看上面的代码,主要是一个利用匿名内部类的形式,实例化了一个 IComputerInterface.Stub 的抽象类对象,并且重写了我们在其中声明的所有方法,并在其中完成主要逻辑,我们一般把它叫做远程接口。这也是一个公开方法的行为,以便客户端进行绑定后,可以进行远程调用。至于服务器里面的具体代码逻辑我就不再一一描述了,就是很简单的一些操作,最后在在编写客户端的代码之前,一定要把当前服务在 AndroidMinafest.xml 中注册哦,要不然不可能会交互成功的。
匿名内部类:它其实是一个实现了接口或者抽象类的具体类,可以把它看成是一个没有名字,在表现形式上借用了父类(也就是接口或者抽象类的)的子类对象,它是没有构造方法的,所以需要显式调用父类的构造方法,同时具体实现抽象方法。
<service
android:name=".ComputerService"
android:exported="true">
<intent-filter>
<action android:name="yya.godinsec.service.ComputerService" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
在写客户端代码之前,需要先回忆一下在同进程使用Service时,使用bindService方法来连接服务的情景,这里跟它几乎一致,唯一不同的是由于跨进程的,客户端其实并没有这个服务,所以这里需要通过隐式意图来启动它。
//由于是跨进程服务,所以需要隐私意图绑定服务
private void attachRemoteService() {
Intent intent = new Intent();
//设置Action,在ServiceManifest定义好的
intent.setAction("yya.godinsec.service.ComputerService");
//在5.0以上不设置包名会导致找不到Service
intent.setPackage("yya.godinsec.service");
//通过BindService方式启动AIDL , 自动连接
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
这时,如果我们通过 bindService 启动服务器了服务器,在非跨进程时 onServiceConnected 回调中的 onBind() 方法会返回一个 mBinder 实例,但是在这里其实是获取不到的,因为是跨进程的,直接获取是获取不到的,这里需要一个了 AIDL 很重要的步骤了,那就是 迁移代码 了,对,你没有看错,就是 迁移代码 。我们需要把服务端第一步和第二步编写的文件全部拷贝过来,其中包括两个 .aidl 文件,还有一个 .java 文件,在这次的 Demo 中也就是 Computer.aidl 、IComputerInterface.aidl 、Computer.java 这三个文件,主要注意的是这三个文件路径也就是全类名,一定要跟服务端的保持一致,否则就会导致无法传输数据。
红框部分就是这三个文件分别在 clien t和 service 的层级目录了,可以看到的是,它们两个完全一致。当你将文件拷贝过来后,Rebuild 一下,这样我们在 client 项目也会得到一个 IComputerInterface.java 实例了,它就是为客户端提供对 AIDL 方法访问权限的副本了。
这里我还要提到一个小知识点,那就是按照上面的写法,在迁移代码时需要拷贝两个文件过去,分别是整个AIDL文件夹和 java 下面的 Computer.java 文件,这样是比较麻烦的,其实还有一种比较快捷的方法,那就是将 Computer.java 和 Computer. 方法一起,反正它们要求全路径保持一致,那么我们就将它都放在 aidl 下面同一个包下面不就好了,比如这样:
当我们再次需要迁移代码时,这时只需要将整个AIDL文件拷贝过去就好了。但是这是如果我们编译是会报错的,说是找不到映射文件,这里则是因为在 Android studio 中 的 Gradle 默认是在 java 下面寻找资源的,所以这里就需要我们在 App Gradle 的 Android 里面加入下面的代码指定在 java 和 aidl 下面找文件就好了。
//默认的一些相关文件
sourceSets {
main {
java.srcDirs = ['src/main/java', 'src/main/aidl']
//设置Gradle 检查资源的时候 分别从 java 和 aidl下面寻找
}
}
好了,代码也迁移成功了,这是我们就可以编写完整的客户端代码了。这里是在Activity中启动了服务
public class ComputerClient extends Activity {
//服务接口
private IComputerInterface mIComputerInterface;
//链接中
private boolean isLinking = false;
//电脑编号,初始化为1
private int ComputerNumber = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_computer_client);
}
//由于是跨进程服务,所以需要隐私意图绑定服务
private void attachRemoteService() {
Intent intent = new Intent();
//设置Action,在ServiceManifest定义好的
intent.setAction("yya.godinsec.service.ComputerService");
//在高版本不设置包名会导致找不到Service
intent.setPackage("yya.godinsec.service");
//通过BindService方式启动AIDL , 自动连接
if (mServiceConnection != null && !isLinking) {
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
}
//bindService时一个传输媒介,主要是为了可以获取到IBinder对象
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
//提供的一个解析方法,主要是判断是否是跨进程,如果是跨进程,返回的是一个代理对象,如果不是,则返回的是一个实体对象
//如果是在同一个进程间通信,那么就会直接返回.java接口对象出去
//如果是不同进程间通信,那么就需用通过IPC机制来实现通信了。
mIComputerInterface = IComputerInterface.Stub.asInterface(iBinder);
if (mIComputerInterface == null) return;
LogUtils.e("远程服务绑定成功");
isLinking = true;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
isLinking = false;
}
};
public void onClick(View view) {
if (mServiceConnection == null) return;
try {
switch (view.getId()) {
case R.id.button1:
Computer computer = new Computer("电脑 编号:" + ComputerNumber, 10000);
mIComputerInterface.AddComputer(computer);
ComputerNumber++;
break;
case R.id.button2:
mIComputerInterface.setComputerPrice("电脑 编号:1", 20000);
break;
case R.id.button3:
//通过接口对象,调用方法,获取到想要的数据
List<Computer> computers = mIComputerInterface.getComputers();
if (computers != null) {
for (Computer computerss : computers) {
LogUtils.e(computerss.toString());
}
}
break;
case R.id.button4:
if (!isLinking) {
attachRemoteService();
}
break;
case R.id.button5:
if (mServiceConnection != null) {
unbindService(mServiceConnection);
}
break;
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
在Activity中我添加了五个按钮 分别控制了 绑定服务,解除服务,添加电脑,更改价格,和获取电脑总集合五个功能。
至此,我们服务端和客户端的所有代码都书写完成了,这时就可以让客户端和服务端愉快的进行深入的交流了。
运行完成服务端代码 和 客户端代码之后,我就直接点击了客户端上面的绑定按钮了
这是服务端代码打印日志,说明服务器启动成功,并且服务端的进程ID为 1062 ,但是这样并不能说明绑定是否成功,我们还需要切到客户端进程上,看打印结果。
这就是客户端的打印结果了,说明我们绑定成功,这时,我们就可以给服务器中的集合添加电脑了。我这里点击五次 add 方法。服务器打印结果如下:
哎呦,我去,我明明在添加的时候给它实例化了一个Computer对象,并且传递过来啊,为什么这里的name 和 price都为没有复制的情况呢?这里就需要我们思考一下了,绑定也成功了,方法也调用了,甚至是对象都添加过来了,不信我们点击一下,查看所有集合的按钮。这里需要切换到客户端查看日志。
是吧,获取到的集合中已经有五个Computer的对象了,但是跟在服务端打印的一样,内部的值都没有赋值成功。大家可能有些疑惑了,其实这就涉及到了 定向TAG 的原因了,具体的我们下面单独讲解,这里我们先把 IComputerInterface.adil 中的 add 方法改为 AddComputer(in Computer computer) ,然后重新运行两个项目,然后绑定后点击五次添加按钮后,在点击一次更改价格的方法,这时服务器的日志为:
看到了吧,是不是赋值成功了,并且将其中name为 1 的 电脑价格更改为了 2000 ,接着我们就可以获取所有电脑了。
一个大大的beautiful,我们完美的获取了所有的集合,这也证明我们这次的跨进程通信的实现了。哈哈哈,会不会很开心!
不得不承认这是一篇很长的博客,但是我想在下一篇中只是设计到 AIDL 底层实现的解析,所以我不得不讲比较细小的知识点放到这篇博客中给大家都说出来,定向 TAG 其实在文章的开头就出现过,但是由于它需要等我们完全熟练了 AIDL 的用法之后通过实际演示的方法才能给大家一个清晰的认知,所以我将它放到了文章的结尾,它同样是一个小知识点,甚至我们可以所有的方法参数都给它定义为 inout 类型的,但是这加大了应用在序列化时的负担,是一种很不成熟的解决方法,还是需要认清楚它们之间的差异,然后合理的运用它们,这里我先给大家一个结论,然后再通过代码去证实它的正确性。
在早期我的理解中,定向TAG 表示了跨进程通信中数据的流向限制,其中 in 表示数据只能从客户端流向服务端,out 表示数据只能从服务端流向客户端,而 inout 是可在两端双向流通。可是在实际使用中我发现并不是这么简单的一回事,因为在官方文档中就提到过,默认参数的 定向TAG 默认且只能是 in。 要是用刚刚那种粗浅的理解就有些勉强了。后来我经过测试得到以下三个结论:
1 :当 TAG为 in 时,表示客户端可以将数据的具体值传输给服务端,但是服务端对这个值更改后,客户端不会因为对这个值的改变而发生改变。
2:当 TAG为 in 时,表示客户端发送给服务端这个值时,可以发送,但是服务端接受到的只是它的一个实例,内部所有的属性值都为空的,但是如果服务端更改后,客户端可以知道修改后的内容,这一点恰恰也可以印证我们刚刚 tag 为 out 时传递的 Computer 的 name 和 price 都未赋值成功的原因。
3 :当 TAG为 inout 时,就是上面两种情况的结合了。
下面我带大家通过一个案列去验证上面的结论,我将刚刚的demo更改了一下,IComputerInterface.aidl 变成了这样的
package yya.godinsec.service;
// Declare any non-default types here with import statements
// 这是我们利用AS自动生成了一个AIDL文件自带的注释,意思就是提醒我们使用import语句在这里声明任何非默认类型
import yya.godinsec.service.entitybean.Computer;
interface IComputerInterface {
List<Computer> getComputers();
//在方法中如果需要传参,非默认支持类型都需要添加定向TAG in
void AddComputerIN(in Computer computer);
//在方法中如果需要传参,非默认支持类型都需要添加定向TAG out
void AddComputerOUT(out Computer computer);
//在方法中如果需要传参,非默认支持类型都需要添加定向TAG inout
void AddComputerINOUT(inout Computer computer);
}
列举了三个不同TAG的方法,然后界面变为了三个不同的添加方法的按钮。服务器端代码更改为
public class ComputerService extends Service {
//一个存放电脑的集合
private List<Computer> mComputers;
//返回Stub 因为Stub继承了Binder,而Binder实现了IBinder接口
@Override
public IBinder onBind(Intent intent) {
LogUtils.e("远程服务开始绑定");
return mStub;
}
@Override
public void onCreate() {
super.onCreate();
LogUtils.e("启动远程服务");
mComputers = new ArrayList<>();
}
private final IComputerInterface.Stub mStub = new IComputerInterface.Stub() {
@Override
public List<Computer> getComputers() throws RemoteException {
return mComputers;
}
@Override
public Computer AddComputerIN(Computer computer) throws RemoteException {
if (mComputers == null) return null;
LogUtils.e("添加了一个电脑 " + computer.toString() + " 到集合中");
//修改传递过来的值,之后再将它添加到集合中去
computer.setName("我是in传递的过来的");
mComputers.add(computer);
return computer;
}
@Override
public Computer AddComputerOUT(Computer computer) throws RemoteException {
if (mComputers == null) return null;
LogUtils.e("添加了一个电脑 " + computer.toString() + " 到集合中");
//修改传递过来的值,之后再将它添加到集合中去
computer.setName("我是out传递的过来的");
mComputers.add(computer);
return computer;
}
@Override
public Computer AddComputerINOUT(Computer computer) throws RemoteException {
if (mComputers == null) return null;
LogUtils.e("添加了一个电脑 " + computer.toString() + " 到集合中");
//修改传递过来的值,之后再将它添加到集合中去
computer.setName("我是inout传递的过来的");
mComputers.add(computer);
return computer;
}
};
@Override
public boolean onUnbind(Intent intent) {
LogUtils.e("解除绑定远程服务");
return super.onUnbind(intent);
}
}
逻辑依然很简单,就是复写声明好的那四个方法,然后将客户端传递过来的数据信息分别打印出来,但是在添加集合之前我会先把传递过来的 name 字段给更改为各自的方法名称,同时将更改过后的数据再次返回给客户端。这样我就可以检测到客户端传递过来的数据了,同时,也可以在客户端检测到回传回去的数据了。
客户端代码
public class ComputerClient extends Activity {
//服务接口
private IComputerInterface mIComputerInterface;
//链接中
private boolean isLinking = false;
//电脑编号,初始化为1
private int ComputerNumber = 1;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_computer_client);
}
//由于是跨进程服务,所以需要隐私意图绑定服务
private void attachRemoteService() {
Intent intent = new Intent();
//设置Action,在ServiceManifest定义好的
intent.setAction("yya.godinsec.service.ComputerService");
//在高版本不设置包名会导致找不到Service
intent.setPackage("yya.godinsec.service");
//通过BindService方式启动AIDL , 自动连接
if (mServiceConnection != null && !isLinking) {
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
}
//bindService时一个传输媒介,主要是为了可以获取到IBinder对象
private final ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
//提供的一个解析方法,主要是判断是否是跨进程,如果是跨进程,返回的是一个代理对象,如果不是,则返回的是一个实体对象
//如果是在同一个进程间通信,那么就会直接返回.java接口对象出去
//如果是不同进程间通信,那么就需用通过IPC机制来实现通信了。
mIComputerInterface = IComputerInterface.Stub.asInterface(iBinder);
if (mIComputerInterface == null) return;
LogUtils.e("远程服务绑定成功");
isLinking = true;
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
isLinking = false;
}
};
public void onClick(View view) {
if (mServiceConnection == null) return;
Computer computer = null;
try {
switch (view.getId()) {
case R.id.button1:
computer = new Computer("电脑 编号:" + ComputerNumber, 10000);
Computer computer1 = mIComputerInterface.AddComputerIN(computer);
LogUtils.e("AddComputerIN = "+computer1.toString());
break;
case R.id.button2:
computer = new Computer("电脑 编号:" + ComputerNumber, 10000);
Computer computer2 = mIComputerInterface.AddComputerOUT(computer);
LogUtils.e("AddComputerIN = "+computer2.toString());
break;
case R.id.button3:
computer = new Computer("电脑 编号:" + ComputerNumber, 10000);
Computer computer3 = mIComputerInterface.AddComputerINOUT(computer);
LogUtils.e("AddComputerIN = "+computer3.toString());
break;
case R.id.button4:
//通过接口对象,调用方法,获取到想要的数据
List<Computer> computers = mIComputerInterface.getComputers();
if (computers != null) {
for (Computer computerss : computers) {
LogUtils.e(computerss.toString());
}
}
break;
case R.id.button5:
if (!isLinking) {
attachRemoteService();
}
break;
case R.id.button6:
if (mServiceConnection != null) {
unbindService(mServiceConnection);
}
break;
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
利用三个按钮,分别去调用AddComputerIN(),AddComputerOUT(),AddComputerINOUT(),方法,并传递相同的值,同时将返回的值打印出来,不在描述了,流程都很简单,主要是看下面的执行日志。
这里是我们分别调用上面三个方法后,Service打印的日志。可以清晰的看到,当定向TAG为in 时,服务端,接收到了完整的数据,当定向TAG为 out 时,内部属性值全未赋值,而当定向TAG为 inout 时跟in类似。这跟我们上面提到的结论完全一致,那么我们看看客户端的日志又是如何的呢!
和我们上面的结论完全一致,想必到了现在大家对这个TAG将会有一个完整的认识了吧。
好了,这篇博客也就到这里结束了,辛辛苦苦码了将近一天的字,补充自己的同时也希望大伙给个赞啊。
下一篇文章我会好好分析一下AIDL生成的那个java接口中是如果实现数据跨进程通信的。
最后放上这篇文章的Demo代码