深入浅出otto——一款轻量的消息总线框架

otto背景

​ otto是sqaure推出的一款应用主要被应用在android上的轻量级事件总线框架,目的是为了解决消息通信的问题,他通过反射帮助你在不持有对方引用的情况下通知到对方,缓解了移动开发中会遇到的耦合问题。

otto使用

​ otto的使用很简单,下面写一个小demo来演示otto的使用:

class TestBus {
  static Bus INSTANCE;
  public synchronized static Bus getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Bus();
    }
    return INSTANCE;
  }
}
class TestEvent {
  TestEvent() {}
}
class TestSubscriber {
  @Subscribe
  public void onTestEvent(TestEvent event) {
    System.out.println("Get TestEvent");
  }
}
class Test {
  public static void main(String[] args) {
    TestSubscriber subscriber = new TestSubscriber();
    TestBus.getInstance().register(subscriber);
    TestBus.getInstance().post(new TestEvent());
    // output: Get TestEvent
    TestBus.getInstance().unregister(subscriber);
    TestBus.getInstance().post(new TestEvent());
    // output nothing
  }
}

​ 类比较少,但是通过demo我们可以看到,Test.main并没有调用TestSubscriber.onTestEvent方法,但是onTestEvent方法确实被执行了,这就是otto框架的作用所在。

otto构成

​ otto框架的构成很简单,主要包含以下几个类:

  1. Bus
    otto核心类,主要方法有三:register, post, unregister,用于完成注册、消息发送和注销三个步骤。

  2. @Subscribe, @Produce

    用在业务代码中,修饰对象是方法,目的是在register(obj)的时候方便Bus找到event在obj中对应method的注解。

    1. @Subscribe表示obj的方法s注册了这个消息,方法s会被执行
    2. @Produce表示obj的方法p注册了这个消息,在subscribe方法执行之前,会执行produce方法用来往event里面塞东西
  3. HandlerFinder

    组合在Bus中,用来给bus找到注册的obj在收到消息event的时候做什么,即找到对应的Method对象,由Bus缓存在map中,到时候好直接拿来用。

  4. ThreadEnforcer

    组合在Bus中,只有一个enforce方法,主要有ThreadEnforcer.Any和ThreadEnforcer.Main两个实现类,

    1. Main用来检测Bus的三大方法是否是在主线程执行,enforce判断如果不是主线程则抛出异常。
    2. Any则没有限制,方法体为空。
  5. DeadEvent

    在Event没有接受者的时候,Bus会发出一个DeadEvent

otto原理

otto的原理我将从Bus的三个主要方法分别来讲述他们分别干了什么事情:

  1. register(Object object)

    1. threadEnforcer.enforce,确保如果是主线程的Bus是否是运行在主线程,如果不是在主线程则抛出异常

    2. handlerFinder.findAllProducers(object),将object里面用@Produce注解且满足条件的方法(主要包括返回值为Event、没有参数等条件)全部加入到producer缓存中去。
      缓存即为producersByType: ConcurrentMap<Class<?>, EventProducer> ,其中key是Event.class, value : EventProducer是对object和他的produce Method的封装。

    3. handlerFinder.findAllSubscribers(object),同第二步类似

    4. producer.produceEvent,找到object注册的所有消息对应的producers,然后把每个produce方法都逐个执行一遍。

      注意:这里是produce方法执行的唯一时间点,在post方法中不执行produce

  2. post(Object event)

    1. threadEnforcer.enforce,同1.1

    2. flattenHierarchy(event.getClass()),获取event的所有基类,也就是说event的handler和event基类的handler在后面步骤中都会被执行— 如果存在的话。

    3. 遍历2.2中获取的event类和基类,找到对应的handler,如果有则enqueueEvent入队列

    4. 如果2.3没有找到handler,则抛出DeadEvent

    5. dispatch(event, eventHandle)

      1. isDispatching.get(),isDispatching类型是ThreadLocal<Boolean>,他是用来判断当前线程的消息队列是否正在执行,如果是的话则return

        因为整个post执行过程是同步的,在执行过程a中如果有新时间进来,也会在a的while循环中消费掉,并且如果是多线程同时执行post会通过ThreadLocal保证线程之间不会有冲突,所以在这里加这个判断是合理的。

      2. while(true) {dispatch},执行掉所有应该执行的event+handler

  3. unregister(Object object)

    同register步骤类似

otto 性能

​ 通过分析otto的执行过程发现他是用反射来做注册的,那么用反射会不会带来性能损耗呢?下面我就用一个TestCase来对比一下看看。对比的方式就是看一个onTestEvent方法直接执行和用bus.post来执行时间上有没有区别,代码如下:

public class TestOtto {

    private static Bus bus = new Bus(ThreadEnforcer.ANY);

    @Test
    public void testOtto() {
        long startTime = System.currentTimeMillis();
        TestSubscriber subscriber = new TestSubscriber();
        long newTime = System.currentTimeMillis();
        System.out.println("new cost " + (newTime - startTime) + " mills");
        TestEvent event = new TestEvent();
//        for (int i = 0; i < 1000; i++) {
//            bus.post(event);
//        }
        for (int i = 0; i < 1000; i++) {
            subscriber.onTestEvent(event);
        }
        System.out.println("invoke end");
        long endTime = System.currentTimeMillis();
        System.out.println("post cost " + (endTime - startTime) + " mills");
    }

    public static class TestEvent {}

    class TestSubscriber {
        TestSubscriber() {
//            bus.register(this);
        }

        @Subscribe
        public void onTestEvent(TestEvent test) {
            System.out.print("onTestEvent");
        }

    }
}

不使用otto执行结果:

new cost 0 mills
onTestEvent.... end
post cost 22 mills

使用otto执行结果:

new cost 24 mills
onTestEvent... invoke end
post cost 61 mills

​ 通过对比结果可以看到,otto的性能同直接调用相比还是有差距的,特别是在注册步骤,因为要用反射遍历object的所有方法,所以时间会拉长。

otto对比

​ 跟otto框架具备几乎相同功能的是EventBus框架,EventBus是一款跟otto框架具备相同功能,但是比otto更重的消息总线框架,otto的用法可以原模原样的搬到EventBus框架上去,连@Subscribe注解都是用的相同的名字。

​ 说到两者的不通电,Frodo的事件总线 —— otto的bus和eventbus对比分析这篇文章列了两个框架的对比分析,概括的非常到位:

1、otto中从源码角度看,要在基类中注册事件是一件比较麻烦的事情。而Evenbus就比较友好(有网友反应如果父类中注册了总线,那么子类中必须实现一个onEvent*方法,否则程序就会崩掉。由于时间问题没进行验证这一点);
2、订阅的事件参数问题,eventbus对多参数不会抛出异常。而otto只允许接收一个参数,否则抛出RuntimeException;(其实这一点作为开源项目对代码的质量还是挺重要的)
3、从个人使用的角度来看,个人更加喜欢otto的特性。因为我只会用otto来简化UI的通信,其他的我并不需要;
4、另外,用java注解的方式来显示的标记订阅方法和生产者方法这非常的友好。至少对刚使用的开发者而言,能够清晰看到代码的思路;
5、不过对不同需求的人群来说,eventbus拓展能力和使用场景更加丰富。如果你的项目通信比较多,而且很复杂的时候;
6、eventbus定义必须onEvent开始的方法感觉还是挺别扭;
7、eventbut是不使用注解是因为注解在2.3之前的系统上会变得缓慢(这一点还需要求证一下);
8、在eventbus中有一个比较难受的地方是:在一个订阅者类中如果有两个同参数类型的接收函数,并且都要执行在主线程,那如何命名呢?由于EventBus只根据事件参数类型来判断接收函数,因此会导致两个函数都会被执行。这当然对开发者来说比较难受了,不过github上已经有人提出采用添加tag的方式来做标记扩展(AndroidEventBus
9、使用otto时候,Bus对象只有作为单例共享的时候才足够高效。

otto总结

​ otto框架是一款足够轻量的消息总线框架,他的使用非常方便,但是因为他在注册的时候用到了反射,所以性能会有一定的影响,如果你的项目对性能要求并不那么高,那么完全可以使用otto框架来减少你的编码。

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

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,587评论 18 139
  • 最近讲的都是square出品的组件,今天还是继续讲解Square旗下的Otto框架 引用otto官网说的 Otto...
    乱码桑阅读 3,992评论 2 6
  • 项目到了一定阶段会出现一种甜蜜的负担:业务的不断发展与人员的流动性越来越大,代码维护与测试回归流程越来越繁琐。这个...
    fdacc6a1e764阅读 3,161评论 0 6
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,401评论 25 707
  • 大多数人买东西总会货比三家,以期物美价廉。 为了更便于开展业务,同功能的系统软件我关注了两个: Y...
    想念吗阅读 302评论 0 0