×

Android 进程间的通信之AIDL

96
那时青菜
2018.04.08 15:54* 字数 3628

Android IPC 简介:

    IPC是Inter-Process Communication的缩写,就是进程间通信或者跨进程通信的意思,指的是两个进程之间进行数据交换的过程。这里简单讲一下进程和线程的区别:进程指的是一个程序,在Android中指的就是一个app;线程是cpu调度的最小单元,我的理解是线程是执行单线任务的,一般来说,每个app都有主线程,主线程相当于主线剧情,不论发生的事件还是执行流程都是围着他进行的。其他线程相当于支线任务,主要是丰富和扩展主线的。因此,一个进程可包含多个线程。

    在Android想要进行线程间的通信,大家都很熟悉Handler,Asynctask,线程池等。但是说到进程间的通信,大家可能了解的不多。事实上,android中实现多进程的方式也是多种多样,他们每个都有自己优缺点,今天主要介绍一下AIDL的通信方式。

Android AIDL 实现:

      在正式实现之前,我们需要搞懂几个基础概念。首先我们知道Aidl分为服务端和客户端。

      1.服务端:

        服务端就是你要连接的进程。他提供给客户端一个Service,在这个Service中监听客户端的连接请求,然后创建一个AIDL接口文件,里面是将要实现的方法,注意这个方法是暴露给客户端的的。最后在Service中实现这个AIDL接口即可(这里是接口的具体实现)。服务端的职责是提供连接和自身

      2.客户端:

        客户端首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转换成AIDL接口所属的类型,最后调用AIDL的方法就可以了。可以看到,客户端还是比较简单的,负责连接和调用。

     3.AIDL所支持的数据类型

        在AIDL中,并非支持所有数据类型,他支持的数据类型如下所示:

        ● 基本数据类型(int、long、char、boolean、double、float、byte、short)

        ● String和CharSequence

        ● List:只支持ArrayList,并且里面的每个元素必须被AIDL支持

        ● Map: 只支持HashMap, 同样的,里面的元素都必须被AIDL支持,包括key和value

        ● Parcelable:所有实现了Parcelable接口的对象

        ● AIDL: 所有的AIDL接口本身也可以在AIDL 文件中使用

        以上就是AIDL所支持的所有类型,其中自定义的Parce对象和AIDL对象必须要显式的import进来,不管它们是否和当前的AIDL文件在同一个包中。另外需要注意的一点是,如果AIDL文件用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。除此之外,AIDL除了基本类型,其他类型的参数都必须标上方向:in、out或者inout,in标上输入型参数,out表示输出型参数,inout表示输入输出型参数。

        好的,准备工作完成。接下来正式开始了。假设XX资讯公司某天接了个业务,公司领导决定和XX招聘合作。需求是这样的,用户在浏览资讯的时候,会不时的插播一条招聘广告(万恶的广告啊)。就这么个简单的需求,用AIDL怎么实现。

        服务端实现:

          首先是招聘广告对象,这个类是一个招聘的具体内容:

package com.example.aykon.aidltest.AD;

import android.os.Parcel;

import android.os.Parcelable;

public class Advert implements Parcelable{

    //职位

    private String position;

    //薪资

    private int salary;

    //具体内容

    private String content;

    public Advert(String position, int salary, String content) {

        this.position = position;

        this.salary = salary;

        this.content = content;

}

    protected Advert(Parcel in) {

        position = in.readString();

        salary = in.readInt();

        content = in.readString();

}

    public static final Creator CREATOR = new Creator() {

        @Override

        public Advert createFromParcel(Parcel in) {

            return new Advert(in);

}

        @Override

        public Advert[] newArray(int size) {

            return new Advert[size];

}

};

    @Override

    public int describeContents() {

        return 0;

}

    @Override

    public void writeToParcel(Parcel dest, int flags) {

        dest.writeString(position);

        dest.writeInt(salary);

        dest.writeString(content);

}

    public String getPosition() {

        return position;

}

    public void setPosition(String position) {

        this.position = position;

}

    public int getSalary() {

        return salary;

}

    public void setSalary(int salary) {

        this.salary = salary;

}

    public String getContent() {

        return content;

}

    public void setContent(String content) {

        this.content = content;

}

}

        这个类就是需要用的实体类,因为是跨进程,所以实现了Parcelable接口,这个是Android官方提供的,它里面主要是靠Parcel来传递数据,Parcel内部包装了可序列化的数据,能够在Binder中自由传输数据。剩下代码十分简单,声明了职位、工资、具体内容3个字段。提供相关的构造方法,getter()和setter()方法。接着就是需要重写的方法,大致是提供一个读一个写两个方法,具体的含义这里不深究。

        之前说过,如果用到了自定义Parcelable对象,就需要创建一个同名的AIDL文件。

// Advert.aidl

package com.example.aykon.aidltest;

parcelable Advert;

        数据有了保障,然后就是给客户端提供获取数据的方法。在这里就是创建AIDL接口,具体就是招聘广告的AIDL文件,这个接口里暂时提供2个方法,为什么说暂时,因为需求从来没确定过。诶!一个是获取所有的广告,再一个就是添加一条广告。


// IAdvertManager.aidl

package com.example.aykon.aidltest;

import com.example.aykon.aidltest.Advert;

interface IAdvertManager {

    List getAdvertList();

    void addAdvert(in Advert ad);

}

        好了,接口有了,服务端最后一步,提供给客户端连接的service,并实现广告接口。

public class AdvertManagerService extends Service{

    private CopyOnWriteArrayList mAdvertList = new CopyOnWriteArrayList<>();

   //核心,Stub里面的方法运行的binder池中。

    private Binder mBinder = new IAdvertManager.Stub(){

        @Override

        public List getAdvertList() throws RemoteException {

            return mAdvertList;

}

        @Override

        public void addAdvert(Advert ad) throws RemoteException {

            mAdvertList.add(ad);

}

};

     @Nullable

    @Override

    public IBinder onBind(Intent intent) {

        return mBinder;

}

    @Override

    public void onCreate() {

        super.onCreate();

        mAdvertList.add(new Advert("Android", 10, "app开发"));

        mAdvertList.add(new Advert("ios", 10, "ios开发"));

}

}

        可以看到,在onCteate()方法里添加了两条假数据,关于CopyOnWriteArrayList 集合,这里简单介绍下,CopyOnWriteArrayList 支持并发读/写,AIDL的发放是运行在服务端的Binder池中,因此当多个客户端同时连接的时候,存在多个线程同时访问的情况,因此这里用CopyOnWriteArrayList 来进行自动的线程同步。另外,细心的小伙伴可能注意到了,我们前面说过,AIDL中支持的List只有ArrayList,那么为什么CopyOnWriteArrayList (并非继承自ArrayList)可以呢?这是因为AIDL支持的是抽象的List,而List是一个接口,因此虽然服务端返回的是CopyOnWriteArrayList ,但是在Binder线程池中,也就是Stub()中,它会形成一个新的ArrayList传递给客户端。

        在我们重写的onBinde()方法中返回Binder对象,这个Binder对象指向IAdvertManager.Stub(),这个Stub类并非我们自己创建的,而是AIDL自动生成的。系统会为每个AIDL接口在build/source/aidl下生成一个文件夹,它的名称跟你命名的AIDL文件夹一样,里面的类也一样。如下图:


系统生成的aidl文件

        这个IAdvertManager.java就是系统为我们生成的相应java文件,简单说下这个类。它声明了两个方法getAdvertList和addAdvert,分明就是我们AIDL接口中的两个方法。同时他声明了2个id用来标识这两个方法,这两个id用于标识在transact过程中客户端请求的到底是哪个方法。接着就是我们的Stub,可以看到它是一个内部类,他本质上是一个Binder类。当服务端和客户端位于同一个进程时,方法调用不会走跨进程的transact过程,当两者处于不同晋城市,方法调用走transact过程,这个逻辑由Stub的内部代理类Proxy完成。

    这个Stub对象之所以里面有我们AIDL的接口,正是因为官方替我们做好了,我们只要在这里具体实现就好了。这两个方法,我在这里做了简单的处理,一个是返回我们之前的集合,一个是向集合里面添加一条广告数据。这里只做演示用,项目中记得活学活用。

 至此服务端的代码都实现了,然后在看看客户端的实现。

客户端:

package com.example.aykon.aidltest;

public class AdvertActivity extends AppCompatActivity {

    public static final String TAG = "AdvertActivity";

    private IAdvertManager mAdvertManager;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_advert);

        Intent intent = new Intent(this, AdvertManagerService.class);

        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

}

    private ServiceConnection mConnection = new ServiceConnection() {

        @Override

        public void onServiceConnected(ComponentName name, IBinder service) {

            //这里将binder对象转换为aidl对象,从而能够调用aidl方法。

            IAdvertManager iAdvertManager = IAdvertManager.Stub.asInterface(service);

            try {

                mAdvertManager = iAdvertManager;

                List advertList = mAdvertManager.getAdvertList();

                //得到广告列表之后就可以为所欲为了。。。。

                Log.i(TAG,advertList.toString());

                Advert advert = new Advert("java", 10, "后台");

                mAdvertManager.addAdvert(advert);

                Log.i(TAG,iAdvertManager.getAdvertList().toString());

            } catch (RemoteException e) {

                e.printStackTrace();

}

}

        @Override

        public void onServiceDisconnected(ComponentName name) {

}

};

    @Override

    protected void onDestroy() {

        //最后解注册

        unbindService(mConnection);

        super.onDestroy();

}

}


        客户端也非常简单,首先我们连接到服务端Service,在连接成功时,也就是onServiceConnected方法里,通过asInterface(service)方法可以将服务端的Binder对象转换成客户端所需的AIDL的接口的对象。这种转换是区分进程的,如果是同一进程,那么此方法返回的就是Stub本身,否则返回的就是系统Stub.proxy对象。拿到接口对象之后,我们就能够调用相应方法进行自己的处理(为所欲为之为所欲为)。

        上面就是一整个AIDL跨进程的方法,同时我们也分析了Binder的工作机制。但是,这里有两点需要额外说明一下:第一个,当客户端发起远程请求时,客户端会挂起,一直等到服务端处理完并返回数据,所以远程通信是很耗时的,所以不能在UI线程发起访问。第二个,由于服务端的Binder方法运行在Binder线程池中,所以应采取同步的方式去实现,因为它已经运行在一个线程中了。

        Binder是会意外死亡的。如果服务端的进程由于某种原因异常终止,会导致远程调用失败,如果我们不知道Binder连接已经断裂, 那么客户端就会受到影响。不用担心,Android贴心的为我们提供了连个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知。


死亡代理

        同时在onServiceConnected连接成功时设置死亡代理binder.linkToDeath(mDeathRecipient, 0);第二个参数是一个标记,我们自己定义的。

      AIDL注册和解注册:

        因为跨进程传输客户端的同一个对象会在服务端生成不同的对象,所以如果我们解注册的时候还是用这个接口,就会报一个unregister listener的错。事实上,这些新生成的对象有一个共同点,那就是它们底层的Binder对象是同一个。当客户端解注册的时候,我们只要便利服务端所有的listener,找出那个和解注册listener具有相同Bidner对象的服务端listener并把它删掉就可以了。RemoteCallbackList已经为我们做好了这些事情。RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。它是一个泛型,支持管理任意的AIDL接口。它的工作原理很简单,在它内部有一个Map专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value是Callback类型。其中Callback封装了真正的远程listener。当客户端注册listener的时候,它会把这个listener的信息存入mCallbacks中。同时,当客户端进程终止后,它能够自动移除客户端所注册的listener。除此之外,RemoteCallbackList内部实现了线程同步,我们使用它来注册和解注册时,不需要做额外的线程同步工作。

            RemoteCallbackList的用法也很简单,你只需在注册和解注册的地方调用mRemoteCallbackList.register(listener)和mRemoteCallbackList.unregister(listener)即可。还有要注意的一点是,RemoteCallbackList并不是一个List,遍历RemoteCallbackList时,必须要配对使用mRemoteCallbackList.beginBroadcast()和mRemoteCallbackList.finishBroadCast()。beginBroadcast返回RemoteCallbackList的size,finishBroadCast结束RemoteCallbackList的遍历,通过mRemoteCallbackList.getBroadcastItem(i)来获取每个注册的接口。

        AIDL权限验证:

          我们的远程服务自然是不想任意的人调用的,所以我们给服务加入权限验证功能。在AIDL进程权限验证,这里介绍两种常用的方法。

        第一种:在onBind中验证,验证不通过就返回null。


声明权限


验证权限

            第二种,我们可以在服务端的onTransact方法中进行权限验证,如果验证失败就直接返回false,这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果,具体的实现方式和第一种一样。另外还可以采用Uid和Pid来进行验证。

本篇文章如果有什么纰漏,还请不吝指出。



文章参考自《Android开发艺术探索》。这真的是一本神器啊,谁用谁知道。

日记本
Web note ad 1