你了解android的进程间通信吗?提到进程间通信,这是android开发中很重要的一环,也是面试高频知识点,本文总结一下进程间通信的几种方式,把这块知识系统起来。

什么是进程间通信

进程间通信(IPC,Interprocess communication)是一组编程接口,让程序员能够协调不同的进程,使之能在一个操作系统里同时运行,并相互传递、交换信息。

如何开启多进程

正常情况下,在Android中多进程是指一个应用中存在多个进程的情况,因此这里不讨论两个应用之间的多进程情况。在Android中上层应用开发使用多进程唯一一种方法,那就是给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidMenifest中指定android:process属性

...
<activity
    android:name="com.ryg.chapter_2.SecondActivity"
    android:configChanges="screenLayout"
    android:label="@string/app_name"
    android:process=":remote" />

<activity
    android:name="com.ryg.chapter_2.ThirdActivity"
    android:configChanges="screenLayout"
    android:label="@string/app_name"
    android:process="com.ryg.chapter_2.remote" />

上面的示例分别为SecondActivity和ThirdActivity指定了process属性,并且它们的属性值不同,这意味着当前应用又增加了两个新进程。

其中SecondActivity和ThirdActivity的android:process属性分别为“:remote”和“com.ryg.chapter_2.remote”,这两种方式是有区别的:

  • 首先,“:”的含义是指要在当前的进程名前面附加上当前的包名,这是一种简写的方法,对于SecondActivity来说,它完整的进程名为com.ryg.chapter_2:remote,而对于ThirdActivity中的声明方式,它是一种完整的命名方式,不会附加包名信息;
  • 其次,进程名以“:”开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以“:”开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。

Android系统会为每个应用分配一个唯一的UID。通常,不同的APK会具有不同的userId,因此运行时属于不同的进程中,而不同进程中的资源是不共享的,然后有些时候,我们自己公司开发了多个APK并且需要他们之间互相共享资源,那么就可以通过设置相同的shareUserId和相同的签名来实现这一目的。

多进程影响

我们知道Android为每一个应用分配了一个独立的虚拟机,或者说为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本

所以,所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。一般来说,使用多进程会造成如下几方面的问题:

  • (1)静态成员和单例模式完全失效。
  • 静态成员属于类,不属于对象
  • 因为单例模式想要保证全局只有一个对象,多进程会有多个副本了,那也就不能保证全局只有一个对象了,也就失去了作用了
  • (2)线程同步机制完全失效。
  • (3)SharedPreferences的可靠性下降。
  • (4)Application会多次创建。

IPC有哪些方式

有问题就要有解决,为了解决这些问题,系统提供了很多跨进程通信方法,虽然说不能直接地共享内存,但是通过跨进程通信我们还是可以实现数据交互。

关于Android中的进程间通信,大概可以通过以下方式进行:

  • Bundle:四大组件间通信
  • File:文件共享
  • AIDL:Binder机制
  • Messager:基于AIDL、Handler实现
  • ContentProvider:应用间数据共享
  • Socket:建立C/S通信模型

使用Bundle

四大组件中的三大组件(Activity、Service、Receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输,这里说明一点ContentProvider的call方法可以使用Bundle,而在其他方法中不能使用。

另外,Bundle不支持的类型我们无法通过它在进程间传递数据,这不用说了。

使用File(文件共享)

共享文件也是一种不错的进程间通信方式,两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。

由于Android系统基于Linux,使得其并发读/写文件可以没有限制地进行,甚至两个线程同时对同一个文件进行写操作都是允许的,那么我们读出的内容就有可能不是最新的,如果是并发写的话那就更严重了。

普通文件并发执行,如果妥善处理并发的速度和频率或许可以保证数据的准确性。然而SharedPreferences是个特例,从本质上来说,SharedPreferences也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写访问,Sharedpreferences有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences

使用Messenger

Messenger可以翻译为信使,顾名思义,通过它可以在不同进程中传递Message(信息)对象,在Messenger(信使)中进行数据传递必须将数据放入Message(信息)中,而Messenger和Message都实现了Parcelable接口,因此可以跨进程传输。

Messenger是一种轻量级的IPC方案,它的底层实现是AIDL:

public Messenger(Handler target) {
     mTarget = target.getIMessenger();
}

public Messenger(IBinder target) {
     mTarget = IMessenger.Stub.asInterface(target);
}

Messenger的使用方法很简单,它对AIDL做了封装,使得我们可以更简便地进行进程间通信。同时,由于它一次处理一个请求,因此在服务端我们不用考虑线程同步的问题,这是因为服务端中不存在并发执行的情形。

Messager一般用在同一个应用多个不同进程服务的通信处理,下边通过代码描述一下服务端和客户端两个过程Messager通信实现过程:

服务端进程

public class MessengerService extends Service {

    private static final String TAG = "MessengerService";

    //继承Handler,MessengerHandler用来处理客户端发送的消息,并从消息中取出客户端发来的文本信息。
    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
            case MyConstants.MSG_FROM_CLIENT:
                Log.i(TAG, "receive msg from Client:" + msg.getData().getString("msg"));
                /**
                 * 因为我们的Messenger是通过客户端来获取的,而在客户端那边这个Messenger是以Handler为参数创建的,
                 * 所以在服务端通过客户端的Messenger发送消息后,在客户端的Handler就会处理这条消息,
                 * 嘻嘻,就达到了消息传送的目的。
                 */
                Messenger client = msg.replyTo;
                Message relpyMessage = Message.obtain(null, MyConstants.MSG_FROM_SERVICE);
                Bundle bundle = new Bundle();
                bundle.putString("reply", "嗯,你的消息我已经收到,稍后会回复你。");
                relpyMessage.setData(bundle);
                try {
                    client.send(relpyMessage);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }

    //这是我们服务端自己的Messenger,它是以上面的Handler对象为参数创建的,
    //这个Messenger是要通过绑定该服务器的时候onBind方法传递给客户端,然后客户端获取了该Messenger,
    //再以该Messenger来发送消息,这样服务端就可以接收到该消息并处理。
    private final Messenger mMessenger = new Messenger(new MessengerHandler());

    //这个方法是在绑定服务的过程中调用的并将结果返回给客户端的,所以通过onBind方法客户端就可以获取我们Messenger的Binder对象了,
    //然后客户端可以根据该Binder对象来创建一个Messenger,这样客户端中用的Messenger和这里的Messenger就是向对应的了。
    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        return super.onStartCommand(intent, flags, startId);
    }
}

然后,注册service,让其运行在单独的进程中:

<service
      android:name="com.ryg.chapter_2.messenger.MessengerService"
      android:process=":remote" >

客户端进程

public class MessengerActivity extends Activity {

    private static final String TAG = "MessengerActivity";

    private Messenger mService;
    //准备一个接收消息的Messenger和Handler
    //这是客户端自己的Messenger,传递给服务端,让服务端返回消息用的。
    private Messenger mGetReplyMessenger = new Messenger (new MessengerHandler ());

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MyConstants.MSG_FROM_SERVICE:
                    Log.i (TAG, "receive msg from Service:" + msg.getData ().getString ("reply"));
                    break;
                default:
                    super.handleMessage (msg);
            }
        }
    }

    /**
     * 这个是客户端用来绑定服务端用的,在绑定过程中会调用onServiceConnected,它的第二个参数IBinder service,
     * 就是在服务端中onBind方法返回的结果,这个结果是服务端的Messenger对象的Binder对象,
     * 然后客户端通过这个Binder对象就可以创建一个Messenger,
     * 所以就是在绑定服务的过程中将服务端的Messenger传递给了客户端,建立起了两者之间的桥梁
     */
    private ServiceConnection mConnection = new ServiceConnection () {
        public void onServiceConnected(ComponentName className, IBinder service) {
            mService = new Messenger (service);
            Log.d (TAG, "bind service");
            Message msg = Message.obtain (null, MyConstants.MSG_FROM_CLIENT);
            Bundle data = new Bundle ();
            data.putString ("msg", "hello, this is client.");
            msg.setData (data);
            //把接收服务端回复的Messenger通过Message的replyTo参数传递给服务端
            msg.replyTo = mGetReplyMessenger;
            try {
                mService.send (msg);
            } catch (RemoteException e) {
                e.printStackTrace ();
            }
        }

        public void onServiceDisconnected(ComponentName className) {
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate (savedInstanceState);
        setContentView (R.layout.activity_messenger);
        Intent intent = new Intent ("com.ryg.MessengerService.launch");
        //在bindService的时候,服务端会通过onBind方法返回一个包含了服务端业务调用的Binder对象,
        //通过这个对象,客户端就可以获取服务端提供的服务或者数据,
        bindService (intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        unbindService (mConnection);
        super.onDestroy ();
    }
}

客户端的实现也比较简单,首先需要绑定远程进程的MessengerService,绑定成功后,根据服务端返回的binder对象创建Messenger对象并使用此对象向服务端发送消息。

Messenger的工作原理图:

android进程与进程通讯 进程间通信 android_服务端

Messenger是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用Messenger就不太合适了。同时,Messenger的作用主要是为了传递消息,很多时候我们可能需要跨进程调用服务端的方法,这种情形用Messenger就无法做到了。但是我们可以使用AIDL来实现跨进程的方法调用。

使用AIDL

AIDL,一种接口定义语言,用于约束两个进程之间的通讯规则,供编译器生成代码实现Android设备的IPC。AIDL也是Messenger的底层实现,因此Messenger本质上也是AIDL,只不过系统为我们做了封装从而方便上层的调用而已。

AIDL方式主要用于两个应用间数据传输和方法调用实现。这里简单描述一下服务端和客户端AIDL各自的工作:

  1. 服务端:服务端首先要创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在Service中实现这个AIDL接口即可
  2. 客户端:客户端所要做事情就稍微简单一些,首先需要绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。

AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以我们要在AIDL方法中处理线程同步,如果是用List存储对象,那么可更换为CopyonWriteArayList来进行自动的线程同步,下边实现一个aidl接口:

// 定义aidl接口
interface IBookManager {

    List<Book> getBookList();

    void addBook(in Book book);
}


//实现aidl接口
public class BookManagerService extends Service {

    private static final String TAG = "BookManagerService";

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();

    private Binder mBinder = new IBookManager.Stub() {

        @Override
        public List<Book> getBookList() throws RemoteException {
            return mBookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            mBookList.add(book);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();

        mBookList.add(new Book(1,"Android"));
        mBookList.add(new Book(1,"IOS"));
    }

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

AIDL工作原理图:

android进程与进程通讯 进程间通信 android_android_02

使用ContentProvider

ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,从这一点来看,它天生就适合进程间通信。和Messenger一样,ContentProvider的底层实现同样也是Binder,但是它的使用过程要比AIDL简单许多,这是因为系统已经为我们做了封装,使得我们无须关心底层细节即可轻松实现IPC。

系统预置了许多ContentProvider,比如通讯录信息、日程表信息等,要跨进程访问这些信息,只需要通过ContentResolver的query、update、insert和delete方法即可。

ContentProvider的使用可参看我之前的一篇文章《Android开发ContentProvider学习总结》

使用Socket

Socket也称为“套接字”,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议。

  • TCP协议是面向连接的协议,提供稳定的双向通信功能,TCP连接的建立需要经过“三次握手”才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传机制,因此具有很高的稳定性;
  • 而UDP是无连接的,提供不稳定的单向通信功能,当然UDP也可以实现双向通信功能。
  • 在性能上,UDP具有更好的效率,其缺点是不保证数据一定能够正确传输,尤其是在网络拥塞的情况下。

两个进程可以通过Socket来实现信息的传输,Socket本身可以支持传输任意字节流,很显然,这是一种IPC方式。

Socket的使用可参看我之前的一篇文章《Java开发揭开socket编程的面纱》

选择合适的IPC

不同IPC方式有不同适用场景,在实际的开发中,只要我们选择合适的IPC方式就可以轻松完成多进程的开发场景。

IPC方式的优缺点和适用场景表:

android进程与进程通讯 进程间通信 android_服务端_03

RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。特指一种隐藏了过程调用时实际通信细节的IPC方法。而IPC概念泛指进程之间任何形式的通信行为。

这里再对比上表总结一下:

  • 只有允许不同应用的客户端用 IPC 方式调用远程方法,并且想要在服务中处理多线程时,才有必要使用 AIDL
  • 如果需要调用远程方法,但不需要处理并发 IPC,就应该通过实现一个 Binder 创建接口
  • 如果您想执行 IPC,但只是传递数据,不涉及方法调用,也不需要高并发,就使用 Messenger 来实现接口
  • 如果需要处理一对多的进程间数据共享(主要是数据的 CRUD),就使用 ContentProvider
  • 如果要实现一对多的并发实时通信,就使用 Socket