Android开发

AIDL使用及原理分析+观察者设计模式

2021-07-28  本文已影响0人  碧云天EthanLee
概述

我们应该知道,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

上一篇下一篇

猜你喜欢

热点阅读