EventBus源码解析(一)关于用法和注解

文章基于EventBus 3.0讲解。
首先对于EventBus的使用上,大多数人还是比较熟悉的。如果你还每次烦于使用接口回调,广播去更新数据,那么EventBus可以帮助你解决这个问题。
第一篇主要将一些用法和注解

EventBus源码解析系列

EventBus源码解析(一)关于用法和注解
EventBus源码解析(二)register与unregister
EventBus源码解析(三)Post方法和注解处理器

一. 使用

先从平常我们的使用方法看,Service处理数据,处理完发给Activity做显示

@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView)findViewById(R.id.tv);
        EventBus.getDefault().register(this);
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                Intent intent = new Intent(MainActivity.this , TestService.class);
                startService(intent);
            }
        } , 3000);
        Log.e(TAG, "onCreate: " );
    }

    @Subscribe
    public void onEventMainThread(String s){
        Log.e(TAG, "onEventMainThread: " + s );
        tv.setText(s);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unregister(this);
    }

在onCreate调用了EventBus.getDefault().register(this);,在onDestory解除了注册。注册之后就可以在@Subcribe注解下面的方法接收到。而发送事件则是在Service

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        EventBus.getDefault().post("Service Test");
        return super.onStartCommand(intent, flags, startId);
    }

Service和Activity之间的通讯通过EventBus就可以很简单的实现出了,如果换成以前的话,则是使用

  • 接口回调
  • IBinder
  • 广播

来进行联系,但是如果这样的话一些代码上就会显得有些冗余,而使用EventBus则可以很简便。

此外,这EventBus的版本迭代上,在3.0则是采用了注解来进行监听事件。
上面那个监听方法

@Subscribe
    public void onEventMainThread(String s){
        Log.e(TAG, "onEventMainThread: " + s );
        tv.setText(s);
    }

只要通过@Subcribe注解的方法,且类型是public的,方法名可以自由取

@Subscribe
    public void test(String s){
        Log.e(TAG, "onEventMainThread: " + s );
        tv.setText(s);
    }

而在3.0之前则默认了onEvent+类型 的方法名,然后通过反射来获取对应的方法。

    //3.0前的版本
    public void onEventMainThread(String s){
        Log.e(TAG, "onEventMainThread: " + s );
        tv.setText(s);
    }

二.注解

这里主要说说注解的使用与实例。没知道注解的可以了解的。
前面讲到了EventBus的一个注解@Subscribe,点进去看下类型

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;
    ...
    boolean sticky() default false;
    ...
    int priority() default 0;
}

没了解过注解的话可能会有点懵。

注解的语法其实比较简单,除了@符号的使用外,它基本与Java固有的语法一样。Java SE5则内置了三种标准的注解

  • @Override:表示当前的方法将覆盖超类中的方法
  • @Deprecated:使用了注解为它的元素编译器将发出警告,因为@Deprecated注解是被弃用的代码,不被赞成
  • @SuppressWarnings:关闭编译器警告信息

对于上述的三个注解大家在日常开发中算是经常见到的,点进去看他们的结构也和@Subscribe差不多.

Java提供了4种注解
  • @Target :表示该注解可以用于什么地方,可能的ElementType参数有:

  • @Retention:表明需要在什么级别保存该注解信息。可选的RetentionPolicy参数包括:

    • SOURCE:注解将被编译器丢弃
    • CLASS:注解在class文件中可用,但会被VM丢弃
    • RUNTIME:VM将在运行期间保留注解,因此可以通过反射机制读取注解的信息。
  • @Document:将注解包含在Javadoc中

  • @Inherited:允许子类继承父类中的注解

定义一个注解的方法

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
}

除了@符号,注解很像是一个接口。定义注解的时候需要用到元注解,上面用到了@Target@RetentionPolicy,它们的含义在上面已给出。

在注解中一般会有一些元素来表示某些值,注解里面的元素看起来很像接口的方法,唯一区别在于注解可以为其制定默认值,没有元素的注解称为标记注解,上面的Test就是一个标记注解。

注解的可用的类型包括以下几种:所有基本类型、String、Class、enum、Annotation、以上类型的数组形式。元素不能有不确定的值,即要么有默认值,要么在使用注解的时候提供元素的值。而且元素不能使用null作为默认值。注解在只有一个元素且该元素的名称是value的情况下,在使用注解的时候可以省略“value=”,直接写需要的值即可。

比如

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
    public String id();
    public String userName() default "Hohohong";
}

这里则有两个元素,他们的类型是String型,第二个元素则设置了默认值。再看下前面@Subscrive注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;
    ...
    boolean sticky() default false;
    ...
    int priority() default 0;
}

它保留在VM运行时,用于方法上。三个元素中,第一个元素的类型是Enum型,默认值则为ThreadMode.POSTING,也比较好理解了。

定义好注解之后,就是如何使用了

public class TestUtil {
    @Test(id="0" , userName = "Hong")
    public void getData(String data){
        Log.e("TestUtil", "getData: " + data );
    }

    @Test(id="1")
    public void dataTest(){
        Log.e("TestUtil", "dataTest" );
    }

使用注解最主要的部分在于对注解的处理,那么就会涉及到注解处理器。

从原理上讲,注解处理器就是通过反射机制获取被检查方法上的注解信息,然后根据注解元素的值进行特定的处理。

 public static void main(String args[]){
        trackTest(TestUtil.class);
    }
    public static void trackTest(Class<?> cls){
        Method[] methods = cls.getMethods();
        for(Method method : methods){
            Test test = method.getAnnotation(Test.class);
            if(test != null){
                System.out.println("Found Test : id = " + test.id() + "  userName = " + test.userName() );
            }
        }
    }

这里就可以拿到每个注解方法上注解的一些属性。
关于注解更多的使用就不扩展开了。

三.EventBus的注解

还是前面那个@Subscribe

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;
    ...
    boolean sticky() default false;
    ...
    int priority() default 0;
}

ThreadMode是一个enum类型,里面定义了四种类型

public enum ThreadMode {
   
    POSTING,

    MAIN,

    BACKGROUND,

    ASYNC
}

对应的方法则为

  • onEventPostThread:代表这个方法会在当前发布事件的线程执行,也就是执行事件和发送事件都在同一个线程中。(默认是这个)

  • onEventMainThread:代表这个方法会在主线程执行

  • onEventBackgroundThread:如果发送事件的线程不是UI线程,则运行在该线程中。如果发送事件的是UI线程,则它运行在由EventBus维护的一个单独的线程池中,事件会加入后台任务队列,使用线程池一个接一个调用。

  • onEventAsync:运行在单独的工作线程中,不论发送事件的线程是否为主线程。跟BackgroundThread不一样,该模式的所有线程是独立的,因此适用于长耗时操作,例如网络访问。它也是使用线程池调用,注意没有BackgroundThread中的一个接一个。

3.1ThreadMode.POSTING

首先@Subscribe它里面ThreadMode默认值是POSTING,也就是说,执行事件和发送事件在同一个线程。

@Subscribe
    public void onEventMainThread(String s){
        Log.e(TAG, "Current Thread name: " + Thread.currentThread().getName() );
        Log.e(TAG, "Current Thread is MainThread? : " + (Thread.currentThread() == Looper.getMainLooper().getThread()) );
        tv.setText(s); //主线程才能更新UI,如果POST在子线程运行则报错
    }
@Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        EventBus.getDefault().post("Service Test");
        return super.onStartCommand(intent, flags, startId);
    }

当然这里的方法名是随便的,如果发送事件在子线程的话可能会有点误区。结果为

 E/MainActivity: Current Thread name: main
 E/MainActivity: Current Thread is MainThread? : true

如果在子线程Post

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                EventBus.getDefault().post("Service Test");
            }
        } , "childThread").start();
        return super.onStartCommand(intent, flags, startId);
    }

结果则为

E/MainActivity: Current Thread name: childThread
E/MainActivity: Current Thread is MainThread? : false

此时在方法里则不能进行UI更新操作。此时如果把它的模式设置为MAIN,则没问题

3.2 ThreadMode.MAIN

@Subscribe(threadMode = ThreadMode.MAIN)
    public void onEventMainThread(String s){
        Log.e(TAG, "Current Thread name: " + Thread.currentThread().getName() );
        Log.e(TAG, "Current Thread is MainThread? : " + (Thread.currentThread() == Looper.getMainLooper().getThread()) );
        tv.setText(s);
    }

结果则跟前面一样,MAIN则是无论发送事件是否在主线程,执行事件总是在主线程

 E/MainActivity: Current Thread name: main
 E/MainActivity: Current Thread is MainThread? : true

3.3 ThreadMode.BACKGROUND

来验证下BACKGROUND,前面介绍则是如果发送事件的线程不是UI线程,则运行在该线程中。如果发送事件的是UI线程,则它运行在由EventBus维护的一个单独的线程池中。延续前面的例子,发送事件是在子线程childThread中,执行事件方法我们指定为ThreadMode.BACKGROUND

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                EventBus.getDefault().post("Service Test");
            }
        } , "childThread").start();
        return super.onStartCommand(intent, flags, startId);
    }
 @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onEventMainThread(String s){
        Log.e(TAG, "Current Thread name: " + Thread.currentThread().getName() );
        Log.e(TAG, "Current Thread is MainThread? : " + (Thread.currentThread() == Looper.getMainLooper().getThread()) );
        tv.setText(s);
    }

此时结果则为

E/MainActivity: Current Thread name: childThread
E/MainActivity: Current Thread is MainThread? : false

跟描述的一样
我们将发送事件的方法放到UI线程看下

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
                EventBus.getDefault().post("Service Test");
        return super.onStartCommand(intent, flags, startId);
    }

结果则为

E/MainActivity: Current Thread name: pool-1-thread-1
E/MainActivity: Current Thread is MainThread? : false

可以看到它运行在EventBus维护的一个线程池中。

3.3 ThreadMode.ASYNC

和前面一样做测试,就不贴出代码,直接给出测试结果。
无论当前发送事件是否在主线程还是子线程,执行事件都是在EventBus的线程池中

E/MainActivity: Current Thread name: pool-1-thread-1
E/MainActivity: Current Thread is MainThread? : false

那么从这一点上看又与ThreadMode.BACKGROUND有什么不用呢,前面说到,BACKGROUND里面的线程池会把当前事件添加到任务队列,一个执行完才到下一个,而ThreadMode.ASYNC从名字上看就知道是异步的,也就是每个事件都可以同时并发执行。

我们看下测试用例
首先,我们一连发送3个事件

 @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        EventBus.getDefault().post("1 Service Test");
        EventBus.getDefault().post("2 Service Test");
        EventBus.getDefault().post("3 Service Test");
        return super.onStartCommand(intent, flags, startId);
    }

模式指定为BACKGROUND

 @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onEventMainThread(String s){
        Log.e(TAG, "currrent String : " + s );
        Log.e(TAG, "Current Thread name: " + Thread.currentThread().getName() );
        Log.e(TAG, "Current Thread is MainThread? : " + (Thread.currentThread() == Looper.getMainLooper().getThread()) );
        //tv.setText(s);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

看下结果输出

03-12 04:29:43.259 E/MainActivity: currrent String : 1 Service Test
03-12 04:29:43.259 E/MainActivity: Current Thread name: pool-1-thread-1
03-12 04:29:43.259 E/MainActivity: Current Thread is MainThread? : false

03-12 04:29:46.263 E/MainActivity: currrent String : 2 Service Test
03-12 04:29:46.263 E/MainActivity: Current Thread name: pool-1-thread-1
03-12 04:29:46.263 E/MainActivity: Current Thread is MainThread? : false

03-12 04:29:49.263 E/MainActivity: currrent String : 3 Service Test
03-12 04:29:49.263 E/MainActivity: Current Thread name: pool-1-thread-1
03-12 04:29:49.263 E/MainActivity: Current Thread is MainThread? : false

可以看到他们里面用的都是同一个线程,一个事件在执行,另一个事件则需要等待。

再看下指定为ThreadMode.ASYNC的结果

03-12 04:27:43.435 E/MainActivity: currrent String : 1 Service Test
03-12 04:27:43.435 E/MainActivity: Current Thread name: pool-1-thread-1
03-12 04:27:43.435 E/MainActivity: Current Thread is MainThread? : false
    
03-12 04:27:43.435 E/MainActivity: currrent String : 2 Service Test
03-12 04:27:43.435 E/MainActivity: Current Thread name: pool-1-thread-2
03-12 04:27:43.435 E/MainActivity: Current Thread is MainThread? : false
    
03-12 04:27:43.439 E/MainActivity: currrent String : 3 Service Test
03-12 04:27:43.439 E/MainActivity: Current Thread name: pool-1-thread-3
03-12 04:27:43.439 E/MainActivity: Current Thread is MainThread? : false

注意看时间和线程名字,可以看到他们几乎是同时执行的,而且各自运行在单独的线程之中。对比出来也比较明了了。

讲解完ThreadMode之后,@Subscribe还有其他两个属性

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Subscribe {
    ThreadMode threadMode() default ThreadMode.POSTING;
    ...
    boolean sticky() default false; //是否是粘性操作
    ...
    int priority() default 0; //优先级
}
  • int priority:表示的是当前事件的优先级,设置该优先级的目的是,当一个事件有多个订阅者的时候,优先级高的会优先接收到事件。
 @Subscribe(threadMode = ThreadMode.MAIN ,priority = 0)
    public void priority0(String s){
        Log.e(TAG, "priority0: " + s );
    }

    @Subscribe(threadMode = ThreadMode.MAIN ,priority = 50)
    public void priority50(String s){
        Log.e(TAG, "priority50: " + s );
    }

    @Subscribe(threadMode = ThreadMode.MAIN ,priority = 100 )
    public void priority100(String s){
        Log.e(TAG, "priority100: " + s );
    }

发送一个事件之后,接受到的结果为

03-13 01:46:57.263 E/MainActivity: priority100: Service Test
03-13 01:46:57.263 E/MainActivity: priority50: Service Test
03-13 01:46:57.263 E/MainActivity: priority0: Service Test

如果此时去掉优先级的话,则顺序是无序的。

  • boolean sticky:表示的是粘性事件,类似于Android的粘性广播,通俗讲就是当你发送事件的时机比注册时机早的时候,如果设置了sticky粘性,则可以在发送事件完毕后再注册,也可以收到事件,但对于同种事件类型(参数类型)的话只会接受到最近发送的粘性事件,以前的不会收到。我们来验证一下。
   @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        EventBus.getDefault().postSticky("Stick Event 1");
        EventBus.getDefault().postSticky("Stick Event 2");
        EventBus.getDefault().postSticky(10);
        EventBus.getDefault().postSticky(20);
    }
    
    @Subscribe(threadMode = ThreadMode.MAIN , sticky = true)
    public void receiveEventString(String s){
        Log.e(TAG, "receiveEventString: " + s );
    }
    @Subscribe(threadMode = ThreadMode.MAIN , sticky = true)
    public void receiveEventInt(Integer i){
        Log.e(TAG, "receiveEventInt: " + i );
    }

可以看到这里注册的时机是在按钮点击之后,但之前消息已经是发送出去了。此时点击注册后,可以看到输出结果

E/MainActivity: receiveEvent: Stick Event 2
E/MainActivity: receiveEventInt: 20

粘性事件的内部,则是用了一个Map在保存这些粘性事件,而key则是这些粘性事件的参数类型,所以对于同个参数类型的话,最终只会保存最近那个使用的。

 public void postSticky(Object event) {
        synchronized (stickyEvents) {
            stickyEvents.put(event.getClass(), event);
        }
        // Should be posted after it is putted, in case the subscriber wants to remove immediately
        post(event);
    }

粘性事件的发送必须满足
* 发送事件必须是EventBus.getDefault().postSticky
* 方法的注解要设置stick = true,而这个默认值则为false

四.小结

  • EventBus在3.0版本可说是做了大改,很多之前版本一些缺点都做了改正,提供了注解,性能有了很大的提升,其实是提供了4种ThreadMode来适应不同的网络情况,相比于Otto来说EventBus的网络请求功能更加丰富

  • ThreadMode.POST:代表这个方法会在当前发布事件的线程执行,也就是执行事件和发送事件都在同一个线程中。(默认是这个)

  • ThreadMode.MAIN:代表这个方法会在主线程执行

  • ThreadMode.BACKGROUND:如果发送事件的线程不是UI线程,则运行在该线程中。如果发送事件的是UI线程,则它运行在由EventBus维护的一个单独的线程池中,事件会加入后台任务队列,使用线程池一个接一个调用。

  • ThreadMode.ASYNC:运行在单独的工作线程中,不论发送事件的线程是否为主线程。跟BackgroundThread不一样,该模式的所有线程是独立的,因此适用于长耗时操作,例如网络访问。它也是使用线程池调用,注意没有BackgroundThread中的一个接一个。

参考资料

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

推荐阅读更多精彩内容

  • EventBus 是一个Android端优化的 publish/subscribe 消息总线,简化了应用程序各个组...
    王世军Steven阅读 1,824评论 4 21
  • EventBus用法及源码解析目录介绍1.EventBus简介1.1 EventBus的三要素1.2 EventB...
    杨充211阅读 1,839评论 0 4
  • 对于Android开发老司机来说肯定不会陌生,它是一个基于观察者模式的事件发布/订阅框架,开发者可以通过极少的代码...
    飞扬小米阅读 1,420评论 0 50
  • 简介 我们知道,Android应用主要是由4大组件构成。当我们进行组件间通讯时,由于位于不同的组件,通信方式相对麻...
    Whyn阅读 499评论 0 1
  • 前言:EventBus出来已经有一段时间了,github上面也有很多开源项目中使用了EventBus。所以抽空学习...
    Kerry202阅读 1,260评论 1 2