Android 进程间的通信之AIDL

作者: 那时青菜 | 来源:发表于2018-04-08 15:54 被阅读809次

    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开发艺术探索》。这真的是一本神器啊,谁用谁知道。

    相关文章

      网友评论

      • IT人故事会:老铁下次注意格式啊,不太清晰,给点建议对于新手需要的是注释啊
        那时青菜:注释不够明白吗?格式有什么要求吗?

      本文标题:Android 进程间的通信之AIDL

      本文链接:https://www.haomeiwen.com/subject/otsphftx.html