Android 进程间的通信之AIDL

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,591评论 18 139
  • 上篇文章介绍了IPC机制的基本概念以及简单使用,文章链接:Android 关于IPC机制的理解(一) 这篇文章主要...
    老实任阅读 703评论 0 2
  • Jianwei's blog 首页 分类 关于 归档 标签 巧用Android多进程,微信,微博等主流App都在用...
    justCode_阅读 5,893评论 1 23
  • 前言 此教程的目的是教会大家如何使用AIDL,包括定义AIDL服务、调用AIDL服务、传递复杂对象、AIDL回调客...
    daking阅读 15,800评论 3 28
  • 用尽每一次力量 直到再也站不起来 我依旧看到 那天,黄昏,落日也如此绚丽 那天,夜空,星星也如此闪耀 那天,我也是...
    林先生容易见光死阅读 359评论 0 1