EventBus源码详解(二):进阶使用

写在前面

EventBus是一个Android平台上基于事件发布和订阅的轻量级框架,可以对发布者和订阅者解耦,并简化Android的事件传递。

本文是关于EventBus系列文章的第二篇,相关文章有:

如果你对EventBus不了解,我建议先阅读该系列文章的第一篇,如果对EventBus已略有所知,那么就可以开始阅读这文章。这文章是关于EventBus的进阶使用,会涉及到粘性事件、EventBus的线程调度、EventBus生成索引提高post效率等。


正文

开始介绍EventBus进阶使用前,我们先约定几个概念:

  • 订阅者:指的是调用了EventBus#register(Object)的类
  • 订阅方法:用@Subscribe标记的方法
  • 事件:被EventBus post的类,即传入EventBus#post(event)event参数

下面开始介绍EventBus的进阶使用。

粘性事件

粘性事件有点类似于Android系统的粘性广播,即在注册广播前,就把广播发送出去,当广播一注册时,就会接收到该粘性广播。而粘性事件也一样,在订阅者注册前,先把粘性事件发送出去,当订阅者注册后,立即触发粘性事件。

订阅黏性事件也很简单,首先也是先定义一个事件(注意:事件无粘性非粘性之分,它们都是类而已,区分粘性是在订阅方法的声明中):

public class StickyEvent {
    String args;
    StickyEvent(String args) {
        this.args = args;
    }
}

然后定义一个订阅方法,在注解里用@Subscribe(sticky = true)声明为粘性事件:

@Subscribe(sticky = true) // 粘性事件
public void onStickyEvent(StickyEvent event) {
    Log.d("Test", event.args);
}

然后在注册订阅者前,先post粘性事件:

EventBus.getDefault().postSticky(new StickyEvent("sticky event 1")); // 注意,是调用“postSticky”而不是“post”
EventBus.getDefault().register(this); // 一注册订阅者,就会调用上面的订阅方法

但需要注意的是,同一个粘性事件只会缓存最近一个,即当你在注册订阅者前,多次调用postSticky,只有最后一次调用才会被保留:

EventBus.getDefault().postSticky(new StickyEvent("sticky event 1"));  // 被覆盖
EventBus.getDefault().postSticky(new StickyEvent("sticky event 2"));  // 被覆盖
EventBus.getDefault().postSticky(new StickyEvent("sticky event 3"));  // 缓存
EventBus.getDefault().register(this); // 一注册订阅者,只会触发最后一个粘性事件

打开logcat会显示:

sticky event 3

为什么粘性事件在注册时就能触发呢?其实原理很简单,粘性事件只是会在注册订阅者时会被检测,如果检测到该订阅者有订阅了粘性事件,即立刻调用post粘性事件。至于更细一步分析会在解读源码文章里讲,这里先记着。

可能有人会问,粘性事件怎么用呢?下面就举一个用粘性事件来替代Intent在Activity传输数据的例子。
首先在MainActivity跳转到StickyActivity前,先post粘性事件:

/* #MainActivity */
public void startStickyActivity(View view) {
    EventBus.getDefault().postSticky(new StickyEvent("I am args"));
    startActivity(new Intent(this, StickyActivity.class));
}

然后在StickyActivityonCreateonStart里注册,就能获取到MainActivity传递的参数:

public class StickyActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_sticky);
        EventBus.getDefault().register(this);
    }

    @Subscribe(sticky = true) // 粘性事件
    public void onStickyEvent(StickyEvent event) {
        Log.d("EventBus", event.args);
        init(event.args);
    }

    private void init(String args) {
        // do init
    }
}

粘性事件介绍完了,但还要提醒注意的是:粘性事件可以当作普通事件使用。即调用EventBus#post(event)时,所订阅的方法也会被触发调用。因为EventBus没有在EventBus#post方法里对粘性事件进行过滤,而EventBus#postSticky实际上也是调用EventBus#post方法,可以先看看源码:

/* #EventBus */
public void postSticky(Object event) {
    synchronized (stickyEvents) {
        stickyEvents.put(event.getClass(), event); // stickyEvents是Map,用作缓存粘性事件
    }
    // Should be posted after it is putted, in case the subscriber wants to remove immediately
    post(event);
}
订阅方法优先级

优先级是对于同一个订阅者所订阅的同一个事件类的不同方法而言的。在声明订阅方法时,用@Subscribe(priority = ?)来指定订阅方法执行的优先级,默认优先级为0,优先级越高,越早被执行。我们来看看下面的例子:

public class PriorityActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_priority);
        EventBus.getDefault().register(this);
        EventBus.getDefault().post(new PriorityEvent());
    }

    @Subscribe(priority = 0) // 指定优先级为0,默认是0
    public void onLowPriorityEvent(PriorityEvent event) {
        Log.i("TEST", "onLowPriorityEvent");
    }

    @Subscribe(priority = 10) // 指定优先级为10
    public void onHighPriorityEvent(PriorityEvent event) {
        Log.i("TEST", "onHighPriorityEvent");
    }
}

上面例子会在logcat依次打印:

I/TEST: onHighPriorityEvent
I/TEST: onLowPriorityEvent

EventBus的线程调度

线程调度指的是可以把订阅方法抛到所指定的线程执行。经过上面的介绍,你可能猜到怎么声明了?没错,也是在标记注解时声明:

@Subscribe(threadMode = ?) // 声明执行线程
public void onThreadEvent(ThreadEvent event) {
}

ThreadMode有四种模式,分别为:

  • POSTING:在当前调用EventBus#post(event)的线程执行
  • MAIN:在主线程(即Android的UI线程)执行
  • BACKGROUND:当前执行线程为主线程,就切换到后台线程执行;当前线程为非主线程,则就在当前线程执行
  • ASYNC:新开后台线程执行

默认的ThreadModePOSTING

再来看看下面的例子:

public class ChangeThreadActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_change_thread);
        EventBus.getDefault().register(this);
        EventBus.getDefault().post(new ThreadEvent());
    }

    @Subscribe(threadMode = ThreadMode.POSTING)
    public void onPostingThreadEvent(ThreadEvent event) {
        Log.i("TEST", "POSTING --> I am on Thread " + Thread.currentThread().getName());
    }

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void onMainThreadEvent(ThreadEvent event) {
        Log.i("TEST", "MAIN --> I am on Thread " + Thread.currentThread().getName());
    }

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void onBackgroundThreadEvent(ThreadEvent event) {
        Log.i("TEST", "BACKGROUND --> I am on Thread " + Thread.currentThread().getName());
    }

    @Subscribe(threadMode = ThreadMode.ASYNC)
    public void onAsyncThreadEvent(ThreadEvent event) {
        Log.i("TEST", "ASYNC --> I am on Thread " + Thread.currentThread().getName());
    }
}

运行打开logcat可以看到下面的日志:

I/TEST: MAIN --> I am on Thread main
I/TEST: ASYNC --> I am on Thread pool-1-thread-1
I/TEST: POSTING --> I am on Thread main
I/TEST: BACKGROUND --> I am on Thread pool-1-thread-2

嗯,切换线程就这么简单!你应该知道在订阅方法执行耗时任务该怎么做了,就不再举例了。

EventBus索引生成

有人看到这里可能一头雾水,索引是什么鬼?之前都没介绍过索引相关的知识。别急,听我慢慢道来~

EventBus 3.0之前的版本是没有索引的,检索订阅方法是通过反射获取的。我们都知道反射的效率令人堪忧,如果频繁地调用的话,肯定会对程序的性能造成影响。而greenrobot也意识到这个问题,所以在EventBus 3.0版本新增一个索引的功能,它主要是通过在编译期处理,生成订阅者和订阅方法的对应关系并缓存起来,从而在程序运行时能快速索引。

EventBus是运用了观察者模式,我们知道,观察者模式一般有两个阶段:准备阶段和运行阶段。准备阶段是维护目标(Subject)和观察者(Observer)的关系,而索引的生成就是在准备阶段。

因为生成索引是在编译期的,所以需要添加一些配置。首先在project下build.gradle添加:

buildscript {
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

然后在app或lib下的build.gradle添加apt插件和EventBus apt工具:

apply plugin: 'com.neenbedankt.android-apt'

dependencies {
    apt 'org.greenrobot:eventbus-annotation-processor:3.0.1'
}

最后,也是在app或lib下的build.gradle apt参数:

apt {
    arguments {
        eventBusIndex "com.leo.eventbus.sample.SampleBusIndex" // 生成索引类,包名和类名可自定义
        verbose "true" // 是否打印编译调试日志
    }
}

build.gradle的完整代码就不贴了,如果不懂可以从文末下载源码参考。

好,现在我们rebuild下项目,可以看到在 ../build/generated/source/apt/debug/com.leo.eventbus.sample/SampleBusIndex 生成了索引。包名和类名就是我们刚刚在build.gradle配置的。

生成索引.png

生成索引后我们还需要手动给EventBus加载,最好在Application里加载:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 加载索引,添加到默认配置的EventBus
        EventBus.builder().addIndex(new SampleBusIndex()).installDefaultEventBus();
    }
}

细心的朋友可能会注意到我上面的注释:“添加到默认配置的EventBus”,所谓默认配置的EventBus也就是调用EventBus.getDefault()获取到的,是EventBus提供的,我前面的例子都是用这个默认的。而EventBus实例是可以创建多个并且相互独立的,这些将在下一节介绍。

至于EventBus.getDefault()相当于我们用单例模式的getInstance,可以先来看看源码:

/* EventBus */
static volatile EventBus defaultInstance;

public static EventBus getDefault() {
    if (defaultInstance == null) {
        synchronized (EventBus.class) {
            if (defaultInstance == null) {
                defaultInstance = new EventBus();
            }
        }
    }
    return defaultInstance;
}

installDefaultEventBus就是给defaultInstance初始化,并且规定了不能多次初始化:

public EventBus installDefaultEventBus() {
    synchronized (EventBus.class) {
        if (EventBus.defaultInstance != null) {
            throw new EventBusException("Default instance already exists." +
                    " It may be only set once before it's used the first time to ensure consistent behavior.");
        }
        EventBus.defaultInstance = build();
        return EventBus.defaultInstance;
    }
}

再来看下生成的索引类,它用Map维护着订阅者和事件的关系,如下(可以先不理解,这部分会在介绍注解时讲解,这里先看下):

/** This class is generated by EventBus, do not edit. */
public class SampleBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

    static {
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();

        putIndex(new SimpleSubscriberInfo(com.leo.eventbus.sample2.PriorityActivity.class, true,
                new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onLowPriorityEvent", com.leo.eventbus.sample2.PriorityEvent.class),
            new SubscriberMethodInfo("onHighPriorityEvent", com.leo.eventbus.sample2.PriorityEvent.class,
                    ThreadMode.POSTING, 10, false),
        }));

        putIndex(new SimpleSubscriberInfo(com.leo.eventbus.sample2.StickyActivity.class, true,
                new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onStickyEvent", com.leo.eventbus.sample2.StickyEvent.class, ThreadMode.POSTING, 0,
                    true),
        }));

        putIndex(new SimpleSubscriberInfo(com.leo.eventbus.sample2.ChangeThreadActivity.class, true,
                new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onPostingThreadEvent",
                    com.leo.eventbus.sample2.ChangeThreadActivity.ThreadEvent.class),
            new SubscriberMethodInfo("onMainThreadEvent",
                    com.leo.eventbus.sample2.ChangeThreadActivity.ThreadEvent.class, ThreadMode.MAIN),
            new SubscriberMethodInfo("onBackgroundThreadEvent",
                    com.leo.eventbus.sample2.ChangeThreadActivity.ThreadEvent.class, ThreadMode.BACKGROUND),
            new SubscriberMethodInfo("onAsyncThreadEvent",
                    com.leo.eventbus.sample2.ChangeThreadActivity.ThreadEvent.class, ThreadMode.ASYNC),
        }));

    }

    private static void putIndex(SubscriberInfo info) {
        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
    }

    @Override
    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if (info != null) {
            return info;
        } else {
            return null;
        }
    }
}

还记得我在上一篇文章 EventBus源码详解(一):基本使用 提到过:事件类和订阅者类最好用public修饰,否则有可能在生成索引时失败吗?这里提到了可能,那如果不用public修饰,什么情况下会失败?什么情况下不会失败呢?答案是:当索引类的包名与事件类和订阅者类的包名相同时,非public修饰但是为包访问权限下能生成索引,否则就会失败。

这稍微想想也知道,如果事件类和订阅者类是非public或者是private/protected修饰的话,在不同包名下是无法访问的,所以生成索引会失败。下面我们来试下,我把之前的介绍线程调度的事件类ThreadEvent改成包访问权限的:

class ThreadEvent {}

而刚刚我在build.gradle配置索引的包名是:com.leo.eventbus.sample,而我的demo包名是:com.leo.eventbus.sample2,我们重新rebuild下项目,回出现一个错误提示,打开编译日志如下:

注: Processing round 1, new annotations: true, processingOver: false
D:\ASWorkspace\EventBusSample\sample2\src\main\java\com\leo\eventbus\sample2\ChangeThreadActivity.java:33: 注: Falling back to reflection because event type is not public
    public void onPostingThreadEvent(ThreadEvent event) {
                                                 ^
注: Indexed @Subscribe at PriorityActivity.onLowPriorityEvent(PriorityEvent)
注: Indexed @Subscribe at PriorityActivity.onHighPriorityEvent(PriorityEvent)
注: Indexed @Subscribe at StickyActivity.onStickyEvent(StickyEvent)
注: Processing round 2, new annotations: false, processingOver: false
注: Processing round 3, new annotations: false, processingOver: true

日志说的很清楚,事件类为非public修饰,返回使用反射的方式检索。需要提醒一下的是:索引是维护了订阅者和订阅方法(包含事件类)的关系,如果某一个订阅者或事件类在外部无法访问,那么该订阅者和其全部订阅方法都不会生成索引。例如订阅者有两个订阅方法,参数分别为事件类Event1public修饰)和Event2(非public修饰),在生成订阅者和Event2的索引时会失败,那么订阅者和Event1的索引也会抛弃,也就相当于有原子性。

索引的使用也不难,只要注意下我提到的注意事项就可以了,至于生成索引的细节,将会在介绍EventBus注解时讲解。


写在最后

先介绍到这里了,本来打算把高级配置也写完的,发现篇幅太长了,所以留在下一篇讲~

下一篇文章: EventBus源码详解(三):高级使用


demo

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

推荐阅读更多精彩内容