架构Android CommunityAndroid知识

自己写一个 EventBus

2017-07-10  本文已影响199人  彼岸sakura

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()方法获取单例,此后可以调用如下方法——

当然还要包含前面提到的 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 而且没有变颜色,这是什么原因呢?
本文结束!欢迎拍砖指教。
文章代码下载

上一篇下一篇

猜你喜欢

热点阅读