当前位置: 代码迷 >> Android >> Android进程间通信(IPC)惯用方式
  详细解决方案

Android进程间通信(IPC)惯用方式

热度:438   发布时间:2016-04-24 11:23:32.0
Android进程间通信(IPC)常用方式

进程间通信方式

Android开发中我们可以通过IntentContentProviders来实现进程间通信,如果不限于Android特有的话,我们还可以使用FileSocket等方式,反正只要进程间能交换信息就行了。

Intent,我们平时使用的时候好像都没感觉出是在进程间通信。其实Android中进程间的通信是非常频繁的,应用里打开一个新的Activity都涉及到了进程间通信,应用里调用打电话、调用浏览器等等都涉及到了。

实际上IntentContentProviders都是对Binder更高级别的抽象,方便我们平时使用。

常用方式

上面说到的一些方式都是系统经过高度封装的,而我们的业务需求可能比较特别,使用上面的方式可能不是特别适合,比如:“我们的音乐播放器希望在独立的进程中播放音乐”。

我们至少得控制音乐的开始、暂停、显示进程这些功能吧,那就需要进程间的通信了。这个时候使用系统经过高度封装的方式都好像显得不太灵活。根据官方文档我们发现有两种相对底层一些的方式,MessengerAIDL

在相对底层一点的进程间通信,Messenger是最简单的方式,Messenger会在单一线程中创建包含所有请求的队列,这样我们就不需要处理线程安全方面的事宜。

Messenger实际上是以AIDL作为其底层结构的。

Messenger的用法

  • 单向通信

    客户端进程相关代码:

public class MainActivity extends AppCompatActivity {    private Messenger mService = null;    // 绑定远程服务成功后相应回调方法    private ServiceConnection mServiceConnection = new ServiceConnection() {        // 绑定成功后会调用该方法        @Override        public void onServiceConnected(ComponentName name, IBinder service) {            mService = new Messenger(service);        }        @Override        public void onServiceDisconnected(ComponentName name) {            mService = null;        }    };    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Intent intent = new Intent(this, RemoteService.class);        // 绑定服务        bindService(intent, mServiceConnection, BIND_AUTO_CREATE);    }    /**     * 点击按钮向远程服务发送消息     * @param view     */    public void onClick(View view) {        // 获取一个what值为0的消息对象        Message msg = Message.obtain(null, 0);        try {            // 将消息对象通过Messenger传递到远程服务器            mService.send(msg);        } catch (RemoteException e) {            e.printStackTrace();        }    }    @Override    protected void onStop() {        super.onStop();        unbindService(mServiceConnection);    }}

布局XML代码就不贴了,很简单,就一个按钮。

上面的代码也很简单,就得我们平常绑定服务的做法是一样的,唯一的区别就是在绑定成功回调方法onServiceConnected()中我们根据返回的IBinder实例化了一个Messenger对象,当我们点击按钮的时候,通过该Messenger对象发送一个消息到远程服务端。

服务端进程代码:

/** * 远程服务端 */public class RemoteService extends Service {    // 用来处理客户端传过来的消息    class ServerSideHandler extends Handler {        @Override        public void handleMessage(Message msg) {            switch (msg.what) {                case 0:                    Log.e("Fiend", "我是远程服务端,我收到客户端传递过来的信息了。");                    break;            }        }    }    // 实例化一个Messenger对象,并传入Handler    final Messenger mMessenger = new Messenger(new ServerSideHandler());    /**     * 客户端绑定服务端的时候将调用该方法     * @param intent     * @return     */    @Override    public IBinder onBind(Intent intent) {        return mMessenger.getBinder();    }}

服务端的代码也很简单,创建一个Handler来处理消息,实例化一个Messenger与该Handler关联,最后通过onBind()方法将MessengerIBinder给返回。在客户端通过该IBinder重建一个Messenger

我们来看一下运行结果:

调用远程结果

确实成功了,而且也确实是在两个进程间。想要让服务运行在别的进程只需要声明的时候指定它的android:process属性就可以了。

但是我们只是客户端向服务端发送了信息,那服务端如何向客户端发送信息呢?

  • 双向通信

    客户端改动地方:

/**     * 点击按钮向远程服务发送消息     * @param view     */    public void onClick(View view) {        // 获取一个what值为0的消息对象        Message msg = Message.obtain(null, 0);        // 将客户端的Messenger对象放到消息中传递到服务端        msg.replyTo = new Messenger(new Handler() {            @Override            public void handleMessage(Message msg) {                switch (msg.what) {                    case 1:                        Log.e("Fiend", "我是客户端,收到服务端的回复了");                        break;                }            }        });        try {            // 将消息对象通过Messenger传递到远程服务器            mService.send(msg);        } catch (RemoteException e) {            e.printStackTrace();        }    }

客户端代码只需要在发送消息之前将本地的一个Messenger对象放到消息里一起传递到远程服务端即可。

服务端改动地方:

    // 用来处理客户端传过来的消息    class ServerSideHandler extends Handler {        @Override        public void handleMessage(Message msg) {            switch (msg.what) {                case 0:                    Log.e("Fiend", "我是远程服务端,我收到客户端传递过来的信息了。");                    try {                        // 通过客户端的Messenger回复一个what值为1的消息                        msg.replyTo.send(Message.obtain(null, 1));                    } catch (RemoteException e) {                        e.printStackTrace();                    }                    break;            }        }    }

服务端改动的代码也非常简单,只是在收到客户端消息的时候, 通过客户端的Messenger回复一个消息。这样就实现了本地客户端与远程服务端的通信了。

对于大多数的应用来说,Messenger就能满足IPC的需求了,完全没必要使用AIDL,而且MessengerAIDL简单得多。如果对于服务需要执行多线程处理的,则应使用AIDL,否则使用Messenger就可以了。

AIDL的用法

使用AIDL和使用Messenger的步骤基本上是类似的。使用AIDL需要自己定义好一个接口作为客户端和服务端通信的规则,手工写一个这样的接口比较复杂,所以Android给我们提供了一个工具来自动生成。

想要自动生成通信的接口,则需要创建一个以.aidl结尾的文件,然后按平常我们定义接口的方式做就好了。下面以Android Studio来讲解生成过程。

  1. 新建一个项目,名字随便取:AIDLExample
  2. 将工程目录结构以Android的形式展示:
    切换视图
  3. 点击项目,右键,新建一个AIDL文件:
    newaidl
  4. 打开新建的AIDL文件IMyAidlInterface.aidl,编写通信规则:
    AIDL内容
  5. 编写完IMyAidlInterface.aidl后,需要重新Build一下项目,然将工程目录结构以Project的形式展示,就可以找到生成的真正接口:
    这里写图片描述

至此AIDL接口就定义好了,剩下的步骤比较简单,和之前讲过的类似。

我们先来编写服务端,直接新建一个Service并在配置文件中将其配置为android:process=":remote",确保它运行在另一个进程中。

// 服务端public class MyService extends Service {    IMyAidlInterface.Stub iBinder = new IMyAidlInterface.Stub() {        // 我们在aidl文件中定义的通信规则        @Override        public String getMsg() throws RemoteException {            return "我来自MyService";        }    };    @Override    public IBinder onBind(Intent intent) {        return iBinder;    }}

代码很简单,在绑定的时候将带有我们自己定义的规则的IBinder返回给客户端。XXX.Stub iBinder = new XXX.Stub() {···}这样的写法是固定的,记住就好了,将XXX替换成你的AIDL接口名称就可以了。

我们来看一下客户端代码:

// 客户端public class MainActivity extends AppCompatActivity {    private IMyAidlInterface mService;    private boolean isBound;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        // 绑定服务端        Intent intent = new Intent(this, MyService.class);        bindService(intent, mServiceConnection, BIND_AUTO_CREATE);    }    // 绑定回调    private ServiceConnection mServiceConnection = new ServiceConnection() {        @Override        public void onServiceConnected(ComponentName name, IBinder service) {            // 获取AIDL接口对象,这样就可以用来通信了            mService = IMyAidlInterface.Stub.asInterface(service);            isBound = true;        }        @Override        public void onServiceDisconnected(ComponentName name) {            mService = null;            isBound = false;        }    };    // 按钮点击回调方法    public void btnClick(View view) {        if (isBound) {            try {                // 调用服务端方法                String result = mService.getMsg();                Log.e("Fiend", "客户端:" + result);            } catch (RemoteException e) {                e.printStackTrace();            }        } else {            Log.e("Fiend", "还没有绑定成功");        }    }    @Override    protected void onDestroy() {        super.onDestroy();        if (isBound) {            unbindService(mServiceConnection);        }    }}

看起来代码有点多,其实并没有什么陌生的内容,都是我们平时非常熟悉的一些代码。应用启动后就绑定远程服务端,点击按钮调用远程服务端的方法,获取到后将结果打印出来。结果如下:

result

成功调用另一个进程中的方法。

onServiceConnected()方法里的这句代码mService = IMyAidlInterface.Stub.asInterface(service);,属于固定写法,和之前的服务端写法一样,记住就好了。

上面这种IPC方式是属于同步的,所谓同步是指,客户端调用后会等待服务端返回后才会继续向下执行。我们来修改一下客户端代码:

    public void btnClick(View view) {        if (isBound) {            try {                Log.e("Fiend", "开始调用服务端方法");                // 调用服务端方法                String result = mService.getMsg();                Log.e("Fiend", "客户端:" + result);            } catch (RemoteException e) {                e.printStackTrace();            }        } else {            Log.e("Fiend", "还没有绑定成功");        }    }

没有改什么实质性的,只是在调用服务端方法之前打印了一个Log,方便我们之前对比时间用。

改一下服务端的代码:

    IMyAidlInterface.Stub iBinder = new IMyAidlInterface.Stub() {        // 我们在aidl文件中定义的通信规则        @Override        public String getMsg() throws RemoteException {            // 5秒后再返回结果            SystemClock.sleep(5 * 1000);            return "我来自MyService";        }    };

同样没有修改多少,只是延迟5秒再返回结果。我们来看一下打印结果:

time

从截图可以看出,客户端确实等服务端返回后再继续执行的,所以是同步。因此,时刻记住客户端调用的时候在工作线程调用,否则有可能阻塞主线程。那想要异步该如何做?

异步调用

想要以AIDL方式异步调用,需要用到关键字oneway,它可以作用在接口上也可以作用在方法上。异步方法必须返回void

  • 异步接口
// 所有方法都是异步的oneway interface IAsynchronousInterface {    void method1();    void method2();}
  • 异步方法
interface IAsynchronousInterface {    // 这个方法是异步执行的    oneway void method1();    void method2();}

异步已经可以了,那结果如何返回呢?通常异步都是以回调接口的方式,在这里也是一样的。我们修改上面的之前演示的示例,增加一个回调接口,方便服务端调用客户端的方法,也就是所谓的反向调用。

增加一个回调AIDL接口定义:

callback

增加回调接口必须重新建立一个.aidl结尾的文件,IMyAidlInterfaceCallback.aidl具体内容如下:

// 用于服务端回调interface IMyAidlInterfaceCallback {    // 结果处理    void handleResult(String result);}

修改IMyAidlInterface.aidl的内容:

import com.fiend.aidlexample.IMyAidlInterfaceCallback;// 和我们平常定义一个接口语法一样oneway interface IMyAidlInterface {    // 定义了一个方法(所谓的通信规则)    void getMsg(IMyAidlInterfaceCallback callback);}

getMsg()方法的返回改为void,并将新定义的回调接口作为参数。这里必须显示import接口,否则编译会报错。

修改服务端部分代码:

    IMyAidlInterface.Stub iBinder = new IMyAidlInterface.Stub() {        @Override        public void getMsg(IMyAidlInterfaceCallback callback) throws RemoteException {            // 5秒后再返回结果            SystemClock.sleep(5 * 1000);            // 通过回调接口返回结果            callback.handleResult("我是异步返回,我来自MyService");        }//        // 我们在aidl文件中定义的通信规则//        @Override//        public String getMsg() throws RemoteException {//            // 5秒后再返回结果//            SystemClock.sleep(5 * 1000);//            return "我来自MyService";//        }    };

注释掉的部分是我们之前的做法,现在是通过回调接口返回结果。

修改客户端部分代码:

    /**     * 回调接口     */    private IMyAidlInterfaceCallback.Stub mCallback = new IMyAidlInterfaceCallback.Stub() {        @Override        public void handleResult(String result) throws RemoteException {            Log.e("Fiend", "客户端:" + result);        }    };    public void btnClick(View view) {        if (isBound) {            try {                Log.e("Fiend", "开始调用服务端方法");//                String result = mService.getMsg();//                Log.e("Fiend", "客户端:" + result);                // 调用服务端方法,将回调接口传过去                mService.getMsg(mCallback);                Log.e("Fiend", "结束调用服务端方法");            } catch (RemoteException e) {                e.printStackTrace();            }        } else {            Log.e("Fiend", "还没有绑定成功");        }    }

增加了一个回调接口,修改了调用服务端方法,之前是调用getMsg()并返回结果,现在是调用getMsg(mCallback)把回调方法传过去,没有返回值。返回结果在回调接口中处理。

我们来看一下运行结果:

result

通过返回时间对比,可以看到,调用完远程服务方法就立刻返回了。而需要返回的数据是在5秒后通过回调接口返回的。

至此,我们就实现了AIDL方式的异步调用了。

AIDL支持的数据类型

AIDL默认支持这么几种数据类型:

  • Java基本数据类型,如intlongboolean等(除了short)
  • String类型
  • CharSequence
  • List类型,所有List中的元素必须是AIDL支持的类型,如List<String>
  • Map类型,所有Map中的元素必须是AIDL支持的类型,如Map<String, Integer>

ListMap的接收方类型必须为ArrayListHashMap

如果默认的类型不能满足你的需要,还可以自定义类型,自定义类型必须支持序列化,也就是实现Parcelable接口。具体可以参考官网。

以上我们介绍的AIDL用法都是在同一个工程里,只是将Service指定运行在了不同的进程中,因此我们的.aidl文件可以只写一份,但是,如果我们的Service是在另一个应用(apk)中,那么另一个应用中也必须有和我们项目中相同的.aidl文件,连包名也必须一样。

总结

Android中实现进程间通信在高层次抽象可以很方便的使用Intent等方式来操作,相对底层的方式我们可以使用MessengerAIDL,大多数情况下我们使用Messenger就可以达到我们想要的效果了,而且使用也比AIDL简单,所以尽量用Messenger,实在不行再考虑AIDL,在介绍AIDL的时候对于支持的数据类型并没有深入的讲解与演示,可以上官网看看。

参考文献

  • 官方文档
  • Android Binder by Thorsten Schreiber from Ruhr-Universit?t Bochum
  • Deep Dive into Android IPC/Binder Framework at Android Builders Summit 2013
  • Efficient Android Threading by Anders Goransson
  相关解决方案