记一个Otto Bus使用上的坑

作者: tmp_zhao | 来源:发表于2016-11-23 23:10 被阅读300次

    缘起

    今天晚上有个同事找我看一个问题,因为他们用到了我们的模块,而我们模块会在工作结束时调用他们塞进来的callback返回回去,但是在他们的callback中两段基本相同的代码却有着不一样的行为,很是令人费解。类似下面这样:

    以下是callback中的伪代码:
    
    case 1: // 不同的case,执行的逻辑是相同的
        // before notify code
        notifyResult(case 1); // 这里面有bus.postEvent(Intent)的调用
        // after notify code
        break;
    case 2:
        // before notify code
        notifyResult(case 2); // 这里面有bus.postEvent(Intent)的调用
        // after notify code
        break;
    
    
    另外的某个Act中有handler方法,如下:
    @subscribe
    public void eventConsumeMethod(Intent intent) {
        System.out.println("consumed");
    }
    

    一般大家都会觉得这2种没什么差别,输出(执行顺序)都应该是:
    before -> consumed - > after
    在这里的case2确实是这样,但case1的输出却是:
    before -> after -> consumed
    我当时看到的时候也觉得很不可思议,因为Bus的代码我曾经认真看过,按我的理解post event肯定会同步执行的,即post event紧接着就会进到handler方法中,所以这里consume肯定是接着before的啊。下面让我们来分析下出现这个神奇现象的原因。

    源码&单步

    在分析之前,再补充说明下,前面代码中的case1是通过我们模块里的post Event调出去的,而case2是直接正常回调出去的。
    接下来,当我单步调试的时候,很自然地来到了Bus.post(Object event)方法,其源码如下:

    public void post(Object event) {
      if (event == null) {
        throw new NullPointerException("Event to post must not be null.");
      }
      enforcer.enforce(this);
    
      Set<Class<?>> dispatchTypes = flattenHierarchy(event.getClass());
    
      boolean dispatched = false;
      for (Class<?> eventType : dispatchTypes) {
        Set<EventHandler> wrappers = getHandlersForEventType(eventType);
    
        if (wrappers != null && !wrappers.isEmpty()) {
          dispatched = true;
          for (EventHandler wrapper : wrappers) {
            enqueueEvent(event, wrapper);
          }
        }
      }
    
      if (!dispatched && !(event instanceof DeadEvent)) {
        post(new DeadEvent(this, event));
      }
    
      dispatchQueuedEvents();
    }
    

    当出现上面case1的情况时,我就在想会不会是在post的过程中有某些return导致提前返回了,所以在看代码的时候,我专门留意了下,这个方法看起来没有我想要找的return,最后我们来到了dispatchQueuedEvents方法,接着往下看,其源码如下:

    protected void dispatchQueuedEvents() {
        // don't dispatch if we're already dispatching, that would allow reentrancy and out-of-order events. Instead, leave
        // the events to be dispatched after the in-progress dispatch is complete.
        if (isDispatching.get()) {
          return; // 罪魁祸首就是这货!!!
        }
    
        isDispatching.set(true);
        try {
          while (true) {
            EventWithHandler eventWithHandler = eventsToDispatch.get().poll();
            if (eventWithHandler == null) {
              break;
            }
    
            if (eventWithHandler.handler.isValid()) {
              dispatch(eventWithHandler.event, eventWithHandler.handler);
            }
          }
        } finally {
          isDispatching.set(false);
        }
      
    

    一进来的if和注释算是给了我们答案,我单步debug的时候也发现确实是在此处提前return了,即这次事件并没有马上被处理。这里的注释翻译下就是说:
    如果我们正在分发事件,则不继续分发又出现的事件,因为那样会导致事件重入和乱序,所以我们会在处理完当前的事件后再回过头来处理新发生的事件。这里的isDispatching,是个ThreadLocal<Boolean>类型,和每个线程关联。这段代码和注释对应到我们前面出问题的case1中就是:
    在我们代码中是通过处理A事件调到上面的callback的(即正在分发处理事件A),而case1中又post了一个新的事件B,so按照这段源码的意思,在处理A事件的过程中,B不会被处理,而是等A处理完后,才会回过来接着处理B,注意理解上面源码中的while(true)循环。

    总结

    一般来说,即使发生了case1的情况也不是啥大问题,但很不巧的是,这位同事的代码刚好就需要先执行consume方法,然后再执行after逻辑,否则就不对。所以,通过上面的分析,我们也看到了,使用Otto Bus最好不要在处理某个事件的过程中又post了另一个事件,因为越复杂的case,可能会产生越出乎你意料之外的行为,有时也可能会困扰你

    当然了,如果全是自己控制,那很好办,大家很容易能避开这样的写法,但就像我们这里一样,一个大的app经常是需要各个模块配合工作的,别人调用你的方法,你不大可能知道他是以怎样的形式回调你的,所以想避免还是不那么明显的。针对这个问题,可以很简单的用Handler.postRunnable来解决,避开post事件的嵌套。可能还有更好的解决方式,欢迎交流、指正。

    相关文章

      网友评论

        本文标题:记一个Otto Bus使用上的坑

        本文链接:https://www.haomeiwen.com/subject/arnvpttx.html