Android 进程间通信

一、引言

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

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

本文主要探索的是AIDLSocket两种实现方式,并在日常使用的基础上根据AIDL所生成的代码分析 Binder跨进程通信机制,感兴趣的童鞋可以看看。

本文完整代码:AndroidIPCDemo

二、使用 AIDLSocket 进行通信

先来说说我们一会儿要实现的通信模型,大致如下图:

需要实现的通信模型

然后看看目录结构:


文件路径

再看看IMyAidlInterface.aidl,这里在定义方法名称的时候需要注意的是方法不能同名,包需要手动导入,可能是因为AIDL文件在解析时不会区分参数类型,导致我在设定同名方法时一直编译错误,搞得我一直找其他问题,所以这点需要注意一下:

// IMyAidlInterface.aidl
package com.project.horselai.bindprogressguarddemo;
// 需要手动导入的包
import com.project.horselai.bindprogressguarddemo.IMyAidlInterfaceCallback;
import com.project.horselai.bindprogressguarddemo.MyMessage;
interface IMyAidlInterface { 

    void sendMessage(String msg); 
    void sendMessageObj(in MyMessage msg); 
    int getProcessId(); 
    void registerCallback(IMyAidlInterfaceCallback callback); 
    void unregisterCallback(IMyAidlInterfaceCallback callback);
} 

然后是IMyAidlInterfaceCallback.aidl:

// IMyAidlInterfaceCallback.aidl
package com.project.horselai.bindprogressguarddemo;
  
interface IMyAidlInterfaceCallback {

    void onValueCallback(int value);
} 

最后是 MyMessage.aidl:

// MyMessage.aidl
package com.project.horselai.bindprogressguarddemo;

parcelable MyMessage;

其他代码太长就不贴出来了,具体请查看 AndroidIPCDemo

演示图如下,具体还是跑起来看看吧。

演示.gif
日志输出信息

这里有个现象就是,unbindService 调用之后,ServiceConnection 并没有中断,因此,如果此时再次发送消息也是能够发送和接收到的。

三、从AIDL生成类源码角度理解Binder机制

1. 先来点关于 IBinder 的理论知识

官方原文:IBinder

IBinder作为远程对象的基础接口,是为高性能执行跨进程和进程内调用设计的轻量级远程调用机制的核心部分,它描述了与远程对象交互时的抽象协议,使用时应该继承 Binder,而不应直接实现 IBinder接口。

IBinder的关键 APItransact(),它与 Binder.onTranscat()配对使用,当调用transcat()方法时会发送请求到IBinder对象,而接收到请求时是在Binder.onTranscat()中接收,transcat()是同步执行的,执行transcat()transcat()会等待对方Binder.onTranscat()方法返回后才返回,这种行为在同一进程中执行时是必然的,而在不同进程间执行时,底层IPC机制也会确保具备与之相同的行为。

transact()方法发送的是Parcel类型的数据,Parcel是一种通用数据缓冲,它包含一些描述它所承载内容的元数据(meta-data),这些元数据用于管理缓冲数据中的IBinder对象引用,因此这些引用可以被保存为缓冲数据而传递到其他进程。这种机制保证了IBinder能够被写入Parcel中并发送到其他进程,如果其他进程发送相同的IBinder引用回来给源进程,则说明源进程收到一个相同的IBinder对象,这种特性使IBinder/Binder对象能够作为进程间的唯一标识(作为服务器token或者其他目的)。

系统会为每个运行的进程维护一个事务线程池,线程池中的线程用于分发所有来自其他进程的IPC事务,例如,当进程A与进程B进行IPC时(此时A为发送进程),由于A调用transact()发送事务到进程B的缘故,A中被调用的线程会被阻塞在transact(),此时如果B进程中的可用线程池线程接收到了来自A的事务,就会调用目标对象(A进程)的Binder.onTranscat(),并回复一个Parcel作为应答。接收到来自B进程的应答后,在A进程中执行transact()的线程就会结束阻塞,从而继续执行其他逻辑。

Binder系统同样支持跨进程递归,例如,如果进程A执行一个事务到进程B,然后进程B处理接收到的事务时又执行了由进程A实现的IBinder.transact(),那么进程A中正在等待原事务执行结束的线程将用于执行由进程B调用的对象的Binder.onTranscat()应答。这种行为保证了递归机制在远程调用Binder对象和本地调用时行为一致。

通过以下三种方式可以确定远程对象是否可用:

  • 当调用一个不存在进程的IBinder.transact()时会抛出RemoteException异常;
  • 调用pingBinder()返回false时表示远程进程已经不存在;
  • 使用linkToDeath()方法给IBinder注册一个IBinder.DeathRecipient,那么当其承载进程被杀死时会通过这个监听器通知;

2. AIDL 生成类源码分析

先来看看生成类的结构:

生成类的结构

其中IMyAidlInterface接口是我们定义AIDL接口的直接代码生成,而IMyAidlInterface.Stub则是实现IMyAidlInterface接口的抽象类,实现了onTranscat()方法,不过它并没有具体实现IMyAidlInterface的方法,而是将这部分的实现交给了IMyAidlInterface.Stub.Proxy

OK,我们来具体分析一下。首先定位到Stub#asInterface,可见它主要负责区分当前进行的是本地通信还是跨进程通信。

public static com.project.horselai.bindprogressguarddemo.IMyAidlInterface asInterface(android.os.IBinder obj) {
    // 1. 查找本地是否存在这个 IBinder 对象
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.project.horselai.bindprogressguarddemo.IMyAidlInterface))) {
        // 如果是本地通信,则稍后进行本地通信
        return ((com.project.horselai.bindprogressguarddemo.IMyAidlInterface) iin);
    }
    // 2. 否则,稍后使用这个对象进行远程通信
    return new com.project.horselai.bindprogressguarddemo.IMyAidlInterface.Stub.Proxy(obj);
}

接着来看看Stub#onTranscat方法, 各参数作用如下

  • code: 标识需要执行的动作,是一个从FIRST_CALL_TRANSACTIONLAST_CALL_TRANSACTION之间的数字。
  • data: transcat()调用者发送过来的数据。
  • reply: 用于给transcat()调用者写入应答数据。
  • flags: 如果是 0,代表是一个普通RPC,如果是FLAG_ONEWAY则代表是一个one-way类型的RPC
  • return: 返回true代表请求成功了,返回false则表示你没有明白事务代码(code)。

基于前面的理论知识,我们已经知道进程A中的onTransact()会被进程B调用,用于远程回调应答数据,下面通过两个标志性的方法解释在onTransact()中都做了什么:

@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
    java.lang.String descriptor = DESCRIPTOR;
    switch (code) {
        // 对于远程写请求
        case TRANSACTION_sendMessage: {
            data.enforceInterface(descriptor);
            java.lang.String _arg0;
            // 1. 从进程A的远程请求包中读取请求数据
            _arg0 = data.readString();
            // 2. 执行进程B中的sendMessage方法写入来自进程A的数据
            this.sendMessage(_arg0);
            reply.writeNoException();
            return true;
        }
        // 对于远程读请求
        case TRANSACTION_getProcessId: {
            data.enforceInterface(descriptor);
            // 1. 执行进程B中的getProcessId() 读取需要作为响应数据的数据
            int _result = this.getProcessId();
            // 2. 将读取到的响应数据写入到进程A的应答中
            reply.writeNoException();
            reply.writeInt(_result);
            return true;
        }
        // ...
        default: {
            return super.onTransact(code, data, reply, flags);
        }
    }
}

可能你会对上面的this.sendMessage(_arg0)this.getProcessId()有所疑问,比如,为什么在TRANSACTION_sendMessage中还要执行this.sendMessage(_arg0),这不就死循环了吗? 不会的,为啥呢,因为TRANSACTION_sendMessage判断的是来自进程A的方法类型码,而在解析了来自进程A的请求参数data后会调用进程B自身的sendMessage(_arg0)方法将数据保存到自己的存储内存中,而它的sendMessage(_arg0)是有我们自己实现的,如下是我们在进程B中的实现:

IMyAidlInterface.Stub myAidlInterface = new IMyAidlInterface.Stub() {
    @Override
    public void sendMessage(String msg) throws RemoteException {
        Log.i(TAG, "sendMessage: " + msg);
    } 

    @Override
    public int getProcessId() throws RemoteException {
        return Process.myPid();
    }
};

到这是不是就很好理解了。

下面通过Proxy#sendMessageProxy#getProcessId两个与上面对应的方法来解释作为客户端的进程A是如何给远程作为服务端的B进程发送请求的:

@Override
public void sendMessage(java.lang.String msg) throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain(); // 请求参数
    android.os.Parcel _reply = android.os.Parcel.obtain(); // 响应数据
    try {
        // 1. 封装远程请求参数
        _data.writeInterfaceToken(DESCRIPTOR);
        _data.writeString(msg);
        // 2. 通过Binder执行远程请求,最终响应数据会封装在_reply
        mRemote.transact(Stub.TRANSACTION_sendMessage, _data, _reply, 0);
        // 3. 没有需要返回数据则仅读取异常
        _reply.readException();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
}

@Override
public int getProcessId() throws android.os.RemoteException {
    android.os.Parcel _data = android.os.Parcel.obtain(); // 请求参数
    android.os.Parcel _reply = android.os.Parcel.obtain(); // 响应数据
    int _result;
    try {
        // 1. 没有参数,则仅写入标识
        _data.writeInterfaceToken(DESCRIPTOR);
        // 2. 通过Binder执行远程请求,最终响应数据会封装在_reply
        mRemote.transact(Stub.TRANSACTION_getProcessId, _data, _reply, 0);
        // 3. transact阻塞结束后读取响应数据
        _reply.readException();
        _result = _reply.readInt();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    return _result;
}

可见,实际上_reply一直使用的都是同一个,由进程A创建,发送给B进程,进程B会将处理好的响应数据写入到_reply中,并最终通过onTranscat方法回调给进程A,这样就完成了一个RPC

总的来说,整个过程的执行流程如下:


使用Binder进行IPC

四、Messenger使用与源码分析

1. 使用

Service进程中如下使用:

public class MessengerRemoteService extends Service {

    private static final String TAG = "MessengerRemoteService";
    private Messenger mMessenger;
    @Override
    public IBinder onBind(Intent intent) {
        return mMessenger.getBinder();
    }
 
    @Override
    public void onCreate() {
        super.onCreate();

        // 3. 使用 Messenger 进行进程间通信
        mMessenger = new Messenger(new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                Log.i(TAG, "handleMessage: " + msg);
                Log.i(TAG, "handleMessage data: " + msg.getData().get("msg"));
                return true;
            }
        }));
    } 
} 

然后在Activity中如下建立服务连接:

// for Messenger
private Messenger mMessenger;
ServiceConnection mServiceConnection3 = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Log.i(TAG, "onServiceConnected3: ");
        mMessenger = new Messenger(service);
        btnBindRemote.setEnabled(false);
        mIsBond = true;
        Toast.makeText(MainActivity.this, "service bond 3!", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        Log.i(TAG, "onServiceDisconnected 3: ");
        mIsBond = false;
        btnBindRemote.setEnabled(true);
    }
}; 

绑定后如下发送信息:

if (mMessenger != null){
    Message message = new Message();
    Bundle bundle = new Bundle();
    bundle.putString("msg","message  clicked from Main ..");
    message.what = 122;
    message.setData(bundle);
    try {
        mMessenger.send( message);
    } catch (RemoteException e) {
        e.printStackTrace();
    }
} 

日志输出如下:

日志输出

基于上面的使用,整个流程下来你会发现Messenger的通信是单向的,如果想要双向的话,那么需要在作为客户端的进程A上也创建一个MessengerHandler,然后在B进程中发送响应消息。

为了能够进行双向通信,我们可以对上面代码进行如下修改,其中MessengerRemoteService中的Messenger可以这么修改:

mMessenger = new Messenger(new Handler(new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        Log.i(TAG, "handleMessage: " + msg);
        Log.i(TAG, "handleMessage data: " + msg.getData().get("msg"));

        Message message = Message.obtain();
        message.replyTo = mMessenger;
        Bundle bundle = new Bundle();
        bundle.putString("msg", "MSG from MessengerRemoteService..");
        message.setData(bundle);
        message.what = 124;
        try {
            // 注意这里
            msg.replyTo.send(message);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return true;
    }
}));

注意到上面的msg.replyTo.send(message),其中msg.replyTo是一个代表发送这个消息的Messenger。在Activity中可以这么改:

// onCreate中
mClientMessenger = new Messenger(mHandler);

// ...
Handler mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == 2) {
            Toast.makeText(MainActivity.this, "" + msg.obj, Toast.LENGTH_SHORT).show();
            return true;
        }else if (msg.what == 124){
            Toast.makeText(MainActivity.this,   msg.getData().getString("msg"), Toast.LENGTH_SHORT).show();
            Log.i(TAG, "handleMessage: " + msg.getData().getString("msg"));
            Log.i(TAG, "handleMessage: ");
            return true;
        }
        textView.setText(String.valueOf(msg.obj));
        return true;
    }
});

最后Activity收到消息时会弹出收到的消息,如下图:

收到MessengerRemoteService消息

整个双向通信的流程如下:


Messenger双向通信

2. Messenger 实现原理

Messenger底层仅仅是简单地包裹了一下Binder,具体来说就是也使用的AIDL,因此它不会影响到进程的生命周期,不过当进程销毁时,连接也会中断。

下面来简要看一下它的部分源码:

public final class Messenger implements Parcelable {
    private final IMessenger mTarget;

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

其中IMessenger是个AIDL接口,如下:

package android.os; 
import android.os.Message; 
/** @hide */
oneway interface IMessenger {
    void send(in Message msg);
} 

有了前面的知识基础,这玩意儿就很好理解了。

五、总结

本文主要描述了Android进程间通信中的AIDLSocket两种方式,文中没有对Socket方式做过多描述和分析,是因为使用Socket通信是比较基础的事情,并且它的实现过程相对容易理解,因此就一笔带过了,具体实现源码请查看 AndroidIPCDemo。文中着重从AIDL生成源码角度分析了Binder的运行机制,并简单介绍了Messenger的使用及其实现。

OK,水平有限,欢迎理性指正。

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

推荐阅读更多精彩内容