Android 跨进程通信(IPC)机制的探索与研究

概述

Inter Process Communication(IPC),指的就是跨进程通信,操作系统中对资源的分配和调度以进程为基本单位,不同进程之间是不能访问其它进程的内存空间地址的,所以当涉及到进程间通信时,需要进行特殊的处理,也就是IPC机制

Android中IPC机制的由来

Android开发中,我们有时候会碰到需要实现跨进程通信的需求,比如两个不同App应用之间的通信,同一个应用里运行在不同进程下的组件之间的通信,都是两个不同进程之间进行通信的场景

Android系统是基于Linux内核的,在Android4.4,也就是L版本之前,APP是运行在一个独立的虚拟机Dalvik上,Android4.4版本之后,改为运行在Android Run Time(ART)中,不管是Dalvik或ART,他们都允许同时运行多个虚拟机实例,每一个应用就是一个进程,它运行在一个单独的Dalvik/ART虚拟机中

Linux内核下的跨进程通信方式有管道、信道通信等很多种传统的方式,但是Android并没有采用Linux的通信方式,而是采用了Binder机制来实现通信,事实上Binder并不是Android独创的,它基于OpenBinder的模式设计

了解Binder的工作原理对于我们理解IPC机制非常有帮助
想要详细了解的,推荐老罗的Binder学习文章,从低层源码的角度分析Binder的运行机制

Android进程间通信(IPC)机制Binder简要介绍和学习计划

Android中的跨进程场景

Android中的跨进程通信普遍的两种表现形式

  • 两个不同应用之间的通信
  • 一个应用中不同进程的通信

(1)两个不同应用之间的通信

比如有两个App01和App02,其中App01作为服务端,App02作为客户端,他们之间需要进行数据的交互,这里最有代表性质的就如同Android中的ContentProvider内容提供者,App01通过注册内容提供者向App02提供数据,这就是一种很常见的跨进程通信方式,它是基于基于AIDL的,内部实现复杂,但是使用起来很方便,帮我们节省了很多的步骤

(2)一个应用中不同进程的通信

跨进程通信的场景不只局限于两个APP之间,同样在一个APP中,也是可以让不同的组件,如Activity、Service等运行在不同的进程中
在Android中,同一个应用,需要实现多进程非常简单,只需要在配置文件中通过给四大组件ActivityServiceContentProviderReceiver使用android:process属性即可开启多进程

如百度地图提供的定位服务就是使用到了多进程方式

<service
    android:name="com.baidu.location.f"
    android:enabled="true"
    android:process=":remote" >
</service>

android:process属性的命名是有规律的,主要有两种写法

  • 以“:”号开头时
    是一种简便的写法,指的是以当前进程名称,也就是当前包名作为前缀,加上冒号后面的字符,组成一个完整的进程名称如当前的包名是“com.example.zhu”,那么通过“:”组合在一起的完整进程名就是“com.example.zhu:remote”

  • 不以":"开头时
    不以冒号开头,那么进程的名称就为android:process属性值的名称,比如android:process="zhu.wen.tao.process",那么创建出来的进程名也就是“zhu.wen.tao.process”

进程间通信的影响

由于每个进程都有自己独立的内存空间,所以两个进程之间,是无法访问对方的内存地址的,这也就造成了基于内存存储的数据无法进行共享访问,比如基于静态成员变量的单例模式,基于全局锁的线程同步机制等将失效,基于单进程创建的Applicaion也将多次创建,基于文件读写的ShaedPreferences可能会由于并发访问而变得不可靠

上面的种种问题表明,想在Android中使用多进程并没有想象中的那么简单,要对使用多进程造成的影响进行全面的评估与适配,为了应对上述问题,Android为我们提供了一些跨进程的通信方法

跨进程通信方式

Android中有六种方式可以来实现跨进程通信

  • 文件共享
  • Bundle
  • AIDL
  • Messenger
  • ContentProvider
  • Socket

文件共享

文件共享的方式是最简单的一种实现方法了,通过将数据写入到一个文件中,然后不管哪个进程都可以对这个文件进行读取访问,以此来达到跨进程通信的目的

也可通过序列化将某个对象数据持久化保存到文件中,然后另一个进程再进行反序列化来获取这个文件保存的对象数据,也可以达到跨进程通信的目的

不过通过文件共享的方法局限性太大,实际开发中使用到的场景非常有限,因为当多个进程对同一份数据文件进行并发访问时,数据的可靠性非常的低,如果没处理好并发访问的情况,将会出现数据读取不完整或者错误的情况,并且在Android中的I/O操作性能并不高,如果过于频繁的话,对资源的消耗量是一个巨大的负担

Bundle

我们在开发过程中肯定都使用过Intent来传递数据吧
我们将GuideActivity在配置文件中通过添加android:process让其运行在一个新的进程中,然后从MainActivity通过Intent启动GuideActivity,并传递一些数据

Intent intent = new Intent(MainActivity.this, GuideActivity.class);
intent.putExtra(GuideActivity.TAG_TYPE, 666);
intent.putExtra(GuideActivity.TAG_URL, "file:///android_asset/zhuwentao.html");
startActivity(intent);

以上代码也可以写成:

Intent intent = new Intent(MainActivity.this, GuideActivity.class);
Bundle bundle = new Bundle();
bundle.putInt(GuideActivity.TAG_TYPE, 888);
bundle.putString(GuideActivity.TAG_URL, "file:///android_asset/zhuwentao.html");
intent.putExtras(bundle);
startActivity(intent);

这个是在启动Activity时传递Bundle数据的方式,实际上它就可以实现跨进程传递数据了,事实上Intent内部就是通过将数据封装成Bundle来传递的,它做好了封装,让我们只需要调用putExtra方法就可以传递不同类型的值了

深入Intent的源码中可以发现它的内部是将数据封装成了Bundle再进行传递的,而Bundle实现了Parcelable接口,也就是说通过Bundle保存的数据是支持序列化和反序列化的,将支持在不同进程间传递数据

根据以上结论,也就明白了为什么在不同进程之间通信时,使用Intent传递数据可以连接不同进程里的Activity、Service、BroadcastReceiver了

AIDL

AIDL(Android Interface Definition Language)指的就是接口定义语言,通过它可以让客户端与服务端在进程间使用共同认可的编程接口来进行通信

AIDL使用的步骤相对较多,主要总结为三个基本步骤:

  • 创建AIDL接口
  • 根据AIDL创建远程Service服务
  • 绑定远程Service服务

(1)创建AIDL接口

  • 定义aidl接口文件

在Android Studio中已经集成好了这个文件的创建方式,直接右击工程,点击New -> AIDL -> AIDL File,然后输入接口的名称就好,将会在src/main目录下创建一个与java目录平级,且里面的包名与java目录里的包名一致,后缀为.aidl的文件

// MyAidlTest.aidl
package demo.csdn.zhuwentao;

// Declare any non-default types here with import statements

interface IMyAidlTest {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

上面这个文件是Android Studio自动创建的模版文件,里面的basicTypes方法不需要使用到可以删掉
AIDL对数据类型的支持包括Java中的所有基本数据类型,还有StringCharSequenceListMap

  • 自定义AIDL的数据类型

在AIDL提供的默认数据类型无法满足需求的情况下,就需要自定义数据类型了
比如我们有个Product类,需要用来传递数据,那么这个类必须要实现Parcelable接口,并在AIDL中新建一个相同类名的aidl文件进行声明,并且这个aidl文件所在的路径必须要和java文件里的实体类路径保持一致,如以下文件Product.aidl

package demo.csdn.zhuwentao.bean;

parcelable Product;

然后在IMyAidlTest.aidl中使用import导入进来,除了AIDL默认支持的数据类型外,其它自定义的类型都需要通过此方法导入进来,包名路径需要精确到类名

package demo.csdn.zhuwentao;

// 如果需要使用到自定义的数据类型,则需要导入包文件,路径需要精确到包名
import demo.csdn.zhuwentao.bean.Product;

interface IMyAidlTest {

    void addProduct(in Product person);

    List<Product> getProductList();
}

这里的方法只作为接口声明的作用,以上定义的接口最终会在Service服务里实现具体的操作逻辑

  • 根据aidl文件生成java接口文件

这个步骤Android Studio已经帮我们集成好了,只需要点击 Build -> Make Project,或者点击AS上的那个小锤子图标就可以,构建完后将会自动根据我们定义的IMyAidlTest.aidl文件生成IMyAidlTest.java接口类,可以在build/generated/source/aidl/debug/路径下找到这个类

(2)根据AIDL创建远程Service服务

上一步中创建好的IMyAidlTest.java接口文件,需要使用Service来进行绑定,这里就需要我们新建一个Service服务

/**
 * 根据AIDL创建远程Service服务端
 * Created by zhuwentao.
 */
public class MyAidlService extends Service {

    private List<Product> mProducts;

    public MyAidlService() {
    }

    private IBinder mIBinder = new IMyAidlTest.Stub() {

        @Override
        public void addProduct(Product product) throws RemoteException {
            mProducts.add(product);
        }

        @Override
        public List<Product> getProductList() throws RemoteException {
            return mProducts;
        }
    };

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        mProducts = new ArrayList<>();
        return mIBinder;
    }
}

mIBinder对象实例化了IMyAidlTest.Stub,并在回调接口中实现了最终的处理逻辑

当与客户端绑定时,会触发onBind()方法,并返回一个Binder对象给客户端使用,客户端就可以通过这个类调用服务里实现好的接口方法

记得要在配置文件中加入声明,并使用android:process属性指定其运行在新的进程中

<service
    android:name=".MyAidlService"
    android:process=":process"/>

配置好以上步骤后,跨进程通信的服务端就配置好了

(3)绑定远程Service服务

跨进程通信服务端实现好了后,就可以在客户端中开始调用它了,首先在Activity中先创建好服务连接对象

private IMyAidlTest mAidlTest;

private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        mAidlTest = IMyAidlTest.Stub.asInterface(service);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        mAidlTest = null;
    }
};

再通过Intent的bindService来绑定Service服务,建立起连接

Intent intent = new Intent(getApplicationContext(), MyAidlService.class);
bindService(intent, mServiceConnection, BIND_AUTO_CREATE);

启动成功后,onServiceConnected方法将会在建立连接时被回调,回调时将生成一个接口实现mAidlTest对象,这个对象就是我们进行跨进程操作调用对象
接下来就是通过这个mAidlTest对象来操作AIDL方法就好了

private void addProduct(String name, int price) {
    Product pro = new Product();
    pro.mName = name;
    pro.mPrice = price;

    try {
        // 通过mAidlTest调用AIDL方法
        mAidlTest.addProduct(pro);
        List<Product> proLists = mAidlTest.getProductList();

        mAIDLTv.setText(proLists.toString());
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

以上就是AIDL使用的基本步骤了

AIDL的使用相对来说比较复杂,光靠这一点点文字是无法详细介绍的,需要详细了解的可以去查看Google的官方文档,介绍的非常详细

https://developer.android.com/guide/components/aidl.html

对于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.

它适用于并发操作情况比较多,需要对数据交互比较频繁的场景,如果对并发的需求并不高,推荐我们使用Messenger的方式进行通信

Messenger

Messenger是一种基于消息的进程间通信方式,其内部是使用了一个消息队列来保存通信时的所有消息,并按先进先出的顺序处理请求的消息,并串行执行

具体使用方法有三个基本步骤

  • 创建服务端Messenger
  • 创建客户端Messenger
  • 绑定远程服务端

(1)创建服务端Messenger

创建一个新的服务运行在另外一个进程下,并在其中创建好服务端Messenger

/**
 * 其它进程下的远程服务端Service
 * Created by zhuwentao.
 */
public class MsgerService extends Service{
    public static final int TAG_SHOW_MSG = 100;
    private Messenger mMessenger = new Messenger(new Handler() {

        @Override
        public void handleMessage(Message msg) {
            // 准备发送给客户端的消息
            Message cMsg = Message.obtain();
            switch (msg.what) {
                case TAG_SHOW_MSG:

                    cMsg.what = msg.what;
                    Bundle bundle = new Bundle();
                    bundle.putString("name", "zhuwentao");
                    bundle.putInt("age", msg.arg1);
                    cMsg.obj = bundle;

                    try {
                        // 调用客户端传递过来的Messenger对象发送消息
                        msg.replyTo.send(cMsg);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
            }
        }
    });

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

Messenger中服务端与客户端是依靠Message对象来进行通信的,Message类也继承了Parcelable,所以能满足跨进程传递的要求

在需要一次性传递比较多的数据参数时,可以使用Bundle封装好数据,然后传递给客户端

onBind()方法中我们返回时调用了Messenger的getBinder()方法,深入其源码中可以发现其内部的实现中返回的是一个继承自IMessenger.StubMessengerImpl对象,这和上面介绍AIDL远程服务端里继承自IMyAidlTest.StubmIBinder是一样的,都是AIDL的实现方式

(2)创建客户端Messenger

在客户端Activity中创建Messenger,用于接收和处理来自远程服务端传递的消息

// 客户端的Messenger
private Messenger mClientMessenger = new Messenger(new Handler(){
    @Override
    public void handleMessage(Message msg) {
        // 处理接收到的远程服务端消息
        switch (msg.what) {
            case MsgerService.TAG_SHOW_MSG:
                Bundle bundle = (Bundle) msg.obj;
                mShowMsgTv.setText(mShowMsgTv.getText().toString() + "\n" + bundle.getString("name") + "、" + bundle.getInt("age", 0));
                break;
        }
    }
});

(3)绑定远程服务端

创建连接远程服务的ServiceConnection

// 是否能继续发送消息
private boolean isCanSend;

// 远程服务端的Messenger
private Messenger mServiceMessenger;

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        // 得到远程服务端的Messenger对象
        mServiceMessenger = new Messenger(iBinder);
        isCanSend = true;
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        mServiceMessenger = null;
        isCanSend = false;
    }
};

onServiceConnected()方法里,使用的new Messenger(iBinder),查看里面的源码实现方式IMessenger.Stub.asInterface()和我们用AIDL来实现的方式是一样的

得到远程服务端里的Messenger后,客户端里就可以通过这个mServiceMessenger给远程服务端发送消息了

创建好连接类后,再通过bindService绑定好服务

Intent intent = new Intent(getApplicationContext(), MsgerService.class);
bindService(intent, mConnection, BIND_AUTO_CREATE);

这些搞定好后,就可以开始进行通信了

// 绑定服务后就可以开始进行通信
mAddDataBtn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        int number = (int) (Math.random() * 100);
        Message msg = Message.obtain();
        msg.what = MsgerService.TAG_SHOW_MSG;
        msg.arg1 = number;
        msg.replyTo = mClientMessenger;

        try {
            if (isCanSend) {
                mServiceMessenger.send(msg);
            }
        } catch (RemoteException e) {
            e.printStackTrace();
        }

    }
});

里面比较关键一点的就是msg.replyTo,这个方法赋值的是一个客户端Messenger,传递给服务端后,服务端就将持有客户端的Messenger,也就是一个Binder对象,服务端就可以利用这个Binder对象和客户端进行通信

使用方法相对于AIDL来说简单很多,如果深入其代码,可以发现它实际上底层就是通过封装AIDL来实现的,只是帮我们省略了编写AIDL的步骤,所以使用Messenger来实现会比较简单一些

ContentProvider

ContentProvider(内容提供者)为我们提供访问数据的统一格式,它封装了底层的具体实现逻辑。数据来源可以是文件、数据库等,我们使用时只需要简单的使用它提供的四个数据操作接口insert(增加)、delete(删除)、update(修改)、query(查询)就可以了,使用起来很简单,这里就不贴代码了

作为Android的四大组件之一,它估计是我们在开发时使用的最少的一个了,不过相信大家在开发中使用到最多的地方,就是用它来读取手机里的通讯录联系人吧,通讯录作为一个单独的APP,被我们的APP通过getContentResolver()得到ContentResolver进行增删改查,也就是两个应用之间的通信了

ContentProvider的底层实现也是Binder,为了满足特定的需求而进行了封装,使其能够胜任对数据交互不频繁时的IPC通信

Socket

Socket俗称套接字,基于TCP/IP协议的通信方式,对TCP/IP协议进行封装,并对外提供调用接口,通过Socket,才能在Android平台上通过TCP/IP来进行开发
Socket主要分为两种

  • StreamSocket:基于TCP协议的封装,以流的方式提供数据交互服务,提供了稳定的双向通信,通过“三次握手”建立连接,传输数据具有较高的稳定性
  • DatagramSocket:基于UDP协议的封装,以数据报文的方式提供数据交互服务,提供了不稳定的单向通信,具有更好的执行效率,由于基于无连接的方式,传输数据不稳定,不保证数据的完整性

从性能上来说,基于无连接的UDP要比基于连接的TCP快很多,因为UDP不需要经过“三次握手”的过程,且不需要每次连接等待服务端响应,节省了很多步骤和时间,但稳定性UDP就比TCP要差很多,二则适用于不同的使用场景,比如实时的视频传输服务就适合采用UDP,UDP适合对数据完整性需求不高,对执行效率要求高的场景,而TCP则适用于对数据交互完整性要求高的场景,Android中使用Socket来进行IPC时建议采用TCP方式,因为跨进程通信对数据的完整性和稳定性要求比较高

套接字平时用的最多的地方就是网络通信,通常是在不同主机之间进行通信时使用,也可用于同主机下不同进程间通信来使用,Socket支持任意字节流的传输,相比于Messenger只能使用Message对象来传递数据的方式灵活的多

总结

  • 文件共享:适用于进程间没有实时数据交互的场景,且没有并发访问的场景,用法简单,但无法满足进程间的实时通信

  • Bundle:适用于四大组件下跨进程通信,用法简单,但只能支持Bundle默认支持的数据类型

  • AIDL:适用于进程间对实时通信要求比较高,客户端数量不止一个,数据交互非常频繁的场景,用法相对复杂,但对多线程访问的场景支持不好

  • Messenger:适用于有串行实时通信需求,低并发的通信场景,用法相对AIDL简单,但是支持的数据类型少,通过Message传输数据,只支持Bundle默认有的数据类型,有一定的局限性

  • ContentProvider:适用于需要向多个客户端提供共享数据,无实时通信需求的场景,用法简单,但使用场景比较单一

  • Socket:适用于一对多的并发通信,实时通信需求较高,需要传输的数据类型比较多样化的场景,用法复杂烦琐,需要客户端和服务端都保持长连接,对资源的消耗比较大

每一种方式都有其存在的意义,在实际开发时,需要对这几种方法的优点和缺点进行深入的评估,结合实际需求,选择最适合的IPC通信方式

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

推荐阅读更多精彩内容