记一个Otto Bus使用上的坑

arls5614 8年前
   <h3><strong>缘起</strong></h3>    <p>今天晚上有个同事找我看一个问题,因为他们用到了我们的模块,而我们模块会在工作结束时调用他们塞进来的callback返回回去,但是在他们的callback中两段基本相同的代码却有着不一样的行为,很是令人费解。类似下面这样:</p>    <pre>  <code class="language-java">以下是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");  }</code></pre>    <p>一般大家都会觉得这2种没什么差别,输出(执行顺序)都应该是:</p>    <p>before -> consumed - > after</p>    <p>在我们这里的case2确实是这样,但case1的输出却是:</p>    <p>before -> after -> consumed</p>    <p>我当时看到的时候也觉得很不可思议,因为Bus的代码我曾经认真看过,按我的理解post event肯定会同步执行的,即post event紧接着就会进到handler方法中,所以这里consume肯定是接着before的啊。下面让我们来分析下出现这个神奇现象的原因。</p>    <h3><strong>源码&单步</strong></h3>    <p>在分析之前,再补充说明下,前面代码中的case1是通过我们模块里的post Event调出去的,而case2是直接正常回调出去的。</p>    <p>接下来,当我单步调试的时候,很自然地来到了 Bus.post(Object event) 方法,其源码如下:</p>    <pre>  <code class="language-java">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();  }</code></pre>    <p>当出现上面case1的情况时,我就在想会不会是在post的过程中有某些 return 导致提前返回了,所以在看代码的时候,我专门留意了下,这个方法看起来没有我想要找的 return ,最后我们来到了 dispatchQueuedEvents 方法,接着往下看,其源码如下:</p>    <pre>  <code class="language-java">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);      }</code></pre>    <p>一进来的if和注释算是给了我们答案,我单步debug的时候也发现确实是在此处提前return了,即这次事件并没有马上被处理。这里的注释翻译下就是说:</p>    <p>如果我们正在分发事件,则不继续分发又出现的事件,因为那样会导致事件重入和乱序,所以我们会在处理完当前的事件后再回过头来处理新发生的事件。这里的 isDispatching ,是个ThreadLocal<Boolean>类型,和每个线程关联。这段代码和注释对应到我们前面出问题的case1中就是:</p>    <p>在我们代码中是通过处理A事件调到上面的callback的(即正在分发处理事件A),而case1中又post了一个新的事件B,so按照这段源码的意思,在处理A事件的过程中,B不会被处理,而是等A处理完后,才会回过来接着处理B,注意理解上面源码中的 while(true) 循环。</p>    <h3><strong>总结</strong></h3>    <p>一般来说,即使发生了case1的情况也不是啥大问题,但很不巧的是,这位同事的代码刚好就需要先执行consume方法,然后再执行after逻辑,否则就不对。所以,通过上面的分析,我们也看到了, 使用Otto Bus最好不要在处理某个事件的过程中又post了另一个事件,因为越复杂的case,可能会产生越出乎你意料之外的行为,有时也可能会困扰你 。</p>    <p>当然了,如果全是自己控制,那很好办,大家很容易能避开这样的写法,但就像我们这里一样,一个大的app经常是需要各个模块配合工作的,别人调用你的方法,你不大可能知道他是以怎样的形式回调你的,所以想避免还是不那么明显的。针对这个问题,可以很简单的用 Handler.postRunnable 来解决,避开post事件的嵌套。可能还有更好的解决方式,欢迎交流、指正。</p>    <p> </p>    <p>来自:http://www.jianshu.com/p/67ccce928edb</p>    <p> </p>