自己写一个EventBus

大名鼎鼎的EventBus很多人一定都用过,这个框架通过利用注解+反射,很好的实现了事件订阅者与发布者的解耦。今天我们就手动实现一个简易版本的EventBus。

先写我们的EventBus类,模仿真正的EventBus类,我们这个类里面包含的也是标准的几个方法:

1.void register(Object subscriber)
2.void unregister(Object subscriber)
3.void post(Object eventType)

EventBus里的方法只有这几个(因为我们是简易版本的嘛)。当然我们的EventBus是一个单例模式类,我用的是双重检测null的那种:

private static EventBus defaultInstance;

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

接下来是一个很重要的成员变量:

private HashMap<Class<?>,ArrayList<Subscription>> subscriptionsByEventType=new HashMap<>();

解释一下:HashMap的key为Class对象,其实就是订阅者中各种onEvent*函数的参数的Class对象,用过EventBus的都知道我们可以利用不同的参数对象实现不同事件的订阅。而HashMap的value是一个ArrayList,其中就是存储针对不同的参数对象的订阅。利用这个HashMap,可以轻易地找到所有订阅某一参数类型事件的所有订阅者。订阅信息就包含在类Subscription中。

我们分析一下Subscription类中应该包含哪些信息:

1.事件的订阅者,即subscriber。
2.订阅的事件,即method。
3.事件应该发生的线程,即ThreadMode。

综上,我们的类Subscription如下:

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

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

其中ThreadMode就是一个简单的枚举,作为演示我只设置了两个值:

public enum ThreadMode {
    MainThread,PostThread
}

即post事件所在的线程以及主线程main。

EventBus的成员变量暂时就这些,还有一个稍后再讲,接下来就是成员方法的实现,我们一个一个来,首先是register方法。先看一下register的全部代码:

public void register(Object subscriber){
        //根据反射机制,查找subscriber中所有已onEvent开头的method
        Class<?> clazz=subscriber.getClass();
        Method[] methods=clazz.getMethods();
        for(Method method:methods){
            String name=method.getName();
            if(name.startsWith("onEvent")){
                Class<?> param=method.getParameterTypes()[0];
                ArrayList<Subscription> subscriptions=subscriptionsByEventType.get(param);
                if(subscriptions==null){
                    subscriptions=new ArrayList<>();
                    subscriptionsByEventType.put(param,subscriptions);
                }
                //根据函数名字决定线程
                if(name.substring("onEvent".length()).length()==0){
                    //onEvent 默认为postThread
                    subscriptions.add(new Subscription(method,subscriber, ThreadMode.PostThread));
                }else{
                    //onEventMainThread
                    subscriptions.add(new Subscription(method,subscriber, ThreadMode.MainThread));
                }
            }
        }
    }

首先利用反射获取订阅者所在类中所有以"onEvent"开头的方法,获取方法的参数,根据参数类型去subscriptionsByEventType查找对应的ArrayList<Subscription>,如果找不到,说明当前还没有订阅者订阅该类型的事件,就新建一个ArrayList<Subscription>,并将该参数类型与新建的ArrayList插入到HashMap中。

接下来就是构建Subscription,这里默认onEvent()为发生在PostThread,而onEventMainThread()发生在主线程main中。这样,register方法就结束,是不是很简单呢?

unregister:

public void unregister(Object subscriber){
        Class<?> clazz=subscriber.getClass();
        Method[] methods=clazz.getMethods();
        for(Method method:methods){
            String name=method.getName();
            if(name.startsWith("onEvent")){
                Class<?> param=method.getParameterTypes()[0];
                ArrayList<Subscription> subscriptions=subscriptionsByEventType.get(param);
                if(subscriptions!=null){
                    for(Subscription subscription:subscriptions){
                        if(subscription.subscriber==subscriber) subscriptions.remove(subscription);
                    }
                }
            }
        }
    }

懂了register怎么写的写unregister就简单多了,同样利用反射找到onEvent*方法,获取参数类型,然后将该订阅者的所有订阅事件Sunscription从HashMap中去除即可。

最后是post方法:

public void post(Object eventType){
        Class<?> clazz=eventType.getClass();
        ArrayList<Subscription> subscriptions=subscriptionsByEventType.get(clazz);
        if(subscriptions==null) Log.d(TAG,"EventBus: no subscriber has subscribed to this event");
        else{
            for(Subscription subscription:subscriptions){
                switch (subscription.mode){
                    case MainThread:
                        mainThreadHandler.post(subscription,eventType);
                        break;
                    case PostThread:
                        try {
                            subscription.method.invoke(subscription.subscriber,eventType);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        } catch (InvocationTargetException e) {
                            e.printStackTrace();
                        }
                        break;
                    default:
                        break;
                }
            }
        }
    }

post中要做的事情是根据参数类型,从HashMap中取出所有订阅该事件的Subscription,然后依次执行每一个订阅。根据订阅事件发生的线程,这里分为两种情况。PostThread很简单,直接在post函数里执行即可,具体方法为利用Method.Invoke(Object obj,Object...args);

subscription.method.invoke(subscription.subscriber,eventType);

接下来是要执行应该发生在MainThread中的订阅事件,提到主线程,我们就联想到了Handler,我们可以创建一个与主线程关联的Handler,然后将事件的处理交给Handler。

MainThreadHandler:

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

    public MainThreadHandler(Looper looper) {
        super(looper);
    }

    @Override
    public void handleMessage(Message msg) {
        try {
            subscription.method.invoke(subscription.subscriber,eventType);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    public void post(Subscription subscription,Object eventType){
        this.subscription=subscription;
        this.eventType=eventType;
        sendMessage(Message.obtain());
    }
}

然后在建立EventBus的时候建立MainThreadHandler:

private EventBus(){
        mainThreadHandler=new MainThreadHandler(Looper.getMainLooper());
    };

至此,我们的EventBus全部完成,是不是很简单呢?接下来就是测试。

我在MainActivity的界面中放了两个 Button,分别对应发生在MainThread以及子线程中。

写了两个订阅函数,订阅事件类型都是ToastEvent:

public class ToastEvent {
}

MainActivity.java:

public class MainActivity extends AppCompatActivity {
    Button bt;
    Button btAsync;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        EventBus.getDefault().register(this);
        bt= (Button) findViewById(R.id.bt);
        btAsync= (Button) findViewById(R.id.btAsync);
        bt.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                EventBus.getDefault().post(new ToastEvent());
            }
        });
        btAsync.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        EventBus.getDefault().post(new ToastEvent());
                    }
                }).start();
            }
        });
    }

    public void onEvent(ToastEvent event){
        Log.d("YJW","onEvent: "+ Thread.currentThread().getName());
    }

    public void onEventMainThread(ToastEvent event){
        Toast.makeText(this, "Haha on MainThread", Toast.LENGTH_SHORT).show();
        Log.d("YJW","onEventMainThread: "+ Thread.currentThread().getName());
    }

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

点击两个Button都出现Toast,并打印相应日志,上两条日志为上面的Button,下面两条日志对应下面的Button,测试功能正常。

至此,大功告成。特别说明:真正的EventBus原理大致如此,不过更复杂,做了很多优化与特殊处理,还利用了注解。但作为演示,此demo足够了。

全部源代码点此

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

推荐阅读更多精彩内容

  • EventBus 是 Android 开发者们都很熟悉的一个库,它可以代替Intent、Handler 或者 Br...
    彼岸sakura阅读 958评论 0 7
  • 先吐槽一下博客园的MarkDown编辑器,推出的时候还很高兴博客园支持MarkDown了,试用了下发现支持不完善就...
    Ten_Minutes阅读 555评论 0 2
  • 项目到了一定阶段会出现一种甜蜜的负担:业务的不断发展与人员的流动性越来越大,代码维护与测试回归流程越来越繁琐。这个...
    fdacc6a1e764阅读 3,147评论 0 6
  • 原文链接:http://blog.csdn.net/u012810020/article/details/7005...
    tinyjoy阅读 535评论 1 5
  • 春归日暮亭头,雨初收。邀友推杯换盏五更留。聚终散,是离乱。满风楼,千语万言难说一个愁。
    长安旧人阅读 287评论 3 12