自己写一个 EventBus

EventBus 是 Android 开发者们都很熟悉的一个库,它可以代替
Intent、Handler 或者 Broadcast 在各个活动、碎片、服务或者线程间传递消息,使用方便,性能开销小。
下面让我们模仿源码,写一个属于自己的小 EventBus。在理解 EventBus 工作原理基础上,也附带复习了一番 Java 的反射、注解知识,一举两得。
注意本文所指的 EventBus 是针对 3.x 版本而非 2.x。
如果你还不了解 EventBus,推荐看这篇文——

实现思路

我们用一个 Java Map 去记录所有的订阅关系。这个 Map 的键,对应一个事件;值,则对应所有订阅了此事件的方法(因此应该是一个集合)。
因此首先要定义一个订阅类,用作此集合的类型。

订阅类 Subscription

打开「安卓死丢丢」,新建一个项目 MyEventBus
订阅类与方法挂钩,又应该能指向所有的类,因此包含一个 Method 和一个 Object 字段。Object 对应的就是「订阅者」,即能接收事件的类。
新建一个类 Subscription

public class Subscription {
    public Method method;
    public Object subscriber;

    public Subscription(Method method, Object subscriber) {
        this.method = method;
        this.subscriber = subscriber;
    }
}

完成订阅类后,就可以写 EventBus主类了。

主类 EventBus

主类 通过getDefault()方法获取单例,此后可以调用如下方法——

  • register(Object subscriber)注册,让订阅者(subscriber)可以接收事件
  • unRegister(Object subscriber)反注册
  • post(Object event)发送事件

当然还要包含前面提到的 Map
新建一个类 EventBus,然后一个个实现上述的方法。

public class EventBus {
    private volatile static EventBus instance;
    private Map<Class<?>, List<Subscription>> map;

    private EventBus() {//原版这个构造器不是私有
        map = new HashMap<>();
    }
    public static EventBus getDefault() {
        if (instance == null) {
            synchronized (EventBus.class) {
                if (instance == null) {
                    instance = new EventBus();
                }
            }
        }
        return instance;
    }
    public void register(Object subscriber) {}
    public void unRegister(Object subscriber) {}
    public void post(Object event) {}
}

register 方法

EventBus 3.x 采用注解区分是否订阅方法,因此先新建一个注解接口 Subscribe,生命周期记得设为 Runtime,否则后面会反射不到。

@Retention(RetentionPolicy.RUNTIME)
public @interface Subscribe {}

然后去写 EventBus 类下的register()方法。拿到订阅者的 class,通过反射获取所有已声明的方法,遍历之。
当发现此方法有@Subscribe注解,便拿到它的 class,拿到它的参数类型(这里只考虑一个参数)。
根据参数类型从 Map 里取出订阅方法集合,如为空则新建,然后 new 出订阅类,放入集合中。

    public void register(Object subscriber) {
        Class<?> clazz = subscriber.getClass();
        //这里其实可能有NoClassDefFoundError,原版在捕获块里用的是getMethods()
        Method[] methods = clazz.getDeclaredMethods();
        for (Method m : methods) {
            if (m.isAnnotationPresent(Subscribe.class)) {
                Subscribe s = m.getAnnotation(Subscribe.class);
                //原版在这区分了不同参数列表的情况
                Class<?> c = m.getParameterTypes()[0];
                List<Subscription> list = map.get(c);
                if (list == null) {
                    list = new ArrayList<>();
                    map.put(c, list);
                }
                list.add(new Subscription(m, subscriber));
            }
        }
    }

unRegister 方法

这个简单了,就是把上一个方法反过来执行——反射获取方法组,遍历,遇到有@Subscribe注解的方法如法炮制,拿到集合,干掉对应的订阅类即可。

    public void unRegister(Object subscriber) {
        Class<?> clazz = subscriber.getClass();
        Method[] methods = clazz.getDeclaredMethods();
        for (Method m : methods) {
            if (m.isAnnotationPresent(Subscribe.class)) {
                Class<?> c = m.getParameterTypes()[0];
                List<Subscription> list = map.get(c);
                if (list != null) {
                    for (Subscription s : list) {
                        if (s.subscriber == subscriber) {
                            list.remove(s);
                        }
                    }
                }
            }
        }
    }

post 方法

这个更简单,根据事件,从 Map 里取出订阅方法集合,遍历并调用就 ok。

    public void post(Object event) {
        Class<?> clazz = event.getClass();
        List<Subscription> list = map.get(clazz);
        if (list == null) {
            return;//这里最好抛异常或打印日志,提醒调用者「没有任何类订阅该事件」
        }
        for (Subscription s : list) {
            try {
                s.method.invoke(s.subscriber, event);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

好,属于你的 EventBus 已初步搞定!马上测试一下吧。
新建一个事件类 TestEvent,携带一个字符串字段。

public class TestEvent {
    private String text;

    public TestEvent(String text) {
        this.text = text;
    }
    public String getText() {
        return text;
    }
}

新建一个活动 SecondActivity 作为发送者,里面放个按钮。
点击后 post 一个 TestEvent ,传入一句话后关闭活动。
布局文件非常简单就不上传了,有兴趣的可以下载源码。

public class SecondActivity extends AppCompatActivity 
        implements View.OnClickListener {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        findViewById(R.id.ac_second_btn1).setOnClickListener(this);
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.ac_second_btn1:
                EventBus.getDefault()//发送
                        .post(new TestEvent("Hello EventBus!"));
                finish();
                break;
        }
    }
}

新建一个活动 MainActivity 作为订阅者,里面一个按钮一个文本。
在活动的生命周期方法onCreate()里注册,onDestory()里反注册,再写一个订阅方法(名字可以随便取了,不像 2.x 版必须取那几个固定的名字),加上@Subscribe注解。把接收到的话展示在文本上再换个颜色。

public class MainActivity extends AppCompatActivity 
        implements View.OnClickListener {
    private TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        EventBus.getDefault().register(this);//注册
        tv = (TextView) findViewById(R.id.ac_main_tv);
        findViewById(R.id.ac_main_btn).setOnClickListener(this);
    }
    @Subscribe//加注解
    public void testFoo(TestEvent event) {//订阅方法(接收)
        tv.setText(event.getText());
        tv.setTextColor(getResources().getColor(R.color.colorAccent));
    }
    @Override
    protected void onDestroy() {
        super.onDestroy();
        EventBus.getDefault().unRegister(this);//反注册
    }
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.ac_main_btn:
                startActivity(new Intent(MainActivity.this, 
                        SecondActivity.class));
                break;
        }
    }
}

运行之,给力!

1.gif

这也太简单了吧?答案当然是 no。一个健壮的框架,需要考虑太多东西,比如代码的可拓展性和可读性,性能优化,可测试性,兼容性,极端情况等等。
心灵鸡汤不常说「二八定理」么,这在编程界其实非常适用!一个好程序 80% 的功能,是由它 20% 代码去实现的;剩下 80% 的代码负责的,除去那 20% 的功能,还有各种查漏补缺 and 重构优化。
下面我们来简单的扩展一下这个 EventBus 吧,让它能区分主线程(MainThread)和发送线程(PostThread)。
原版 EventBus 默认的 ThreadMode 是「发送线程」,我们也如法炮制,先新建一个常量管理类 ThreadMode

public class ThreadMode {//原版这里用的是枚举类,我个人更喜欢写成静态常量
    public static final int POST_THREAD = 0;//发送线程
    public static final int MAIN_THREAD = 1;//主线程
}

然后修改注解接口 Subscribe,添加一个字段threadMode(),默认为发送线程。

@Retention(RetentionPolicy.RUNTIME)
public @interface Subscribe {
    int threadMode() default ThreadMode.POST_THREAD;
}

新建一个类 MainThreadHandler 继承 android.os.Handler,用于把消息从发送线程传递给主线程。

public class MainThreadHandler extends Handler {
    private Object event;
    private Subscription subscription;

    public MainThreadHandler(Looper looper) {//注意构造器里带上looper
        super(looper);
    }
    public void post(Subscription subscription, Object event) {
        this.subscription = subscription;
        this.event = event;
        sendMessage(Message.obtain());
    }
    @Override
    public void handleMessage(Message msg) {
        try {
            subscription.method.invoke(subscription.subscriber, event);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }
}

修改订阅类 Subscription 的构造器,让它能分辨线程模式。

public class Subscription {
    public int mode;
    public Method method;
    public Object subscriber;

    public Subscription(Method method, Object subscriber, int mode) {
        this.method = method;
        this.subscriber = subscriber;
        this.mode = mode;
    }
}

修改主类 EventBus,增加一个成员变量 handler,在构造器里初始化。
然后修改register()post()两个方法,unRegister()不用动。

    private MainThreadHandler handler;

    private EventBus() {
        map = new HashMap<>();
        handler = new MainThreadHandler(Looper.getMainLooper());
    }
    public void register(Object subscriber) {
        Class<?> clazz = subscriber.getClass();
        Method[] methods = clazz.getDeclaredMethods();
        for (Method m : methods) {
            if (m.isAnnotationPresent(Subscribe.class)) {
                Subscribe s = m.getAnnotation(Subscribe.class);
                Class<?> c = m.getParameterTypes()[0];
                List<Subscription> list = map.get(c);
                if (list == null) {
                    list = new ArrayList<>();
                    map.put(c, list);
                }
                switch (s.threadMode()) {
                    case ThreadMode.POST_THREAD:
                        list.add(new Subscription(m, subscriber,
                                ThreadMode.POST_THREAD));
                        break;
                    case ThreadMode.MAIN_THREAD:
                        list.add(new Subscription(m, subscriber,
                                ThreadMode.MAIN_THREAD));;
                        break;
                    default:
                        break;
                }
            }
        }
    }
    public void post(Object event) {
        Class<?> clazz = event.getClass();
        List<Subscription> list = map.get(clazz);
        if (list == null) {
            return;
        }
        for (Subscription s : list) {
            switch (s.mode) {
                case ThreadMode.POST_THREAD:
                    try {
                        s.method.invoke(s.subscriber, event);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InvocationTargetException e) {
                        e.printStackTrace();
                    }
                    break;
                case ThreadMode.MAIN_THREAD:
                    handler.post(s, event);
                    break;
                default:
                    break;
            }
        }
    }

好了,测试一下!把 SecondActivityonClick()方法改一改。

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.ac_second_btn1:
                new Thread(new Runnable() {//另起一个线程发送
                    @Override
                    public void run() {
                        EventBus.getDefault()
                                .post(new TestEvent("Hello EventBus!"));
                    }
                }).start();
                finish();
                break;
        }
    }

然后给 MainActivity 的订阅方法的注解添加字段。

    @Subscribe(threadMode = MAIN_THREAD)//在主线程里处理
    public void testFoo(TestEvent event) {//接收
        tv.setText(event.getText());
        tv.setTextColor(getResources().getColor(R.color.colorAccent));
    }

效果和上面的动图是一样的。如法炮制,你能给它加入更多的功能。
留一个问题给读者思考:如果上一段不写(threadMode = MAIN_THREAD),执行结果是文本上只有一个 Hello 而且没有变颜色,这是什么原因呢?
本文结束!欢迎拍砖指教。
文章代码下载

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

推荐阅读更多精彩内容

  • 大名鼎鼎的EventBus很多人一定都用过,这个框架通过利用注解+反射,很好的实现了事件订阅者与发布者的解耦。今天...
    mrrobot97阅读 493评论 0 11
  • 前言:EventBus出来已经有一段时间了,github上面也有很多开源项目中使用了EventBus。所以抽空学习...
    Kerry202阅读 1,262评论 1 2
  • 对于Android开发老司机来说肯定不会陌生,它是一个基于观察者模式的事件发布/订阅框架,开发者可以通过极少的代码...
    飞扬小米阅读 1,420评论 0 50
  • EventBus用法及源码解析目录介绍1.EventBus简介1.1 EventBus的三要素1.2 EventB...
    杨充211阅读 1,839评论 0 4
  • 留下来自习 控制自己要自律,很多东西 6级花心思 晚上换地点自习 完成结束生活学习辅助的东西
    派森girl阅读 144评论 0 0