手把手带你造轮子-自己动手实现一款EventBus框架

一、前言:

EventBus是一款事件发布与订阅的框架,主要用来替代Intent、Handler、BroadCast来实现组件通信,线程间通信。优点是:开销小,代码优雅,以及将发送者与接收者解耦,使用非常简便。分析EventBus的博客很多,但是很少有带大家去实现一款EventBus框架的。没错,这里我将带你实现一款自己的EventBus框架。不管你之前是否接触过EventBus,都可以通过学习而本篇文章,来学习EventBus。

二、准备

在看这篇博客之前需要对反射、注解有一定的认识,否则你是不容易看懂的。

三、流程

整个EventBus发布订阅的流程可以形象为以下过程,包括后面的部分都是以这个流程来分析的

1、订阅过程:

假设某公司要招聘Android、IOS、前端、Java...程序员,它会先在招聘中介网站上(比如“前程无忧”)提交这些信息。而“前程无忧”需要记下这些信息,它会将该公司招聘的所有岗位存入一个List<招聘岗位>集合中,因为招人的公司肯定不止这一家,所以然后"前程无忧"会以公司为键,招聘岗位List为值存入一个Map<公司,List<招聘岗位>>中。这就是一个订阅过程、招聘的公司就是订阅者、“前程无忧”就是一个中介。

2、发布过程:

求职者为了找工作,也会去“前程无忧”上面发布找工作的消息,假设我们找的是Android的工作,“前程无忧”就会去它的Map<公司名,List<招聘岗位>>中找,看有哪个公司是招聘Android的,找到之后就会把Android求职推荐给该公司。

3、注销过程:

假设公司不需要再招人,那么怎么办呢?那就注销呗,此时“前程无忧”就会在它的Map<公司名,List<招聘岗位>>中,将该公司移除掉。

上面的过程大概可以用下面这张图来表示:

IMG20170527163016.jpg

理清思路之后我们就可以来撸码了。

四、正式撸码

1、先看下我们所写代码的整体结构:

![


021IU)(QA507A[Y3K%@20KT.png](http://upload-images.jianshu.io/upload_images/4933844-54f113ac78de4734.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

没错,就这四个类就是实现了一款EventBus,麻雀虽小,但也是五脏俱全。EventBus类就是“前程无忧”,SubscribleMethod类就是招聘岗位,ThreadMode和Subscrible是啥先别管它。

2.EventBus类的基本实现(前程无忧)

求职者要找工作,公司要招人,那么肯定得先有一个招聘的招聘的中介"前程无忧"。那么我们现在就来写一个中介EventBus类,至于怎么写了?大家在使用EventBus的时候都知道使用以下这句代码进行注册(订阅):
EventBus.getDefault().regist(this);
那么我们就依葫芦画瓢,写一个getDefault()
方法,代码如下:

/**
 * Created by Administrator on 2017/5/22.
 * 招聘中介
 */
public class EventBus {
    //招聘中介的信息表(表里面填了各大公司需要招聘的岗位)
    //Map<招聘公司,该公司招聘的岗位集合>
    private Map<Object,List<SubscribleMethod>> cacheMap;
    private static volatile EventBus instance;
    private EventBus(){
        cacheMap=new HashMap<>();
    }
    public static EventBus getDefault(){
        if(instance==null){
            synchronized (EventBus.class){
                if(instance==null){
                    instance=new EventBus();
                }
            }
        }
        return instance;
    }
}

代码行数不多,采用了一个双重检查锁式的单例模式来创建EventBus的实例。cacheMap就是用来存储各公司招聘信息的。SubscribleMethod这个类就是招聘岗位信息的封装类,暂时不用管它,咱们后面再说。

2、注册方法的实现()

有了EventBus的实例,那么我们就来实现regist方法,代码如下:

 /**
     * 注册
     * @param obj 公司
     */
    public void regist(Object obj){ 
        List<SubscribleMethod> list = cacheMap.get(obj);
        //还没有注册过,那么就将岗位集合put进中介的map中,
        //如果已经注册过了,那么久不用再次注册,那么什么都不做
        if(list==null){
            //找到公司obj招的所有岗位的集合
            List<SubscribleMethod> methods = findSubscribleMethod(obj); 
            cacheMap.put(obj,methods);
        }else {
            throw new RuntimeException("你就已经注册(订阅)过了,不需要再次注册(订阅)");
        }
    }

上面这个注册方法,参数就是要注册的公司对象。首先会去“前程无忧”的Map集合里面找一下,看是否这个公司已经注册(订阅)过了,如果已经注册(订阅)过了,那就抛一个异常告诉它不需要再注册了,如果没有注册(订阅),才让他注册(订阅),注册的公司obj需要招人,那么就得知道obj这个公司到底招聘那些岗位,findSubscribleMethod(obj)这个方法就是找到公司obj的所有招聘岗位(具体实现咱们后面再说,你现在只要知道它是干啥的就行),找到公司obj所有岗位的集合后,后面的自然是将公司招聘信息存入“前程无忧”的招聘信息表Map里面去了, 也就是这句代码cacheMap.put(obj,methods)。

2、Subscrible是个啥?

上面的代码结构图中出现Subscrible,那它是个啥呢?我们现在就来说一说,代码如下:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Subscrible {
    ThreadMode value() default ThreadMode.PostThread;
}

没错,它就这几行代码,灰常简单,可以看出它就是一个注解, 简单的来说,招聘的公司招人它得有个招聘的方法呀,招聘的公司肯定也得干其他的事,肯定也有非招聘的方法,那么怎么知道它的哪些方法是招聘的方法呢?那么就给它的招聘方法上面加个Subscrible注解我们就知道了,里面的value()先不管它,下面先给个示例:

 /**
     *公司招人的方法
     * @param content
     */
    @Subscrible(ThreadMode.PostThread)
    public void receive(AndroidProgrammer content){
        Log.i("zkx","Thread  "+Thread.currentThread().getName());
        //Toast弹不出来,不知道为啥
        //Toast.makeText(this,Thread.currentThread().getName(),Toast.LENGTH_SHORT);
    }

@Subscrible(ThreadMode.MainThread)这句代码就表示了这个方法是一个招聘方法,()里面的代码表示这个方法是在主线程里面执行的。该方法的参数类型就表明了招的是什么职位,我这里是招聘AndroidProgrammer,至于ThreadMode请看下面。

3、ThreadMode是个啥?

ThreadMode听名字就知道是线程模式的意思,具体的请看代码:

public enum ThreadMode {
    PostThread,//发送线程执行
    MainThread,//主线程执行
    BackgroundThread,//后台线程执行
    Async

这段其实就是直接复制EventBus源码里面的,英文注释太长了,我给去掉了。可以看出他就是一个枚举,请看我给出的注释。再回到
注解Subscrible的代码中,我们就可以知道它里面的value()值就是说加上这个注解的在哪个线程执行了,我的代码中默认指定的是PostThread。

4、SubscribleMethod是个啥?

从名字是可以看出它是订阅方法的意思,实际是上它是封装了招聘岗位信息(公司招聘的方法、招聘岗位的类型、招聘方法在哪个线程执行)的一个JavaBean。具体代码如下:

**
 * Created by Administrator on 2017/5/22.
 * 招聘岗位信息类
 */
public class SubscribleMethod {
    //招聘这个岗位的方法
    private Method method;
    //招聘这个岗位的方法在哪个线程里面执行
    private ThreadMode threadMode;
    //招聘的岗位的类型
    private Class<?> eventType;

    public SubscribleMethod(Method method, ThreadMode threadMode, Class<?> eventType) {
        this.method = method;
        this.threadMode = threadMode;
        this.eventType = eventType;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public ThreadMode getThreadMode() {
        return threadMode;
    }

    public void setThreadMode(ThreadMode threadMode) {
        this.threadMode = threadMode;
    }

    public Class<?> getEventType() {
        return eventType;
    }

    public void setEventType(Class<?> eventType) {
        this.eventType = eventType;
    }
}

5、深入细节

到这里EventBus的各个类的意思已经说完了,我们来看一些细节的东西。上面“2、注册方法的实现”这段,我们说到findSubscribleMethod(obj)这个方法就是找到公司obj的所有招聘岗位,那么我们就来看看它是怎么找的,代码如下:

 /**
     * 寻找该公司的招聘岗位
     * @param activity 该公司
     * @return
     */
    private List<SubscribleMethod> findSubscribleMethod(Object activity) {
        List<SubscribleMethod> list  = new CopyOnWriteArrayList<>();
        Class<?> clazz = activity.getClass();
        Method[] methods = clazz.getDeclaredMethods();
        //循环查找父类 接收方法
        while(clazz!=null){//如果该公司又隶属于某个更大的公司,那么它也要为这个更大的公司招人
            String name = clazz.getName();
            //系统方法直接break
            if(name.startsWith("java.")||name.startsWith("javax.")||name.startsWith("android.")){
                break;
            }
            //遍历所有类的方法(其中有Subscrible注解的为招人的岗位(方法))
            //每找到一个招聘岗位就将其放到集合中,最终返回的集合list就是该公司所有招聘岗位的集合
            for (Method method:methods){
                //持有Subscrible注解的才是个接收方法,才是个岗位
                //发生线程
                Subscrible subscrible = method.getAnnotation(Subscrible.class);
                if (subscrible==null){
                    continue;
                }
                //拿到方法的参数类型的数组
                Class<?>[] parameters = method.getParameterTypes();
                if(parameters.length!=1){//只能接收一个参数
                    throw new RuntimeException("eventbus must be one parameter");
                }
                //获得参数类型数组的第一个元素(也只有一个元素)
                Class<?> paramClass  = parameters[0];
                //拿到发生的线程(默认发生在PostThread)
                ThreadMode threadMode = subscrible.value();
                //new一个招聘岗位
                SubscribleMethod subscribleMethod = new SubscribleMethod(method,threadMode,paramClass);
                //招聘中介(前程无忧) 把该公司所有岗位缓存到集合
                list.add(subscribleMethod);
            }
            clazz=clazz.getSuperclass();
        }
        return list;
    }

从代码总可以看出,我们其实就是通过反射拿到该招聘公司的所有方法,遍历这些方法,判断这些方法是不是招聘的方法,具体的就是看这些方法有没有加Subscrible 注解,加了注解的肯定就是招聘方法,继而拿到,每一个招聘方法的参数类型,也就是招聘的岗位,然后将所有的岗位存入List集合中。还有一点要注意的是如果公司类是继承自某个类,也就是说该公司它是某个更大的公司的一个子公司,那么它在招人的同时也得给它的父公司招人。所以有了我们的最外层的while循环,不断遍历它的父类,直到系统类为止。

6、发布的方法

到这里,中介有了,公司也注册(订阅)了,那么就到我们求职者出场了,假设我们要找Android程序员的工作,那么就只需要调用下面这句代码就行:

EventBus.getDefault().post(new AndroidProgrammer("zkx","123456"));

其实就是调用了EventBus的post方法,具体实现如下:

 /**
     * 安卓程序员 来找工作
     * @param object 求职的岗位类型
     */
    public void post(final Object object) {
        //拿到集合cacheMap里所有的公司
        Set<Object> set = cacheMap.keySet();
        Iterator iterator = set.iterator();
        while(iterator.hasNext()){
            //拿到具体的每一个公司
            final Object activity = iterator.next();
            //拿出该公司招聘的岗位集合
            List<SubscribleMethod> list = cacheMap.get(activity);
            //遍历该公司招聘岗位的集合,看有没有招聘安卓的
            for (final SubscribleMethod method:list) {
                //如果是属于同样的类型(如果有岗位匹配)
                if(method.getEventType().isAssignableFrom(object.getClass())){
                    //判断当前接收方法是在哪个线程
                    switch (method.getThreadMode()){
                        //发送方法的线程和接收的线程在同一个线程,直接invoke就完事了
                        case PostThread:
                            //activity:公司,method:岗位,object:求职者
                            //岗位匹配,把这个人招进来
                            invoke(activity,method,object);
                            break;
                        case MainThread:
                            //判断发送线程是在哪个线程,
                            //如果发送线程就是主线程就不用切换线程
                            if(Looper.myLooper()==Looper.getMainLooper()){
                                //直接调用
                                invoke(activity,method,object);
                            }else {//发送是在子线程
                                handler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        invoke(activity,method,object);
                                    }
                                });
                            }
                            break;
                        //指定接收方法发生在子线程
                        case BackgroundThread:
                            //如果当前发送线程就是子线程就不用切换线程
                            if(Looper.myLooper()!=Looper.getMainLooper()){
                                //发生在子线程
                                invoke(activity,method,object);
                            }else {
                                executorService.execute(new Runnable() {
                                    @Override
                                    public void run() {
                                        invoke(activity,method,object);
                                    }
                                });
                            }
                            break;
                    }
                }
            }
        }
    }

代码有些长,但是基本上每行代码都给出了注释。理解起来应该不难。大体的流程就是在”前程无忧”的信息表里面拿到所有的公司,然后遍历这些公司,看这些公司的招聘岗位里面有木有Android的岗位(是否匹配),如果有的话,那就利用反射去调用有招聘Android岗位的公司的招聘方法来招聘这个求职者。还有需要注意的就是,我们会根据发布方法(求职者发布求职的方法)和接收方法(公司招人的方法)所在线程的不同,采取不同的策略。具体请看代码,有详细的注释。反射调用公司的招聘方法代码如下:


    /**
     * @param activity 公司
     * @param method   岗位信息封装类
     * @param object   求职者
     */
    private void invoke(Object activity, SubscribleMethod method, Object object) {
        try {
            //调用公司activity的岗位method的招人方法method.getMethod()方法,
            //传参object,代表招Object类型岗位的人
            method.getMethod().invoke(activity,object);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

7注销方法的实现(取消订阅)

公司不招人了,那么就得注销(取消订阅),代码很简单,就是将该公司的信息从EventBus的map中移除就行了

 public void unregist(Object object){
        //在中介的map中将该公司object移除
        cacheMap.remove(object);
 }

五、最后

到这里我们所有的代码就写完了,现在就可以使用我们写的这款框架来进行组件间、线程间通信了,写的不好的或者错误的,欢迎批评指正。
全部代码下载地址:
https://github.com/zkxok/MyEventBus

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,049评论 25 707
  • EventBus源码分析(一) EventBus官方介绍为一个为Android系统优化的事件订阅总线,它不仅可以很...
    蕉下孤客阅读 3,966评论 4 42
  • 感恩今天和好姐妹一起庆祝,庆祝我的第一份兼职收入资金到位,用劳动和学习来的成果换取的劳务费,值得庆祝下。也要感谢我...
    念秀阅读 90评论 0 1
  • 各位亲: 因朋友拥有一个车类域名,可能是职业的原因,一直以来从事,网络运营工作,十来年,也已经做到公司高层,这次应...
    tyuguang阅读 1,114评论 1 50
  • 大河向东流,天上的星星参北斗.....大家是唱下来这句话的请点赞了。 这首好汉歌来大家都很熟悉,水浒传主题曲。水浒...
    小学生老韩阅读 738评论 0 0