进程间通信--IPC

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

前言:

进程间通信(Inter-Process Communication),简称IPC,就是指进程与进程之间进行通信.一般来说,一个app只有一个进程,但是可能会有多个线程,所以我们用得比较多的是多线程通信,比如handler,AsyncTask.

但是在一些特殊的情况下,我们app会需要多个进程,或者是我们在远程服务调用时,就需要跨进程通信了

1.设置多进程

Android设置多进程的步骤很简单,只用在清单文件中为四大组件加上process属性

<service android:name=".MessagerService"
             android:process=":messager">
</service>

( :messager 最终的进程名会变成 包名+:messager)

虽然多进程设置起来很简单,但是使用的时候却会有一系列的问题

(两个进程对应的是不同的内存区域)

  • 1.Application对象会创建多次
  • 2.静态成员不共用
  • 3.同步锁失效
  • 4.单例模式失效
  • 5.数据传递的对象必须可序列化

2.可序列化

进程间通信传递的对象是有严格要求的,除了基本数据类型,其他对象要想可以传递,必须可序列化,Android实现可序列化一般是通过实现Serializable或者是Parcelable

如果你在进程通信中不需要传非基本数据类型的对象,那么你可以不了解序列化,但是可序列化是进程间通信的基础,所以还是建议不了解的朋友先熟悉一下

笔者之前介绍过序列化的相关知识,这里就不重复介绍了

序列化--Serializable与Parcelable

http://blog.csdn.net/yulyu/article/details/56481665

3.通信

跨进程通信的方法有很多,比如通过Intent传递,通过AIDL以及Messager通信,通过socket通信,这里主要介绍的是基于Binder的AIDL和Messager

3.1 Intent

Intent进行数据的传递是我们平时最常用的,他的原理其实是对于Binder的封装,但是他只能做到单向的数据传递,所以并不能很好的实现跨进程通信,我们这里就不展开来介绍了

3.2 Messager

Messager的底层也是基于Binder的,其实应该说他是在AIDL的基础上封装了一层

一般来说安卓中使用Binder主要是通过绑定服务(bindService),服务端(这里指的不是后台,是指其中一个进程)主要是运行Service,客户端通过bindService获取到相关的Binder,Binder就作为桥梁进行跨进程的通信.

这里我们先演示同一个应用内的多进程通信

3.2.1 服务器端

首先我们先创建一个Service,

public class XiayuService extends Service{
    
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

并在清单文件中配置他的进程

<service android:name=".XiayuService"
             android:process=":xiayu"
 />

在Service里面创建一个Hander用来接受消息

private final static Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        System.out.println("地瓜地瓜,我是土豆,我是土豆, 听到请回答,听到请回答");

    }
};

在Service里面创建一个Messager,并把Handler放入其中

private final static Messenger mMessenger = new Messenger(mHandler);

重写onbind方法,返回Messager里面的Binder

public IBinder onBind(Intent intent) {
    return mMessenger.getBinder();
}

3.2.2 客户端

创建一个对象实现ServiceConnection

private class MyServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //当连接上服务后会调用这个方法
            //TODO 
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
}

绑定服务

Intent    intent = new Intent(MainActivity.this, XiayuService.class);

MyServiceConnection  myServiceConnection = new MyServiceConnection();

bindService(intent, myServiceConnection, BIND_AUTO_CREATE);

绑定服务后,会调用ServiceConnection的onServiceConnected方法,通过Messager发送消息,服务器端的Handler就能够收到消息了

private class MyServiceConnection implements ServiceConnection {

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        //通过Binder创建Messager
        Messenger messenger = new Messenger(service);
        //创建msg
        Message msg = Message.obtain();

        try {
            //通过Messager发送消息
            messenger.send(msg);
            
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
}

这样的话我们就能够通过bindService获取到一个包含Binder的Messager进行通信了,但是我们目前只实现了客户端对服务器端传递消息,那么服务器端如何对客户端传递消息呢?

我们先对服务器端的代码进行修改,首先修改Service的Handler

(关键代码是 Messenger messenger = msg.replyTo)

private final static Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        System.out.println("地瓜地瓜,我是土豆,我是土豆, 听到请回答,听到请回答");
        //获取Messager
        Messenger messenger = msg.replyTo;
        //创建消息
        Message msg_reply = Message.obtain();
        try {
            //发送
            messenger.send(msg_reply);
            
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
};

接着我们在客户端也增加一个Handler和Messager来处理消息

private final static Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        System.out.println("土豆,土豆,我是地瓜,我已收到你的消息");
    }
};

private final static Messenger mReplyMessager = new Messenger(mHandler);

还有一个比较关键的地方,就是要在客户端发送消息的时候把客户端的Messager通过消息传送到服务器端

(msg.replyTo =mReplyMessager)

private class MyServiceConnection implements ServiceConnection {

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Messenger messenger = new Messenger(service);

        Message msg = Message.obtain();
        //通过msg把客户端的Messager传送到服务器端(关键代码)
        msg.replyTo =mReplyMessager;
        
        try {

            messenger.send(msg);

        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
}

这样一来,服务器端和客户端就能很好的实现跨进程通信了.

如果需要传送数据的话,可以通过Bundle设置数据,除了基本数据类型,还可以通过消息传送可序列化的对象

发送方:

        Message msg = Message.obtain();

        Bundle bundle = new Bundle();

        //传输序列化对象
        //bundle.putParcelable();

        //bundle.putSerializable();

        msg.setData(bundle);

接收方:

        Bundle data = msg.getData();
        
        //获取数据
        //data.getSerializable()

        //data.getParcelable()

3.2.3 弊端

上面我们已经实现了跨进程通信,但是这里面其实是有弊端的,服务端处理客户端的消息是串行的,必须一个一个来处理,所以如果是并发量比较大的时候,通过Messager来通信就不太适合了

3.2.4 注意

上面演示的是应用内跨进程通信,绑定服务可以通过显示意图来绑定,但是如果是跨应用的进程间通信,那么就需要用到隐式意图了.这里有一点需要注意的就是,在5.0以后隐式意图开启或者绑定service要setPackage(Service的包名),不然会报错

    mIntent = new Intent();

    //设置Package为Service的包名
    mIntent.setPackage("com.xiayu.ipcservice");

    mIntent.setAction("myMessager");

3.3 AIDL

上面提到过通过Meaager跨进程不适合并发量大的情况,那么如果并发量大的话,我们用什么来处理呢?那就可以通过AIDL来进行,这里是Google的描述

Note: Using AIDL is necessary only if you allow clients from different applications to access your service for IPC and want 
to handle multithreading in your service. If you do not need to perform concurrent IPC across different applications, you 
should create your interface by implementing a Binder or, if you want to perform IPC, but do not need to handle 
multithreading, implement your interface using a Messenger. Regardless, be sure that you understand Bound Services before 
implementing an AIDL.

主要意思就是你可以用Messager处理简单的跨进程通信,但是高并发量的要用AIDL

我们还是先演示一下同一个应用内的跨进程通信

3.3.1 服务端

首先我们创建一个Service

public class AIDLService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}

然后在清单文件里面设置Service的进程

    <service android:name=".AIDLService"
             android:process=":xiayu"
    />

然后右键选择新建AIDL文件,Android Studio就会帮你在你的aidl目录的同名文件夹下面创建一个AIDL文件

// IShop.aidl
package com.xiayu.aidldemo;

interface IShop {
    //此方法是创建aidl自带的方法,告知你可以使用那些数据类型
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

在AIDL文件里面会有一个接口,并声明了一个方法,那个方法主要是告诉你AIDL支持哪些数据类型传输,所以我们把这个方法删掉,我们再自己声明一个方法,用于之后的调用

(注意:每次修改了AIDI文件后,需要同步一下才会生效,因为每次同步后,Android Studio会在 项目/build/generated/source/aidl/debug 目录下生成相应的java文件)

interface IShop {
    //自己声明的方法,用于之后的调用
    void sell();
}

我们在Service中创建一个Binder,并在onbind的时候返回

public class AIDLService extends Service{

    private Binder mBinder = new IShop.Stub() {
        @Override
        public void sell() throws RemoteException {
            System.out.println("客官,您需要点什么?");
        }
    };


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

3.3.2客户端

创建自定义一个类实现ServiceConnection

private class  XiayuConnection implements ServiceConnection{

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
       //绑定成功时会调用这个方法
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
}

绑定服务,当绑定成功时会走Connection的onServiceConnected方法,并把Binder传过来

mIntent = new Intent(this, AIDLService.class);
mXiayuConnection = new XiayuConnection();
bindService(mIntent, mXiayuConnection, BIND_AUTO_CREATE);

在onServiceConnected方法里面通过asInterface获取服务器传过来的对象,并调用服务端的方法

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    //获取到服务器传过来的对象
    IShop iShop = IShop.Stub.asInterface(service);
    try {
        iShop.sell();
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

现在客户端就可以调用sell方法来进行跨进程通信了,但目前只能传输基本数据类型的数据,那么如果想要传其他数据呢?那么我们接着往下讲

3.3.3 通过AIDL传送复杂数据

首先我们要知道AIDL支持那么数据类型

  • 1.基本数据类型
  • 2.实现了Parcelable接口的对象
  • 3.List:只支持ArrayList,并且里面的元素需要时AIDL支持的
  • 4.Map:只支持HashMap,并且里面的key和value都需要是被AIDL支持的

那么我们定义一个对象Product实现Parcelable接口,如何实现Parcelable我这里也不重复介绍了,如果不了解的朋友可以看看笔者之前写的这篇文章

序列化--Serializable与Parcelable

http://blog.csdn.net/yulyu/article/details/56481665

Product我们设置了两个字段

public class Product implements Parcelable {
    public String name;
    public int    price;
    ...
}

接着我们需要在aidl文件夹的相同目录创建一个相同文件名的aidl文件

(注意,这里我们是要通过new File的方式创建,并且要自己输入文件后缀aidl,如果你用new AIDL的方式创建的话,他会提示你Interface Name must be unique)

接着我们需要在这个aidl文件里面输入包名,并且声明一下变量为Parcelable类型

(注意,这里声明的时候是用小写的parcelable)

// Product.aidl
package com.xiayu.aidldemo;

parcelable Product;

我们回到之前的IShop.aidl,删掉之前的sell方法,并再创建两个新方法

// IShop.aidl
package com.xiayu.aidldemo;

import com.xiayu.aidldemo.Product;

interface IShop {

    Product buy();

    void setProduct(in Product product);

}

这里有三个需要注意的地方

(1)IShop.aidl虽然跟Product.aidl在同一个包下,但是这里还是需要手动import进来

(2)这里声明方法时,需要在参数前面增加一个tag,这个tag有三种,in,out,inout,这里表示的是这个参数可以支持的流向:

  • 1.in: 这个对象能够从客户端到服务器,但是作为返回值从服务器到客户端的话数据不会传送过去(不会为null,但是字段都没有赋值)
  • 2.out: 这个对象能够作为返回值从服务器到客户端,但是从客户端到服务器数据会为空(不会为null,但是字段都没有赋值)
  • 3.inout: 能从客户端到服务器,也可以作为返回值从服务器到客户端

用一张图来总结:

(不要都设为inout,要看需求来设置,因为会增加开销)

(3)默认实现Parcelable的模版只支持in ,如果需要需要支持out或inout需要手动实现readFromParcel方法

@Override
public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(this.name);
    dest.writeInt(this.price);
}
//手动实现这个方法
public void readFromParcel(Parcel dest) {
    //注意,这里的读取顺序要writeToParcel()方法中的写入顺序一样
    name = dest.readString();
    price = dest.readInt();
}

现在就可以在客户端中通过IShop调用方法来进行通信了

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    IShop iShop = IShop.Stub.asInterface(service);
    try {
        //调用方法进行通信
        iShop.setProduct(mProduct);
        Product buy = iShop.buy();
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

3.4 不同应用间的多进程通信(AIDL)

上面我们介绍了同一个应用内的进程间通信,接下来我们就来介绍不同应用之间的进程间通信

3.4.1 服务器端

首先我们需要把Product.java放到aidl目录相同名字的文件夹下(如果要提供服务给其他app,最好把需要的对象都放在aidl目录下,这样比较容易拷贝)

但是这个时候你运行程序的话,编译会提示说找不到Product,那是因为Android Studio默认会去java目录下找,这时候需要在build.gradle文件 android{ } 中间增加一段代码,让aidl目录里面的java文件也能被识别

sourceSets {
    main {
        java.srcDirs = ['src/main/java', 'src/main/aidl']
    }
}

接着我们为Service增加intent-filter,这样其他应用才能通过隐式意图绑定服务,服务器端的修改就结束了

<service android:name=".AIDLService"
         android:process=":xiayu">
    <intent-filter>
        <action android:name="action.xiayu"/>
    </intent-filter>
</service>

3.4.2 客户端

我们需要创建一个新的应用来作为客户端,并且把服务器端的aidl目录下的所有文件都拷贝过来,这里要注意的就是里面的目录不能改变,需要与以前一致

点击同步,Android Studio会自动生成相应的java文件供我们使用

这个时候我们需要通过隐式意图来绑定服务了

(注意:5.0以后隐式意图开启或者绑定service要setPackage,不然会报错)

    mIntent.setAction("action.xiayu");
    mIntent.setPackage("com.xiayu.aidldemo");

接下来的操作就和之前一样了,创建一个类实现ServiceConnection

private class  XiayuConnection implements ServiceConnection {

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        //TODO
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {

    }
}

绑定服务

bindService(mIntent, mXiayuConnection, BIND_AUTO_CREATE);

通过ServiceConnection的onServiceConnected里面的IBinder进行通信

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        IShop iShop = IShop.Stub.asInterface(service);
        try {
            iShop.setProduct(mProduct);
            Product buy = iShop.buy();
            System.out.println("buy=" + buy.price);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

解除绑定的时候释放资源

public void unbind(View v) {
    unbindService(mXiayuConnection);
    mXiayuConnection = null;
    mIShop = null;
}

这样我们就可以通过获得的IShop进行不同应用之间的进程间通信了

最后再提几点用到服务时需要注意的地方(很简单,但是有些人经常会忽略这几点)

  • 1: startService和stopService需要用同一个Intent对象
  • 2: bindService和unbindService需要用同一个ServiceConnection对象
  • 3: 5.0以后隐式意图开启或者绑定service要setPackage(包名)

热门文章

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

推荐阅读更多精彩内容