Android通过仿写EventBus组件通信框架 掌握运行时注解+反射及事件总线通信核心原理

前言

EventBus框架想必做过Android开发的或多或少接触过,使用过;它是一款Android/Java的发布-订阅事件总线框架,简化了Android组件间消息通信的过程,将我们从复杂的组件内通信解脱出来,其Github地址EventBus;既然它这么好用,那不对它进行一番解析,或者说仿写一款同样的框架,实在说不过去;然后在加深对它的理解的同时,站在作者的角度去解决问题,也能提高我们平时的开发质量;本篇文章重点不是对它进行源码解析(源码分析放在后续文章),而是仿写一款事件总线框架

有同学可能觉得对事件总线的理解有点拗口,其实事件总线是对发布-订阅模式的一种实现,它是一种集中式事件处理机制,允许不同的组件之间进行通信,又不需要相互依赖,达到解耦的目的

组件通信方式

我们平时常用的组件通信方式有:

  • Intent:使用它实现组件跳转,并且能携带参数,但只能携带少量数据,同时在跨组件通信时局限性比较大
  • Handler:使用Handler进行组件通信耦合严重,容易造成内存泄漏
  • Broadcast:使用广播进行组件通信效率不高,安全性不高
  • Interface:不能跨线程通信
  • aidl:使用成本高

正是因为Google提供的api有各种各样的缺点,所以就产生了一些通信框架,比如RxBus,EventBus等,这里我们对EventBus进行分析

仿写EventBus

我从以下几个步骤来一步步实现仿写:

  • 分析EventBus事件分发框架
  • 分析事件分发原理和一些设计模式
  • 了解运行时注解的原理
  • 仿写EventBus事件分发框架

EventBus分析

使用过EventBus的都知道,它包含三个角色:

  • Event:事件,是Object类型,或者说是任意类型;同时EventBus会根据类型的不同进行发布消息
  • Subscriber:订阅者,现在大家应该都是用的3.0之后的版本了,只要在方法上加上@subscribe注解,方法名随意取,同时需要使用register方法进行注册,这样该组件就是一个订阅者了
  • Publisher:发布者,事件发布者可以在任意线程里进行发布,使用post方法

同时EventBus还支持线程切换,默认有四种线程模型:

  • POSTING:默认类型,即线程发布的线程类型跟订阅者的订阅方法是相同的线程类型;比如你在子线程发布一个事件,那么在Activity的订阅方法中的操作也是在子线程
  • MAIN:主线程,即订阅方法的操作是在主线程执行的,所以这时候不能进行耗时操作,不论发布者处于什么线程
  • BACKGROUND:子线程,即订阅方法的操作是在子线程执行的,所以这时候不在进行UI更新操作;如果发布者是在主线程,那将会开启一个子线程将事件推给订阅者,如果发布者在子线程,那就在这个线程里将事件推送给订阅者
  • ASYNC:子线程,不论发布者是在主线程还是子线程,都会开启一个新的线程将事件推送给订阅者

使用方法很简单:

  1. 引入依赖

    implementation 'org.greenrobot:eventbus:3.1.1'
    
  2. 封装事件对象

    public class MessageObject {
    
        public final String message;
    
    	public void setMessage(String message){
    		this.message = message
    	}
    
    	public String getMessage(){
    		return message;
    	}
    	
    }
    
  3. 发布事件

    任何对象在任何线程都可以发布事件

    EventBus.getDefault().post(new MessageObject ());
    
  4. 注册事件

    只要你对别人发布的事件感兴趣,就可以注册该事件,在订阅方法里接收事件

     @Override
     public void onStart() {
         super.onStart();
         EventBus.getDefault().register(this);
     }
    
     @Override
     public void onStop() {
         super.onStop();
         EventBus.getDefault().unregister(this);
     }
     
    @Subscribe(threadMode = ThreadMode.MAIN)  
    public void onMessageEvent(MessageObject event) {
    	/* Do something */
    };
    

EventBus原理

使用一张图来说明

在这里插入图片描述

有很多的订阅者与发布者,它们都由代理中心去管理,维护它们的关系;仔细一想,其实这跟观察者模式是有点像的(观察者模式可参考Android面试题–设计模式之观察者模式的通俗易懂实现 与发布/订阅框架有区别解析);只不过这里将发布者与订阅者之间解耦了,将所有的订阅者维护在一个Map里

注解

注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的上面,用来对这些元素进行标记说明,它本身不会在运行时起什么作用,需要我们编写注解处理器处理这些注解(编译时注解),或者在程序运行时通过反射得到这些注解做出相应的处理(运行时注解)

每个注解都必须使用注解接口@interface进行声明,这实际上会创建一个Java接口,也会编译成一个class文件,注解接口内部的元素声明实际上是方法声明,方法没有参数,没有throws语句,也不能使用泛型

注解又分为标准注解、编译时注解和运行时注解:

  • 标准注解:Java API中默认定义的注解我们称为标准注解,它们定义在java.lang、java.lang.annotation、javax.annotation中;按照使用场景不同又可以分为三类:

    • 编译相关注解:编译相关的注解是供编译期使用的,如@Override:编译器会检查被注解的方法是否真的重写了父类的方法,没有的话编译器会提示错误;@Deprecated:用来修饰任何不再鼓励使用或被弃用的方法

    • 资源相关注解:这个一般用在JavaEE领域,在Android开发中没有用到,比如@Resource:用于Web容器的资源注入,表示单个资源

    • 元注解:这个一般用来定义和实现注解的注解,也就是用来修饰注解的,总共有如下5种:

      • @Target:用来指定注解所适用的对象范围,这个注解的取值是一个ElementType类型的数组,总共有如下几种不同类型:
      1. ANNOTATION_TYPE:注解类型声明,表明该注解只能作用在注解上
      2. CONSTRUCTOR:构造方法,表明该注解作用于构造方法上
      3. FIELD:变量,表明该注解只能作用在变量上
      4. LOCAL_VARLABLE:局部变量,表明该注解作用在局部变量
      5. PARAMETER:参数,表明该注解只能作用在参数上
      6. METHOD:方法,表明该注解只能作用在方法上
      7. TYPE:类和接口,表明该注解只能作用在类和接口上
      • @Retention:用来指明注解的访问范围,也就是在什么级别保留注解,有如下三种选择:
      1. 源码级注解:在定义注解接口时,使用@Retention(RetentionPolicy.SOURCE)修饰的注解,该类型的注解信息只会保留在.java源码里,源码经过编译后,注解信息会被丢弃,不会保留在class文件中
      2. 编译时注解:在定义注解接口时,使用@Retention(RetentionPolicy.CLASS)修饰的注解,该类型的注解信息会保留在.java源码里和class文件里,在执行的时候,会被Java虚拟机丢弃,不会加载到内存中
      3. 运行时注解: 在定义注解接口时,使用@Retention(RetentionPolicy.RUNTIME)修饰的注解,该类型的注解信息不光会保留在.java源码里和class文件里,在运行时也会被虚拟机保留,可以通过反射机制读取注解信息
      • @Documented:表示被修饰的注解应该被包含在被注解项的文档中,比如用JavaDoc生成的文档
      • @Inherited:表示该注解可以被子类继承;作用在类上的自定义注解可以被继承下来。作用在接口上自定义注解不能被实现它的类继承下来;类和接口中方法上的自定义注解不能被重写/实现了其方法的子类继承,也就是说如果子类重写了父类的带有注解的方法,那是获取不到的父类的注解的,除非子类不去重写父类的方法,通过class.getMethods()获取这个类的所有方法,这样能获取父类方法上的注解
      • @Repeatable:表示该注解可以在同一个项上面应用多次,不过这个是在Java8引入的,其它四个都是Java5就存在了
  • 编译时注解:要定义编译时注解只需在定义注解时使用@Retention(RetentionPolicy.SOURCE)或者@Retention(RetentionPolicy.CLASS)修饰即可,编译时注解能够自动处理Java源文件并生成更多源码、配置文件、脚本等; 实现手段有APT(注解处理器)和JavaPoet(自动生成代码),在编译期完成操作,像@Nullable@NonNull这类的注解就是编译时注解;一些开源框架如BufferKnife,阿里路由 ARout、Dagger、Retrofit等都有使用编译时注解

  • 运行时注解:只需在定义注解时使用@Retention(RetentionPolicy.RUNTIME)修饰即可;运行时注解一般和反射配合使用,相比编译时注解,性能较低,但是灵活,实现方便;像@Subscribe@Autowired等都是通过反射API进行操作,otto、EventBus等框架会使用运行时注解

EventBus就定义了运行时注解@Subscribe,当有事件发布时,只要某个订阅者内部有方法添加了该注解,且事件类型与订阅的方法参数类型一致,那这个方法就会被调用,接受事件

新建项目

实现步骤:

  • 创建线程模式
  • 创建注解
  • 封装方法类
  • 通过反射获取被注解的方法,在发布时调用它

项目结构如图

在这里插入图片描述

创建线程模式

线程模式的意思在上面已经介绍过了,为了业务需要,有时候需要进行线程切换,它最后是作为注解的参数使用

/**
 * Author: Mangoer
 * Time: 2019/5/15 20:41
 * Version:
 * Desc: TODO(线程模型)
 */
public enum  ThreadMode {

    POSTING,

    MAIN,

    BACKGROUND,

    ASYNC
}

创建运行时注解

注解是跟类处于同一层次的东西,使用它对订阅的方法进行标记,方便我们在运行时获取到它,如下

@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Subscribe {
    //定义线程模型,指定默认值
    ThreadMode threadMode() default ThreadMode.POSTING;
    
}

以 @interface 格式声明注解,为了使这个注解有效,还需要在上面至少添加两个注解@Target 和 @Retention,它们的含义在上面已经介绍过,显然我们需要使用ElementType.METHOD和RetentionPolicy.RUNTIME;至于@Inherited注解是允许该注解被子类继承

封装方法类

要知道我们的订阅方法携带的信息比较多,比如:

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void showMsg(String msg){

    }

首先它有注解修饰,还有方法参数,还有方法本身,为了方便处理,我们对其进行封装

/**
 * Author: Mangoer
 * Time: 2019/5/15 20:23
 * Version:
 * Desc: TODO(订阅者对象)
 */
public class SubscriptionMethod {

    //订阅方法
    private Method method;

    //订阅方法的参数类型
    private Class<?> type;

    //注解里的参数
    private ThreadMode threadMode;


    public Method getMethod() {
        return method;
    }

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

    public Class<?> getType() {
        return type;
    }

    public void setType(Class<?> type) {
        this.type = type;
    }

    public ThreadMode getThreadMode() {
        return threadMode;
    }

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

获取注解方法

这一步其实是当组件进行注册时需要进行处理的,在这里将其内部使用注解修饰的订阅方法保存起来;当有发布者发布消息时再去调用该方法

这里定义一个代理类处理所有订阅者以及发布者的消息通信

/**
 * Author: Mangoer
 * Time: 2019/5/15 20:20
 * Version:
 * Desc: TODO(代理类)
 */
public class MangoBus {

    /**
     * 订阅方法
     * key 订阅类
     * value 类中的订阅方法集合
     */
    private Map<Object,List<SubscriptionMethod>>  mSubcription ;

    /**
     * 将这个类进行单例处理,保证mSubcription唯一性
     */
    private static class SingleTonInsance{
        private static final MangoBus MANGO_BUS = new MangoBus();
    }

    private MangoBus() {
        mSubcription = new HashMap<>();
    }

    public static MangoBus getInstance(){
        return SingleTonInsance.MANGO_BUS;
    }
}

接着添加注册方法,获取订阅方法

/**
     * 供订阅者调用 进行注册
     * 取出订阅者的所有订阅方法,将其保存到mSubcription
     * @param subscriber
     */
    public void register(Object subscriber){

        List<SubscriptionMethod> methodList = mSubcription.get(subscriber);
        if (methodList == null) {
            Class<?> clazz = subscriber.getClass();
            methodList = new ArrayList<>();
            /**
             * 某些情况下我们继承了父类,但父类并没有注册,只提供订阅方法,让子类去注册
             * 那么就需要将父类的订阅方法也保存起来
             */
            while (clazz != null) {
                String className = clazz.getName();
                if (className.startsWith("java.") || className.startsWith("javax.")
                        || className.startsWith("android.")) {
                    break;
                }
                findAnnotationMethod(methodList,clazz);
                clazz = clazz.getSuperclass();
            }
            mSubcription.put(subscriber,methodList);
        }
    }

    private void findAnnotationMethod(List<SubscriptionMethod> methodList, Class<?> clazz){
        //获取订阅者自身的所有方法,而getMethod会将父类的方法也拿到
        Method[] m = clazz.getDeclaredMethods();
        int size = m.length;
        for (int i=0; i<size; i++) {
            Method method = m[i];
            //拿到该方法的注解,找到使用Subscribe注解的方法
            Subscribe annotation = method.getAnnotation(Subscribe.class);
            if (annotation == null) continue;
            /**
             * 到这里说明该方法使用了我们定义的Subscribe注解
             * 接下来需要判断该注解方法是否符合规范
             * 1. 返回值必须是void
             * 2. 方法修饰符必须是public,且是非静态抽象的
             * 3. 方法参数必须只有一个
             */
            //如果方法返回类型不是void 抛出异常
            Type genericReturnType = method.getGenericReturnType();
            if (!"void".equals(genericReturnType.toString())) {
                throw new MangoBusException("方法返回值必须是void");
            }
            //如果方法修饰符不是public 抛出异常
            int modifiers = method.getModifiers();
            if ((modifiers & Modifier.PUBLIC) != 1) {
                throw new MangoBusException("方法修饰符必须是public,且是非静态,非抽象");
            }
            //如果方法参数不是一个 抛出异常
            Class<?>[] parameterTypes = method.getParameterTypes();
            if (parameterTypes.length != 1) {
                throw new MangoBusException("方法参数个数必须是一个");
            }

            //这里就需要实例化订阅方法对象了
            SubscriptionMethod subscriptionMethod = new SubscriptionMethod();
            subscriptionMethod.setMethod(method);
            subscriptionMethod.setType(parameterTypes[0]);
            subscriptionMethod.setThreadMode(annotation.threadMode());
            methodList.add(subscriptionMethod);
        }
    }

这里需要用到一个自定义的异常类

public class MangoBusException extends RuntimeException {

    public MangoBusException(String message) {
        super(message);
    }
}

获取有效的订阅方法步骤如下:

  • 拿到该方法的注解,并判断使用的是否是我们定义的注解
  • 我们规定所有的订阅方法必须是无返回值的,所以需要检查该方法如果有返回值就抛出异常
  • 规定订阅方法必须是public修饰的,否则抛出异常
  • 规定方法参数必须只能是一个,否则抛出异常

接下来就是发布者发布消息了,那就得提供一个发布的方法

    /**
     * 发布事件
     * 根据参数类型找出对应的方法并调用
     * @param event
     */
    public void post(Object event){

        Set<Object> set = mSubcription.keySet();
        Iterator<Object> iterator = set.iterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            List<SubscriptionMethod> methodList = mSubcription.get(next);
            if (methodList == null || mSubcription.size() == 0) {
                continue;
            }
            int size = methodList.size();
            for (int i = 0; i < size; i++) {
                SubscriptionMethod method = methodList.get(i);
                //method.getType()是获取方法参数类型,这里是判断发布的对象类型是否与订阅方法的参数类型一致
                if (method.getType().isAssignableFrom(event.getClass())) {
                    invoke(next,method,event);
                }
            }
        }

    }

    private void invoke(Object next, SubscriptionMethod method, Object event) {

        Method m = method.getMethod();
        try {
            m.invoke(next,event);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

这里主要就是从Map中遍历出符合要求的方法,然后执行它,到这里基本上就完成的差不多了,现在来使用下看看:现在有两个activity,第一个activity注册,从第一个activity跳转到第二个activity,第二个activity发布事件,然后结束自己回到第一个activity,看看第一个activity能不能收到刚才发布的事件

第一个activity

public class MainActivity extends AppCompatActivity {

    TextView intent;
            
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MangoBus.getInstance().register(this);

        intent = findViewById(R.id.intent);
        intent.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this,OtherActivity.class));
            }
        });
    }
    

    @Subscribe(threadMode = ThreadMode.MAIN)
    public void subscribeEvent(Event event){
        intent.setText(event.getMsg());
    }


}

第二个activity

public class OtherActivity extends AppCompatActivity {

    Button push;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.act_other);
        push = findViewById(R.id.push);
        push.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                MangoBus.getInstance().post(new Event("OtherActivity"));
                finish();
            }
        });
    }
}

在这里插入图片描述

可以看到第一个activity的textview内的文字变化了,说明这套流程已经通了

线程切换

线程切换的功能这里要通过注解的参数【线程模型】来进行动态设置

修改下post方法即可

public void post(final Object event){

        Set<Object> set = mSubcription.keySet();
        Iterator<Object> iterator = set.iterator();
        while (iterator.hasNext()) {
            final Object next = iterator.next();
            List<SubscriptionMethod> methodList = mSubcription.get(next);
            if (methodList == null || mSubcription.size() == 0) {
                continue;
            }
            int size = methodList.size();
            for (int i = 0; i < size; i++) {
                final SubscriptionMethod method = methodList.get(i);
                //method.getType()是获取方法参数类型,这里是判断发布的对象类型是否与订阅方法的参数类型一致
                if (method.getType().isAssignableFrom(event.getClass())) {
                    //进行线程切换
                    switch (method.getThreadMode()) {
                        case POSTING:
                            invoke(next,method,event);
                            break;
                        case MAIN:
                            //通过Looper判断当前线程是否是主线程
                            //也可以通过线程名判断 "main".equals(Thread.currentThread().getName())
                            if (Looper.getMainLooper() == Looper.myLooper()) {
                                invoke(next,method,event);
                            } else {
                                mHandler.post(new Runnable() {
                                    @Override
                                    public void run() {
                                        invoke(next,method,event);
                                    }
                                });
                            }
                            break;
                        case BACKGROUND:
                            if (Looper.getMainLooper() == Looper.myLooper()) {
                                THREAD_POOL_EXECUTOR.execute(new Runnable() {
                                    @Override
                                    public void run() {
                                        invoke(next,method,event);
                                    }
                                });
                            } else {
                                invoke(next,method,event);
                            }
                            break;
                        case ASYNC:
                            THREAD_POOL_EXECUTOR.execute(new Runnable() {
                                @Override
                                public void run() {
                                    invoke(next,method,event);
                                }
                            });
                            break;
                    }

                }
            }
        }

    }

接下来验证下,在订阅方法中修改下,即在子线程接收事件,然后获取下线程名,看看是不是在子线程

    @Subscribe(threadMode = ThreadMode.BACKGROUND)
    public void subscribeEvent(Event event){
        intent.setText(event.getMsg()+"-"+Thread.currentThread().getName());
    }

在这里插入图片描述

可以看到线程名不是 【main】,说明这里的线程切换是正常的

最后需要提供一个方法供订阅者取消订阅,防止出现内存泄漏

    /**
     * 取消订阅
     * @param target
     */
    public void unRegister(Object target){

        List<SubscriptionMethod> methodList = mSubcription.get(target);
        if (methodList == null) return;
        methodList.clear();
        mSubcription.remove(target);
    }

到这里我们的仿写EventBus框架就结束了,使用到的核心技术就是运行时注解和反射

代码可从Github下载

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值