AIDL使用及原理分析+观察者设计模式
概述
我们应该知道,Android为了进程安全,采取了进程隔离的机制,即为每个进程分配独立虚拟地址空间。也就是说进程间无法直接互相访问对方的数据,但是进程间通信是有需求的,也就是 IPC。所以 Android就提供了接口来实现 IPC,此外还提供了 AIDL(Android接口定义语言)给我们去定义接口。
AIDL概念性的东西这里就不解释了。这次我们将讲一下 AIDL的基本使用以及分析一下它的基本原理,然后在此基础上,结合观察者设计模式去实现一个类似于微信公众号订阅的、基于 AIDL的CS(客户端-服务端)系统。
一、AIDL基本使用
- 服务端
创建一个基于 AIDL的CS端的第一步就是,我们先去创建一个在客户端和服务端之间传递的对象的类。并且由于涉及 IPC,所以这个类要实现可序列化接口。这里我们就把这个类名定为 Hero。因为笔者比较看好《复仇者联盟》,所以这里我们创建的服务端就相当于复仇者联盟,里面保存着各英雄的列表(钢铁侠、美国队长、、、)。然后客户端这边则可以通过 IPC向服务端添加英雄信息,也可以查询英雄列表。
我们先创建一个服务端应用,这里的包名定为:com.ethan.aidlservice。然后下面我们再创建这个在客户端和服务端之间传递的对象的类:
// 注释 1,这里的包名一定要和 Hero.aidl 文件的包名相同 !
package com.ethan.aidlservice.aidl;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
/****************
* 一个类实现 Parcelable接口,就需要重写两个方法:writeToParcel(Parcel out, int i)(序列化) 、describeContents()
* 需定义 CREATOR 变量并赋值,反序列化过程由该实例实现。
* 此外还需一个带 Parcel形参的构造器,以实现 CREATOR 内部的反序列化对象的创建
* ********************/
// 实现 Parcelable可序列换接口
public class Hero implements Parcelable {
// 真名
private String realName;
// 昵称
private String nickName;
// 英雄类型
private String heroType;
public Hero() {
}
public Hero(String realName, String nickName, String heroType) {
this.realName = realName;
this.nickName = nickName;
this.heroType = heroType;
}
private Hero(Parcel in) {
readFromParcel(in);
}
private void readFromParcel(Parcel in) {
this.realName = in.readString();
this.nickName = in.readString();
this.heroType = in.readString();
}
/**
* 实例化静态内部对象CREATOR实现接口Parcelable.Creator
* 这是用于将对象反序列化的(读取)
*/
public static final Creator<Hero> CREATOR = new Creator<Hero>() {
@Override
public Hero createFromParcel(Parcel in) {
return new Hero(in);
}
@Override
public Hero[] newArray(int i) {
return new Hero[i];
}
};
/**
* 重写writeToParcel方法,将 Hero对象序列化为一个Parcel对象
* 这是用于将对象序列化的(写入)
*
* @param out 输出
* @param i
*/
@Override
public void writeToParcel(Parcel out, int i) {
out.writeString(realName);
out.writeString(nickName);
out.writeString(heroType);
}
/**
* 重写describeContents方法,内容接口描述,默认返回0就可以
*
* @return
*/
@Override
public int describeContents() {
return 0;
}
@NonNull
@Override
public String toString() {
return String.format("[%s , %s , %s ]\n", realName, nickName, heroType);
}
}
上面就是一个用于保存英雄信息的 javabean类,这个类的创建有3点需要注意的地方,下面逐一点出来:
(1)上面注释 1处包名的地方,这个类的包名一定要和 Hero.aidl 文件的包名相同 !Hero.aidl 是后话,待会儿会讲。
(2)这个 Hero.java 类要实现可序列化接口 Parcelable(因为涉及到 IPC)
(3)这个类要实现两个方法 writeToParcel、describeContents以及要创建一个 Creator对象(变量名是 CREATOR )。这些要实现的方法或者对象用于序列化或反序列化,具体作用上面方法注释有说明。
然后上面我们就要定义IPC数据交互的接口了,那就是创建 AIDL文件。为什么说是定义接口而不是创建接口呢?因为我们 AIDL文件里面的内容其实并不是一个真正用于数据交互的接口,AIDL本身是接口定义语言,是官方提供给我们定义接口的。在工程编译构建的时候,系统会根据我们在 AIDL文件里的定义去生成真正的 java类接口。
下面我们来创建这个 ADIL接口。右键点击工程里的app文件夹处,点 new -> AIDL -> AIDL file ,填一下接口名称点确定就可以了。然后我们会发现与 java文件夹同级的目录就多出了一个 aidl目录,因为刚才我们创建的 Hero .java类放在一个叫aidl的包里,所以这里aidl目录下我们再创建一个 aidl文件夹与之同步。
下面是IPC接口的定义:
// HeroesInterface.aidl 文件
// 包名别搞错
package com.ethan.aidlservice.aidl;
// 注释 2, 要手动引用这个包,记得包名一定要对上
import com.ethan.aidlservice.aidl.Hero;
interface HeroesInterface {
// Hero
void addHero(in Hero hero);
List<Hero> getHeroes();
}
上面定义了此次进程间通信的接口 HeroesInterface.aidl 。因为接口里要引用到我们刚才创建的 Hero.java 类,而这里是 aidl文件又不能直接引用。所以我们还要再定义一个 Hero.java 类的 aidl映射:
// Hero.aidl
// 注释 3, 包名别搞错,要和 Hero.java 文件所在包名相同 !
package com.ethan.aidlservice.aidl;
// Declare any non-default types here with import statements
parcelable Hero ;
上面注释 注释 2和注释 3是两个需要特别注意的点。注释 3处这个 Hero.aidl的包名一定要和 Hero.java所在的包名一致,否则最后将找不到 Hero.java类。注释 2的地方 HeroesInterface.aidl文件里引用到了 Hero.aidl。但编译器不会提示我们去导包,所以我们要手动导入,否则构建时将找不到 Hero.aidl。
下面是创建完三个文件后的目录结构:
aidl.png
好了下面我们开始写服务端 service的代码了:
/*************
* 在AIDL实现的IPC通信当中,service用于返回给客户端的binder由AIDL接口的内部类Stub
* 实现,该类实现了IBinder接口,HeroesInterface.Stub。
* *************/
public class AidlService extends Service {
private static final String TAG = "AidlService";
// 注释 4,保存英雄信息的列表
private CopyOnWriteArrayList<Hero> serviceHeroes = new CopyOnWriteArrayList<>();
@Override
public void onCreate() {
super.onCreate();
serviceHeroes.add(new Hero("Tony" , "钢铁侠" , "战士、坦克"));
serviceHeroes.add(new Hero("Steve" , "美国队长" , "战士"));
serviceHeroes.add(new Hero("Thor" , "雷神" , "坦克、战士"));
}
/**
* 注释 5
* 定义 HeroesInterface.aidl接口定义后,构建过程中系统会自动给我们创建 HeroesInterface.java接口
* HeroesInterface.java接口里会有一个 Stub静态内部类,继承自 IBinder接口
*/
private final HeroesInterface.Stub binder = new HeroesInterface.Stub() {
@Override
public void addHero(Hero hero) throws RemoteException {
serviceHeroes.add(hero);
}
@Override
public List<Hero> getHeroes() throws RemoteException {
return serviceHeroes;
}
};
/**
* 给客户端返回 Binder
*
* @param intent
* @return
*/
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
看到上面代码,这就是一个普普通通的一个 Service,注释 4处创建一个 CopyOnWriteArrayList(写时复制)列表用于服务端保存英雄信息。这里唯一不普通的地方就是上面注释 5处用于返回给客户端的 binder 对象的创建。我们可以看到这是通过 HeroesInterface.java类的静态内部类 Stub 创建的。而HeroesInterface.java接口就是在我们定义完 aidl构建完之后系统为我们自动生成的(写完 aidl别忘了 rebuild一下工程)。所以如果我们不嫌麻烦,手动去写这个HeroesInterface.java文件的话,我们就根本不需要创建aidl文件。这里还有一个要注意的点,那就是 Service的注册:
<!-- service要加上相关属性,否则不允许进程间绑定-->
<service
android:name=".aidl.AidlService"
android:process=":remote"
android:enabled="true"
android:exported="true"
/>
作为 4大组件之一, Service当然要注册。看上面该加的属性不能少,否则别的应用可能无权绑定这个 Service。
- 客户端
好了,服务端创建完了。我们现在看看客户端那边怎么搞。先创建一个新的工程,包名随便。
然后我们把服务端的 Hero.java拷贝到客户端工程。这里有一个点需要注意:
连同包名一起拷,客户端 Hero.java路径和服务端的要一致 !
连同包名一起拷,客户端 Hero.java路径和服务端的要一致 !
连同包名一起拷,客户端 Hero.java路径和服务端的要一致 !
然后再把我们服务端的两个 ADIL文件: Hero.aidl、HeroesInterface.aidl也拷到客户端工程,这里也有一个问题要注意:
连同包名一起拷,客户端 AIDL 文件路径和服务端的要一致 !
连同包名一起拷,客户端 AIDL 文件路径和服务端的要一致 !
连同包名一起拷,客户端 AIDL 文件路径和服务端的要一致 !
下面时拷贝后客户端的目录:
aidlC.png
然后,客户端这边我们开始绑定服务:
/***以设置组件名称的方式构建Intent 用以跨应用间绑定服务***/
private void bindAidlService() {
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.ethan.aidlservice",//应用的包名
"com.ethan.aidlservice.aidl.AidlService"));//带路径的服务类名,用于跨应用绑定
bindService(intent, aidlConnection, Context.BIND_AUTO_CREATE);
}
private HeroesInterface mHeroesInterface;
private ServiceConnection aidlConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
Log.d(TAG, "onServiceConnected");
if (iBinder != null) {
// 注释 6, 获取接口实现类对象
mHeroesInterface = HeroesInterface.Stub.asInterface(iBinder);
} else {
Log.d(TAG, "PERMISSION_DENIED");
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
mHeroesInterface = null;
}
};
这里跟我们平时绑定服务的形式差不多。有两点细节不一样。一个是我们通过 intent.setComponent() 方式来绑定服务。因为这是跨进程绑定,所以无法显式指定 Service类型。另外一个是注释 6的地方,我们要通过 HeroesInterface.Stub的方法获取 HeroesInterface.java 接口的实现类的对象。绑定成功后,就可以用 HeroesInterface 接口对象访问服务端了。
二、原理分析
下面我们来分析一下通信。上面说到,我们在定义完 aidl接口编译过后,系统会自动给我们生成一个用于此次进程间通信的接口实现类 HeroesInterface.java。那么我们要分析通信机制当然要从这个类着手。这个类是代码生成的,所以显得有点杂乱。所以我整理了一下,贴出核心部分作分析:
public interface HeroesInterface extends IInterface {
/**
* Stub
* HeroesInterface 接口的实现类,继承自 Binder。服务端创建该实例返给客户端
*/
public static abstract class Stub extends Binder implements HeroesInterface {
public static HeroesInterface asInterface(IBinder obj) {
// 注释 7, 客户端首先在当前进程查询是否有这个接口实现类的对象,如果有则直接使用。说明不涉及 IPC
// 如果没有则创建一个代理对象
IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof HeroesInterface))) {
// 注释 8, 在当前进程找到该接口实现类对象,直接返回
return ((HeroesInterface) iin);
}
//注释 9, 当前进程中找不到,那就创建一个代理对象
// 需要代理一下,因为在像服务器发请求之前需要处理一下数据
return new HeroesInterface.Stub.Proxy(obj);
}
/**
* onTransact 方法运行在远程服务端 的Binder连接池中
*
* @param code 方法代号
* @param data 客户端请求数据
* @param reply 返回给客户端的值
* @param flags
* @return
* @throws android.os.RemoteException
*/
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
throws android.os.RemoteException {
String descriptor = DESCRIPTOR;
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(descriptor);
return true;
}
// addHero方法
case TRANSACTION_addHero: {
Hero hero;
// 注释10, 将客户端传来的 data反序列化
hero = Hero.CREATOR.createFromParcel(data);
// 注释11, 将反序列化的数据存入 Service
this.addHero(hero);
......
return true;
}
// getHeroes方法
case TRANSACTION_getHeroes: {
// 获取服务端列表数据
List<Hero> _result = this.getHeroes();
// 将查询到的数据返回给客户端
reply.writeTypedList(_result);
return true;
}
default: {
return super.onTransact(code, data, reply, flags);
}
}
}
private static class Proxy implements HeroesInterface {
private IBinder mRemote;
Proxy(IBinder remote) {
mRemote = remote;
}
@Override
public void addHero(Hero hero) throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
try {
......
// 注释 12, 将客户端请求添加的对象 hero写进 Parcel对象里
hero.writeToParcel(_data, 0);
......
// 注释 13, 客户端调用远程服务端返回的 Binder(mRemote)的transact方法写入数据
// Stub.TRANSACTION_addHero : 方法代号code
// _data : 客户端请求添加的数据
// _reply : 当方法有返回值的话,调用完该方法会返回该值
boolean _status = mRemote.transact(Stub.TRANSACTION_addHero, _data, _reply, 0);
......
} finally {
_reply.recycle();
_data.recycle();
}
}
@Override
public java.util.List<Hero> getHeroes() throws android.os.RemoteException {
Parcel _data = android.os.Parcel.obtain();
Parcel _reply = android.os.Parcel.obtain();
List<Hero> _result;
try {
// 客户端调用远程服务器的 Binder的 transact 方法
boolean _status = mRemote.transact(Stub.TRANSACTION_getHeroes, _data, _reply, 0);
......
// 获取服务器返回结果
_result = _reply.createTypedArrayList(Hero.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
}
}
看上面代码,HeroesInterface 类里面有一个静态内部类Stub 。这个静态内部类继承自 Binder 并且实现了 HeroesInterface.java 接口。在上文我们提到,服务端用这个静态内部类Stub 创建Binder对象返回给客户端。
然后我们现在把注意力放到这个静态内部类Stub上,因为它实现了主要的核心逻辑。这个Stub里面有 3个重要的成员:asInterface() 方法、onTransact()方法、Proxy 代理类(Stub里的静态内部类)。现在我们注意分析三个哥们儿的作用就差不多了。
asInterface方法: 这个方法的作用是将服务端返回给我们的远程 Binder对象打包成 HeroesInterface 的接口实现类,以方便调用客户端调用向远程服务端的请求方法。从上面注释7、注释8、注释9我们可以看到,获取HeroesInterface 对象的过程是这样的。先查询当前进程内有没有该对象(注释 7),如果有则说明不涉及 IPC,直接使用当前进程的接口对象(注释8)。如果没有,则通过代理类Proxy 创建一个(注释 9).
onTransact()方法:运行在服务端的Binder线程池当中,当远程客户端通过Binder的transact方法发起远程请求后,服务端会触发该方法,并处理相关请求。该方法参数有包含客户端请求的方法代码code,包含客户端请求的方法参数的Parcel对象及包含返回给客户端返回值的Parcel。onTransact方法的布尔型返回值决定客户端的请求成功与否,可用于权限控制。上面注释10、注释11处涉及到服务端的反序列化过程,及数据存储及数据返回过程。
Proxy 代理类:这个代理类是代理客户端向服务端发起请求的。这是一种静态代理设计模式。为什么要代理?因为客户端在发起请求前需要处理一下数据,比如将数据序列化等等。上面注释 12 的地方是客户端将数据进行了序列化操作,然后注释13处再调用远程服务端返回给客户端的 Binder对象( mRemote引用)向远程服务端发起请求。也就是上面注释 13处的调用了调用了transact()方法方法,该方法再服务端会触发 onTransact()方法,也就是上面提到的这个方法。
三、观察者设计模式实现基于AIDL的客户端订阅
现在我们再基于上文第一步实现的功能再扩展一下。我们在服务端创建一个列表,用于存放客户端注册过来的观察者。当服务端的数据发生变化时,就遍历列表的观察者,然后调用观察者的方法将最新数据推送给所有观察者。这样就做成了类似于微信公众号一样的系统,当公众号有更新时,所有订阅的用户都能收到。
下面我们来完善一下aidl接口:
// OnNewHeroJoinListener.aidl
//包名要与其他类及相关接口的包名要保持一致
package com.ethan.aidlservice.aidl;
//AIDl特性,即使所用到的类与本接口同属一个包,仍然要引入该类声明,否则报错
import com.ethan.aidlservice.aidl.Hero;
interface OnNewHeroJoinListener {
/**
* Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
void onNewHeroJoin(in List<Hero> heroes);// 形参需声明in,否则会报错
}
上面我们新增一个AIDL接口定义,用于观察者这边的数据回调。
然后 HeroesInterface 接口我们再新增两个方法,用于客户端注册观察者以及注销。HeroesInterface.aidl改成这样:
// HeroesInterface.aidl
// 包名别搞错
package com.ethan.aidlservice.aidl;
// 要手动引用这个包,记得包名一定要对上
import com.ethan.aidlservice.aidl.Hero;
import com.ethan.aidlservice.aidl.OnNewHeroJoinListener;
interface HeroesInterface {
void addHero(in Hero hero);
List<Hero> getHeroes();
// 用于注册以及注销观察者
void registerListener(OnNewHeroJoinListener onNewHeroJoinListener);
void unRegisterListener(OnNewHeroJoinListener onNewHeroJoinListener);
}
aidl文件改完了,然后客户端的aidl文件别忘了也同步更新一下。然后我们再改动一下服务端的Service:
/*************
* 在AIDL实现的IPC通信当中,service用于返回给客户端的binder由AIDL接口的内部类Stub
* 实现,该类实现了IBinder接口,HeroesInterface.Stub。
* *************/
public class AidlService extends Service {
private static final String TAG = "AidlService";
private CopyOnWriteArrayList<Hero> serviceHeroes = new CopyOnWriteArrayList<>();
// 注释 14,观察者列表
private CopyOnWriteArrayList<OnNewHeroJoinListener> Listeners = new CopyOnWriteArrayList<>();
@Override
public void onCreate() {
super.onCreate();
serviceHeroes.add(new Hero("Tony" , "钢铁侠" , "战士、坦克"));
serviceHeroes.add(new Hero("Steve" , "美国队长" , "战士"));
serviceHeroes.add(new Hero("Thor" , "雷神" , "坦克、战士"));
}
private final HeroesInterface.Stub binder = new HeroesInterface.Stub() {
@Override
public void addHero(Hero hero) throws RemoteException {
serviceHeroes.add(hero);
// 注释 15,遍历所有观察者,推送服务器最新数据
if (Listeners.size() > 0){
for (OnNewHeroJoinListener listener : Listeners){
listener.onNewHeroJoin(serviceHeroes);
}
}
}
@Override
public List<Hero> getHeroes() throws RemoteException {
return serviceHeroes;
}
// 注释 16, 注册观察者
@Override
public void registerListener(OnNewHeroJoinListener onNewHeroJoinListener) throws RemoteException {
if (!Listeners.contains(onNewHeroJoinListener)) Listeners.add(onNewHeroJoinListener);
}
// 注释 17, 注销观察者
@Override
public void unRegisterListener(OnNewHeroJoinListener onNewHeroJoinListener) throws RemoteException {
if (Listeners.contains(onNewHeroJoinListener)) Listeners.remove(onNewHeroJoinListener);
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG , "onBind");
return binder;
}
@Override
public void onDestroy() {
super.onDestroy();
}
}
上面改动分别是注释14-注释 17处。注释 14处新增一个列表,用于客户端注册过来的观察者。注释 15处,当有客户端向服务端添加数据之后,服务端即遍历所有观察者,并向所有注册的客户端推送最新数据。注释16、注释17处分别是AIDL接口新增的注册和注销观察者的方法。
最后我们看看客户端注册的改动:
// MainActivity.java
private HeroesInterface mHeroesInterface;
private ServiceConnection aidlConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
Log.d(TAG , "onServiceConnected");
if (iBinder != null){
mHeroesInterface = HeroesInterface.Stub.asInterface(iBinder);
bindServiceButton.setEnabled(false);
bindServiceButton.setText(bindServiceButton.getText().toString() + "(已绑定)");
try {
// 注释 18,绑定Service成功后,向服务器注册观察者
mHeroesInterface.registerListener(mOnNewHeroJoinListener);
}catch (RemoteException r){}
}else {
Log.d(TAG , "PERMISSION_DENIED");
}
}
@Override
public void onServiceDisconnected(ComponentName componentName) {
Log.d(TAG , "onServiceDisconnected");
mHeroesInterface = null;
bindServiceButton.setEnabled(true);
bindServiceButton.setText("绑定AIDL服务");
}
};
//注释19, 观察者对象
private final OnNewHeroJoinListener mOnNewHeroJoinListener = new OnNewHeroJoinListener.Stub() {
@Override
public void onNewHeroJoin(List<Hero> heroes) throws RemoteException {
showList(heroes);
}
};
上面只贴出了客户端核心改动的部分,其他改动都是客户端的显示逻辑。我们看到上面注释 18的地方,当客户端绑定服务端成功之后就向服务端注册观察者。注释 19处是观察者对象。
最后,我们看下效果吧:
AIDL2021728020121.gif
上面可以看到,客户端每点一次添加,服务端都会主动返回最新的数据。
最后抛出一个问题:AIDL接口里的方法顺序改变了,但客户端没有同步更新,会不会有问题?这个问题曾经有人问过笔者,这里亲自试过,是有问题的 !一旦AIDL文件里的方法顺序改变了,但客户端没有及时更新,那么顺序改变的方法将会乱入。我们可以看看AIDL文件生成的java类接口源码就知道了:
// HeroesInterface.java
// 方法代码 code
static final int TRANSACTION_addHero = (IBinder.FIRST_CALL_TRANSACTION + 0);//0
static final int TRANSACTION_getHeroes = (IBinder.FIRST_CALL_TRANSACTION + 1);//1
static final int TRANSACTION_registerListener = (IBinder.FIRST_CALL_TRANSACTION + 2);//2
static final int TRANSACTION_unRegisterListener = (IBinder.FIRST_CALL_TRANSACTION + 3);//3
看到没?上面就是4个接口方法的代码 code,它的大小是按aidl文件里定义的顺序来赋值的。所以服务端AIDL文件变动时,一定别忘了给咱客户端也同步更新一下哈。
服务端Demo:Service
客户端Demo:Client