Android插件中使用EventBus出现java.lang.IllegalArgumentException: Expected receiver of type xxx, but got xx

本文分析了在Android插件化架构中使用EventBus2进行跨插件通信时出现的异常问题,主要原因是不同ClassLoader导致的类同名转换异常,并提供了解决方案。

使用场景

由于所开发的Android项目是个老项目,EventBus使用的还是EventBus2,整个项目是插件化架构,不同插件使用的ClassLoader不同。插件1中有个onEvent方法,用来更新插件1中的一些信息。在插件2中发送一个EventBus消息更新插件1。在插件升级的时候(新老插件ClassLoader不同)偶现下述异常。

java.lang.IllegalArgumentException: Expected receiver of type 
com.yuntao.plugin.order.DetailActivity, but got 
com.yuntao.plugin.order.DetailActivity at java.lang.reflect.Method.invoke(Native Method) at 
java.lang.reflect.Method.invoke(Method.java:372) at 
de.greenrobot.event.EventBus.invokeSubscriber(EventBus.java:498) at 
de.greenrobot.event.EventBus.postToSubscription(EventBus.java:429) at 
de.greenrobot.event.EventBus.postSingleEventForEventType(EventBus.java:410) at 
de.greenrobot.event.EventBus.postSingleEvent(EventBus.java:383) at 
de.greenrobot.event.EventBus.post(EventBus.java:263) at 
com.yuntao.plugin.PaymentActivity$2.onClick(PaymentActivity.java:141) at 
android.view.View.performClick(View.java:4783) at 
android.view.View$PerformClick.run(View.java:19887) at 
android.os.Handler.handleCallback(Handler.java:739) at 
android.os.Handler.dispatchMessage(Handler.java:95) at 
android.os.Looper.loop(Looper.java:135) at 
android.app.ActivityThread.main(ActivityThread.java:5290) at 
java.lang.refle.... 

异常原因

看异常结果是在调用Method.invoke中抛出的。追到invoke方法查看

public Object invoke(Object receiver, Object... args)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        return invoke(receiver, args, isAccessible());
    }

    private native Object invoke(Object receiver, Object[] args, boolean accessible)
            throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;

最终只是调用了一个Native方法。那看下方法的注释啥时候会抛出IllegalArgumentException异常

/**
@throws IllegalArgumentException
  * if the number of arguments doesn't match the number of parameters, the receiver
  * is incompatible with the declaring class, or an argument could not be unboxed
  * or converted by a widening conversion to the corresponding parameter type
  */

翻译来看遇到下述情况:参数数目不同,接收的对象与声明的类不一致,参数不能自动拆箱,参数不能通过拓宽转换为相应的参数类型。
由于参数都是String类型,异常是Expected receiver of type ,基本可以断定是receiver与声明的类不一致的问题。
写一段测试代码看看:

public class TestReflect {
    public void show(Integer a) {
        Log.i("pyt", a + "");
    }
}

public class TestReflect1 {
    public void show(Integer a) {
        Log.i("pyt", a + "");
    }
}

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        try {
            Class<?> c = Class.forName("com.yuntao.testeventbus.TestReflect");
            Method method = c.getDeclaredMethod("show", Integer.class);
            method.setAccessible(true);
            method.invoke(new TestReflect1()); //正常传递c.newInstance(),这里换成另一个类的对象
        } catch (Exception e) {
            e.printStackTrace();
            Log.e("pyt", e.toString());
        }
    }

果然会抛出异常

 void invokeSubscriber(Subscription subscription, Object event) {
        try {
            subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
        } catch (InvocationTargetException e) {
            handleSubscriberException(subscription, event, e.getCause());
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("Unexpected exception", e);
        }
    }

EventBus2源码分析

按照method的声明类与invoke参数的对象声明类不一致会出现异常的思路来分析。
直接查看异常的调用栈,最终方法

 void invokeSubscriber(Subscription subscription, Object event) {
        try {
            subscription.subscriberMethod.method.invoke(subscription.subscriber, event);
        } catch (InvocationTargetException e) {
            handleSubscriberException(subscription, event, e.getCause());
        } catch (IllegalAccessException e) {
            throw new IllegalStateException("Unexpected exception", e);
        }
    }

这里写图片描述
看下两个类的关系Subscription里包含了EventBus注册的那个对象与onEvent方法。subscriber就是注册的实例,subscriberMethod中包含的就是当前实例的onEvent方法。上边的方法是直接取出method.invoke(subscription),都是保存在Subscription实例中的,那就要看Subscription实例是如何创建的了。

追溯到EventBus的register方法,查看

List<SubscriberMethod> subscriberMethods = subscriberMethodFinder.findSubscriberMethods(subscriber.getClass());

findSubscriberMethods方法

 List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        String key = subscriberClass.getName();
        List<SubscriberMethod> subscriberMethods;
        synchronized (methodCache) {
            subscriberMethods = methodCache.get(key);
        }
        if (subscriberMethods != null) {
            return subscriberMethods;
        }
        subscriberMethods = new ArrayList<SubscriberMethod>();
        ////省略n行代码,主要是反射当前subscriberClass中的onEvent方法,创建SubscriberMethod对象,缓存到methodCache(一个静态HashMap变量)中,key是类的name。
}        

看上述方法拿到了当前类的name,然后就会去methodCache中查找,有的话会直接返回。看到这里一定会联想到我们的框架,当有静态变量缓存的时候会出现的同名类转换异常问题(ClassLoader不同)。
当插件升级的时候,如果methodCache缓存了旧的插件的method,新插件获取到了旧的插件的method,然后invoke方法传入的是新插件的实例,这时候会出现异常。

上述分析比较简单,不懂可以详细参考EventBus源码

解决方案

更改EventBus源码。更改methodCache的key为Class对象,或者为Class对象的hashCode,这样可以使用SparseArray缓存,提高性能
在创建新的插件的时候调用EventBus的clearCaches方法清空缓存。

已经博主授权,源码转载自 https://pan.quark.cn/s/a4b39357ea24 QueueForMcu 基于单片机实现的队列功能模块,主要用于8位、16位、32位非运行RTOS的单片机应用,兼容大多数单片机平台。 开源代码:https://.com/xiaoxinpro/QueueForMcu 一、特性 动态创建队列对象 动态设置队列数据缓冲区 静态指定队列元素数据长度 采用值传递的方式保存队列数据 二、快速使用 三、配置说明 目前QueueForMcu只有一个静态配置项,具体如下: 在文件 中有一个宏定义 用于指定队列元素的数据长度,默认是 ,可以根据需要更改为其他数据类型。 四、数据结构 队列的数据结构为 用于保存队列的状态,源码如下: 其中 为配置项中自定义的数据类型。 五、创建队列 1、创建队列缓存 由于我们采用值传递的方式保存队列数据,因此我们在创建队列前要手动创建一个队列缓存区,用于存放队列数据。 以上代码即创建一个大小为 的队列缓存区。 2、创建队列结构 接下来使用 创建队列结构,用于保存队列的状态: 3、初始化队列 准备好队列缓存和队列结构后调用 函数来创建队列,该函数原型如下: 参数说明: 参考代码: 六、压入队列 1、单数据压入 将数据压入队列尾部使用 函数,该函数原型如下: 参数说明: 返回值说明: 该函数会返回一个 枚举数据类型,返回值会根据队列状态返回以下几个值: 参考代码: 2、多数据压入 若需要将多个数据(数组)压入队列可以使用 函数,原理上循环调用 函数来实现的,函数原型如下: 参数说明: 当数组长度大于队列剩余长度时,数组多余的数据将被忽略。 返回值说明: 该函数将返回实际被压入到队列中的数据长度。 当队列中的剩余长度富余...
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值