跨进程通信原理解析

Binder架构的组成

Binder框架有3个方面组成:Binder服务端、Binder驱动以及客户端组成。

Binder服务端:Binder服务端实际上就是一个Binder对象,该对象一旦创建就会开启一个隐藏的线程,该线程用来接收Binder驱动发送的消息,然后执行onTransact函数,并根据onTransact的参数执行不同的服务代码;因此要实现一个Binder服务就得重载onTransact方法。

重载onTransact方法的主要内容就是onTransact函数的参数转为服务函数的参数,而onTransact的参数来自客户端 调用的transact方法;因此,如果transact参数确定了,那么onTransact的参数也就确定了。

Binder驱动:任意一个服务端Binder对象被创建,同时会在Binder驱动中创建一个 mRemote对象,该对象的类型也是一个Binder类;客户端就是通过mRemote 来访问远程服务。

客户端:客户端想要访问远程服务,必须要获取远程服务在Binder对象中对应的mRemote引用。怎么获取呢?

获取到mRemote后就可以调用transact方法了,在Binder驱动中,也重载了transact方法,重载的内容主要包括下面几项:

1.以线程间通信的模式,向服务端发送客户端传递过来的参数
2.挂起当前线程,当前线程正是客户端线程,并等待服务端执行完 指定的 服务函数后通知(notify)
3.接收到服务端线程的通知,然后继续执行客户端线程,并返回到客户端代码区.

Binder的结构图如下:

binder结构图

2.如何设计Binder

2.1 设计Binder服务端

从代码角度来说很简单,就是继承自Binder然后重写onTransact方法,以下为IMediaPlayerService的例子:

public class IMediaPlayerService extends Binder {
    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        return super.onTransact(code, data, reply, flags);
    }

    public void start(String path){

    }

    public void stop(){

    }
}

当要启动该服务,只需要在Activity中new一个IMediaPlayerService对象即可。重写onTransact方法并从data中获取客户端传递过来的参数,比如start方法中传递过来的path。然而,这里有个问题,就是服务端如何确定客户端传递过来path在data中的位置?因此,这里需要和客户端约定好。

这里假设客户端在传入包裹data中放入的第一个数据就是path,那么onTransact的代码可以如下写:

protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
    switch (code){
        case 1001:
            data.enforceInterface("IMediaPlayerService");
            String path = data.readString();
            start(path);
            reply.writeString("我是执行完返回的结果");
            break;
        case 1002:
            stop();
            break;
    }

    return super.onTransact(code, data, reply, flags);
}


onTransact中的code表示 客户端希望调用服务端的哪个函数,所以,客户端和服务端要约定好 一组int值,不同的值表示想要调用不同的服务端函数。例如这里的1001表示start,1002表示要调用stop。

enforceInterface:是某种校验,和客户端的writeInterfaceToken是对应的,等下做具体说明。

readString:表示从包裹data中取出一个字符串path供start调用。

如果想要返回客户端执行的结果就可以在reply中调用Parcel提供的 相关函数来写入相应的结果,比如上面的reply.writeString(“我是执行完返回的结果”)。

2.2 Binder客户端设计

想要使用服务端,就得获得Binder驱动中对应的mRemote的引用。获取方法下面详解,然后调用mRemote的transact方法。transact方法原型如下:

public boolean transact(int code, @NonNull Parcel data,@Nullable Parcel reply, int flags)throws RemoteException;

其中data表示要传递给服务端的包裹(Parcel),远程服务端需要的数据都需要放入这个包裹中。包裹只支持原子类型:String、int、long等,以及实现Parcelable接口的对象。客户端调用的代码可以写成类似下面这样的:

IBinder mRemote = null;
String path = "/sdcard/media/xxx.mp4";
int code = 1001;
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken("IMediaPlayerService"); //和服务端enforceInterfac一一对应
data.writeString(path);
mRemote.transact(code, data, reply, 0);
IBinder binder = reply.readStrongBinder();
reply.recycle();
data.recycle();

看到上面的代码感觉是不是很熟悉,是不是和使用aidl进行IPC中自动生成的代码很像,哈哈。上面分析可知,data和reply不是new出来的,而是调用Parcel.obtain()申请的。就和邮局一样,你只能使用邮局使用的信封。其中date和reply都是由客户端提供的,data提供服务端需要的数据,reply是给服务端将返回结果放入其中的。

writeInterfaceToken:标注远程服务的名称,理论上不是必须的,因为客户端已经获取了远程服务的mRemote引用,那么就不会调用了其他的远程服务。该名称是Binder驱动确保客户端想调用的是指定的服务端。

writeString:用于向包裹中写入一条String类型的数据。注意,包裹中添加的内容是有序的,这个顺序必须是客户端和服务端之前约定好的。在服务端的onTransact方法中会按照指定的顺序取出数据。

最后调用transact方法:调用该方法后,客户端线程进入 Binder驱动,Binder驱动会挂起当前的线程,并向远程服务中发送一个消息,该消息包含客户端传进来的包裹,服务端拿到包裹后,进行数据解析,然后调用相应的服务函数,最后将返回结果写入reply中。然后向Binder驱动发送一个通知(notify)唤醒客户端线程,从而使得客户端线程从Binder驱动代码区返回到客户端代码区。

tansact方法中最后一个参数flag表示IPC的调用模式,0表示服务端执行完后会返回执行结果,1表示单向的,服务端不会返回执行结果。

3.如何获取Binder对象

使用过AIDL技术的同学应该都能想到,那就是使用Service。调用bindService即可,bindService函数原型如下:

public boolean bindService(Intent service, ServiceConnection conn,int flags);

最关键的就是其中的ServiceConnection ,ServiceConnection 中包含这个函数:

void onServiceConnected(ComponentName name, IBinder service);

请注意onServiceConnected第二个参数Service,当客户端调用AMS启动某个Service后,如果Service正常启动,那么AMS就会调用ActivityThread中的ApplicationThread对象,调用参数中就包含Binder对象的引用,然后在 ApplicationThread中会回调bindService中的conn接口。因此,客户端就可以在onServiceConnected方法中将service参数保存为一个全局变量,以供随时调用。这就解决了第一个问题,客户端如何获得Binder对象的引用。

4.保证包裹类的参数顺序

Android SDK中提供了aidl工具,该工具可以把一个aidl文件转换为一个java文件,在该Java类文件中,同时重载了onTransact和transact方法,统一了存入包裹和读取包裹的参数。Aidl工具不是必须的,有经验的程序员完全可以自己写出参数统一的包裹存入和包裹读出的代码。

下面我们看看aidl文件自动生成的java文件是什么样的?我们先定义一个aidl文件,如下:

interface IBookManager {
    List<Book> getAllBooks();
    void addBook(in Book book);
}

注意,aidl文件只支持原子类型和实现了Parcelable接口的类。上面的Book类就实现了Parcelable。对应的java文件如下:

package com.example.za_zhujiangtao.zhupro;
// Declare any non-default types here with import statements

public interface IBookManager extends android.os.IInterface
{
/** Local-side IPC implementation stub class. */
public static abstract class Stub extends android.os.Binder implements com.example.za_zhujiangtao.zhupro.IBookManager
{
private static final java.lang.String DESCRIPTOR = "com.example.za_zhujiangtao.zhupro.IBookManager";
/** Construct the stub at attach it to the interface. */
public Stub()
{
this.attachInterface(this, DESCRIPTOR);
}
/**
 * Cast an IBinder object into an com.example.za_zhujiangtao.zhupro.IBookManager interface,
 * generating a proxy if needed.
 */
public static com.example.za_zhujiangtao.zhupro.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.example.za_zhujiangtao.zhupro.IBookManager))) {
return ((com.example.za_zhujiangtao.zhupro.IBookManager)iin);
}
return new com.example.za_zhujiangtao.zhupro.IBookManager.Stub.Proxy(obj);
}
@Override public android.os.IBinder asBinder()
{
return this;
}
@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 INTERFACE_TRANSACTION:
{
reply.writeString(descriptor);
return true;
}
case TRANSACTION_getAllBooks:
{
data.enforceInterface(descriptor);
java.util.List<com.example.za_zhujiangtao.zhupro.Book> _result = this.getAllBooks();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook:
{
data.enforceInterface(descriptor);
com.example.za_zhujiangtao.zhupro.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.example.za_zhujiangtao.zhupro.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
default:
{
return super.onTransact(code, data, reply, flags);
}
}
}
private static class Proxy implements com.example.za_zhujiangtao.zhupro.IBookManager
{
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote)
{
mRemote = remote;
}
@Override public android.os.IBinder asBinder()
{
return mRemote;
}
public java.lang.String getInterfaceDescriptor()
{
return DESCRIPTOR;
}
@Override public java.util.List<com.example.za_zhujiangtao.zhupro.Book> getAllBooks() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.example.za_zhujiangtao.zhupro.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getAllBooks, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.example.za_zhujiangtao.zhupro.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override public void addBook(com.example.za_zhujiangtao.zhupro.Book book) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book!=null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
}
finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getAllBooks = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
static final int TRANSACTION_registerListener = (android.os.IBinder.FIRST_CALL_TRANSACTION + 2);
static final int TRANSACTION_unregisterListener = (android.os.IBinder.FIRST_CALL_TRANSACTION + 3);
}
public java.util.List<com.example.za_zhujiangtao.zhupro.Book> getAllBooks() throws android.os.RemoteException;
public void addBook(com.example.za_zhujiangtao.zhupro.Book book) throws android.os.RemoteException;
}

这些代码主要完成了一下的3个任务:

1.定义了一个interface IBookManager,内部包含aidl文件声明的所有方法,并且继承了IInterface,即该interface的实现类需要提供一个asBinder()函数。

2.定义一个Proxy类,该类实现了IBookManager,该类作为客户端访问服务端的代理,所谓代理就是为了前面提到的第二个问题—统一包裹的输入和读取参数。

3.定义一个Sub类,他是一个抽象类,继承了Binder类且实现了IBookManager接口。之所以是抽象类是因为具体的服务函数需要程序员自己在Service类中实现。例如上面的onTransact方法中的 addBook方法最终调用的是程序员自己在Service类中实现的。

private Binder mBinder = new IBookManager.Stub() {
    @Override
    public List<Book> getAllBooks() throws RemoteException {
        return mBookList;
    }

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

};

这个就是我在Service类中实现的。同时,在Sub类中重载了onTransact方法,由于transact方法内部给包裹类写入顺序是由aidl工具决定的,因此,在onTransact方法中,aidl工具自然知道按照何种顺序从包裹中取出数据。

在Sub类中还定义了一些int型参数,如TRANSACTION_getAllBooks, TRANSACTION_addBook, 这些常量与服务函数对应,onTransact和transact方法的第一个参数就是code的值就来源于此。

在Sub类中还定义了一个方法,asInterface:提供这个函数的原因是服务端提供的服务除了其他进程可以调用之外,在本服务进程内部的其他类也可以调用,对于后者则不需要经过IPC调用,而直接在进程内部调用。Bindern内部有一个queryLocalInterface的方法,该函数是通过输入字符串来判断来判断该Binder对象是不是本地Binder对象的引用。

总结下来说就是,当创建一个Binder对象时,服务端进程内部会创建一个Binder对象,Binder驱动中也会创建一个Binder对象。如果从远程获取服务端的Binder,则只会返回Binder驱动中的Binder对象。而如果从服务端进程内部获取Binder对象,则会返回服务端本身的Binder对象。如下图:

服务端客户端交互图

因此,asInterface函数正是利用了queryLocalInterface方法,提供了一个统一接口。无论是本地服客户端还是远程客户端,当获取了Binder对象后,都可以把该Binder对象作为asInterface的参数,来返回一个IBookManager接口。
转载自:https://me.csdn.net/zhujiangtaotaise

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

推荐阅读更多精彩内容