android 进阶半栈工程师Android开发

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

2017-09-19  本文已影响584人  安卓大叔

写在前面

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

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

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


正文

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

下面开始介绍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有四种模式,分别为:

默认的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

上一篇下一篇

猜你喜欢

热点阅读