自己写一个 EventBus

作者: 彼岸sakura | 来源:发表于2017-07-10 10:31 被阅读199次

    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 而且没有变颜色,这是什么原因呢?
    本文结束!欢迎拍砖指教。
    文章代码下载

    相关文章

      网友评论

        本文标题:自己写一个 EventBus

        本文链接:https://www.haomeiwen.com/subject/lczcfxtx.html