IPC机制:AIDL调用流程分析和常见问题处理

当发起一次AIDL调用时,是如何进行进程间切换的?都经过了哪些步骤?有哪些重要方法?
下面我们用一个例子来具体看一下


这里假定我们声明了一个aidl方法,如下:

interface IMyAidlInterface {
    WeatherEntity queryWeather(in RequestData request, in ICallBack callback);
}

下面看一下,一次完整的IPC过程是怎样的,中间都发生了哪些事情

第一步:绑定服务,获取服务端binder引用

Client绑定了service后,首先会拿到server的一个Binder引用:

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        // 我们这里拿到的对象其实就是其Stub的内部类Proxy对象
        myAidlInterface = IMyAidlInterface.Stub.asInterface(service);

上面这步转化,会根据当前client和server是否为同一个进程,如果是不同进程,则返回其内部Proxy代理对象


第二步:客户端发起发起调用

result = myAidlInterface.queryWeather(request, callback);

调用目标方法queryWeather()之后,会按照上面所述走到Proxy.queryWeather()中,逻辑如下:
IMyAidlInterface.Stub.Proxy.queryWeather():

@Override 
public com.example.myapplication.WeatherEntity queryWeather(com.example.myapplication.RequestData request, 
com.example.myapplication.ICallback callback) throws android.os.RemoteException {
    // 获取两个新的Parcel对象,_data是入参,_reply是返回值
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();

    com.example.myapplication.WeatherEntity _result;
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        if ((request!=null)) {
            _data.writeInt(1);
            // 序列化参数1 RequestData进_data中
            request.writeToParcel(_data, 0);
        } else {
            _data.writeInt(0);
        }

        // 序列化参数2 Callback进_data中
        _data.writeStrongBinder((((callback!=null))?(callback.asBinder()):(null)));

        // 发起远程调用,remote其实就是我们在onServiceConnected中,获取到的IBinder对象,
        // 也就是server端的Binder对象,也就是说在这里将请求转交给了server去处理
        mRemote.transact(Stub.TRANSACTION_queryWeather, _data, _reply, 0);

        _reply.readException();
        // 这里readInt判断是否为0,确认请求是否成功,因为在server端,会判断返回值result是否为null,
        // 如果为null则writeInt(0),否则写1
        if ((0!=_reply.readInt())) {
            _result = com.example.myapplication.WeatherEntity.CREATOR.createFromParcel(_reply);
        } else {
            _result = null;
        }
        
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    return _result;
}
  1. 首先初始化两个parcel:传入参数_data和返回参数_reply,
    将方法中所有的参数都写入_data中,普通对象调用的是它的writeToParcel方法,
    如果参数是一个aidl接口即继承了IInterface接口,则调用的是Parcel.writeStrongBinder方法。

  2. 将所有参数都写入后,就开始调用mRemote.transact()方法,这里的mRemote其实就是我们在onServiceConnected中, 获取到的IBinder对象,即在server端的Service.onBind方法中返回的Binder对象。在这一步将请求转交给了server去处理


第三步:Binder.transact()

下面看下Binder的transact(),这个方法携带code(也就是目标方法标示),入参data和返回值reply:

/**
 * Default implementation rewinds the parcels and calls onTransact.  On
 * the remote side, transact calls into the binder to do the IPC.
 */
public final boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply,
        int flags) throws RemoteException {
    if (false) Log.v("Binder", "Transact: " + code + " to " + this);

    if (data != null) {
        data.setDataPosition(0);
    }
    boolean r = onTransact(code, data, reply, flags);
    if (reply != null) {
        reply.setDataPosition(0);
    }
    return r;
}

这里做了两件事:
在调用onTransact()发起调用前后,对参数data和返回值reply重置position,然后调用onTransact(),这下就到了我们服务端中.aidl自己实现的onTransact()方法了。
这个方法如果返回false,表示服务端没有目标方法,原因下面会分析


第四步:服务端onTransact()

服务端的onTransact()方法逻辑如下:

// 根据传入的code参数确定需要调用哪个方法
case TRANSACTION_queryWeather: {
    data.enforceInterface(descriptor);
    com.example.myapplication.RequestData _arg0;
    if ((0!=data.readInt())) {
        // 反序列化参数1 RequestData
        _arg0 = com.example.myapplication.RequestData.CREATOR.createFromParcel(data);
    } else {
        _arg0 = null;
    }

    com.example.myapplication.ICallback _arg1;
    // 反序列化参数2 Callback
    _arg1 = com.example.myapplication.ICallback.Stub.asInterface(data.readStrongBinder());
    
    // 然后调用我们在服务端service中实现的目标方法,得到result
    com.example.myapplication.WeatherEntity _result = this.queryWeather(_arg0, _arg1);
    
    reply.writeNoException();
    if ((_result!=null)) {
        reply.writeInt(1);
        // 将result序列化进reply中
        _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
    } else {
        reply.writeInt(0);
    }
    return true;
}

在服务端的onTransact()方法中,会根据传入的code参数调用对应方法,如果没有code相对应,则调用父类也就是Binder.onTransact()方法。
Binder.onTransact()中,判断code如果为预置的几种,返回true,否则返回false,那我们传入的code肯定不是预置的,所以这时候会返回false。
这时,可以明确一点:
如果在调用时,mRemote.transact()返回false,则表示服务端没有目标方法。

第五步,service中IMyAidlInterface.Stub子类实现的各个方法

经过上面的调用,逻辑就已经被我们在服务端service处理完了,拿到结果reply后,通过Binder将其返回给client端。到这里为止,一次IPC调用就结束了。


上面的流程可以用下图来说明:

Binder流程.png


IPC调用时,序列化和反序列化不匹配的问题

这里有一个问题需要注意:从上面的过程可以看出,在一次IPC调用中,一个方法的所有参数,都是写在同一个Parcel中的, 这就要求我们必须同步更新client和server端的序列化字段,保证二者一致,否则就会出现因为序列化和反序列化不匹配,导致空指针等问题。

为了理解上面的问题,我们需要对Parcel有一个简单的认识。

Parcel可以用于跨进程传输数据,它提供了一套机制,可以将序列化后的数据写入一个共享内存,其他进程通过Parcel从共享内存中读出字节流并反序列化成对象。
Parcel是一块连续的内存,并且会根据需要自动扩展其大小。也就是说写入数据时,如果系统发现已经超出了Parcel的存储空间,它会自动申请所需内存并扩展dataCapacity。
在我们向parcel里读写值的时候,parcel内部会维护一个dataPosition,类似于数组的索引,
序列化/反序列化都是严格按照这个position来执行的,我们可以通过setDataPosition()方法来设置该值。

例如上面的例子,我们有两个参数request和callback,假设client的request多了一个字段test, 这两个参数按照定义顺序依次写入parcel中,那在server反序列化时,原本position应该是callback的, 现在变成了新增字段test,这就会导致callback无法正常序列化而变成null,影响正常功能。

如何规避因为序列化和反序列化不匹配导致的问题

  1. 在序列化时,可以通过手动setDataPosition(),通过控制position 将新增字段写进固定的没有使用过的位置,从而使其不影响反序列化
  2. 在写aidl接口时,如果无法做到同时更新client和server,那可以考虑将常改变的参数,例如request定义在后面,例如test(callback, request),从而避免因为request的改变影响callback的序列化。

在AIDL调用中,如何判断server端的目标方法是否存在?

通过上面的分析可以明确,当客户端在调用时,mRemote.transact()返回false,则表示服务端没有目标方法

在系统根据aidl自动生成的java类中,该方法的返回值是被忽略的,如果要处理这种情况,可以选择自己实现该类,判断如果返回值为false,就抛出异常,然后在方法调用处去处理该异常。

修改后的代码如下:

boolean res = mRemote.transact(Stub.TRANSACTION_queryNumberInfoAsync, _data, _reply, 0);
// If mRemote.transact() return false indicates that the method we call is not exist
// in server, so we need to throw an exception to notify the caller.
if (!res) {
    throw new RemoteNoSuchMethodException("The method you are calling queryWeather() is not exist in server!");
}

这里的异常是继承自RuntimeException的自定义异常


上面是对Binder流程的简单分析,如果对AIDL原理不熟悉可以参考我的这篇博客
https://www.jianshu.com/p/a9bf2e513751

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