专栏首页终身开发者100行代码拆解EventBus核心逻辑(三)

100行代码拆解EventBus核心逻辑(三)

关于我 一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。 Github:https://github.com/hylinux1024 微信公众号:终身开发者(angrycode)

在前文的讲解中对 EventBus 的实现逻辑有了大概的理解之后,我们知道 Java 解析注解可以在运行时解析也可以在编译期间解析。由于运行时解析是通过反射来获取注解标记的类、方法、属性等对象,它的性能要受到反射的影响。因此在一些基础组件中更常见的做法是使用注解解析器技术,像 DaggerbutterknifeARouter 以及本文所接触的 EventBus等框架库都是使用到了注解解析器的技术。接下来我们来实现一个注解解析器。(本文代码有点多)

项目结构

首先我们需要把项目结构改造一下

# 项目结构省略了部分文件展示
├── annotation              # 注解等元数据定义
├── annotationProcessor     # 注解解析以及代码生成
├── app                     # 客户端使用入口
├── easybuslib              # 核心接口
├── local.properties
└── settings.gradle

app 同级的目录增加了 annotationannotationProcessoreasybuslib。其中创建 annotationannotationProcessor 这两个项目时一定要选择 java library。前者主要是用于定义注解和封装一些基础数据结构,后者是用于解析注解。注意 annotationProcessor 在项目使用时,并不会打包到 app 中,它只会在编译期间对注解进行解析处理。easybuslibandroid library

它们之间的关系为

# 符号 “->” 表示库依赖
# 符号 “=>” apt 依赖,并不会打包到 app 中
app -> easybuslib -> annotation
app => annotationProcessor
annotationProcessor -> annotation
annotation

annotation 是一个纯粹的 java 项目,主要定义了注解 EasySubscribeSubscriberMethodSubscription 这个是 EasyBus 会直接使用到的类,而在 meta 包中定义了注解解析器需要使用到的数据结构。这个包结构分工是很明确的。

# annotation 主要的项目结构
└── src/main/java
    └── com.gitlab.annotation
        ├── EasySubscribe.java
        ├── SubscriberMethod.java
        ├── Subscription.java
        └── meta
            ├── SubscriberInfo.java
            ├── SubscriberInfoIndex.java
            └── SubscriberMethodInfo.java

在这个库中实现自定义的注解

annotationProcessor
# annotationProcessor 主要的项目结构
└── src/main/java
    └── com.gitlab.annotationprocessor
        └── EasyBusAnnotationProcessor.java
        └── resources/META-INF.services
            └── javax.annotation.processing.Processor

这个只有一个 java 类和一个配置 Processor 的文件。解析注解生成 java 代码的逻辑就在 EasyBusAnnotationProcessor 里面。

easybuslib
# easybuslib 主要项目结构
└── src
    └── main/java
        └── com.gitlab.easybuslib
            ├── EasyBus.java
            └── Logger.java
            └── res

这里封装了 EasyBus 主要接口,其逻辑在前面已经解释过了。不过今天也会对它进行改造使它支持编译期间解析得到的订阅者的 onEvent 方法(不是必需以 onEvent 开头,本文为了表达方便而使用)。

项目结构改造完成之后,接下来我们自上而下对注解解析器进行解读和实现。

改造EasyBus

定义注解

定义注解在前面已经解读过,这里直接贴出代码

EasySubscribe.java

/**
 * 自定义注解
 * 指定该注解修饰方法
 * 由于我们使用编译期间处理注解,所以指定其生命周期为只保留在源码文件中
 */
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface EasySubscribe {
}
添加索引列表

修改 EasyBus 中的注册逻辑,添加由注解解析器生成的索引列表,并从索引列表中获取到订阅者被 @EasySubscribe 标记的方法。

SubscriberInfoIndex.java

/**
 * 订阅者的索引接口
 * 通过Class获取到该Class下定义的被标记的 @EasySubscribe 方法
 */
public interface SubscriberInfoIndex {
    SubscriberInfo getSubscriberInfo(Class<?> subscriberClass);
}

这个接口非常重要,我们使用注解解析器生成的类将继承于这个接口,这样我们在 EasyBus 中就依赖于该接口,而接口的实现交给注解解析器。

修改后的 EasyBus

public class EasyBus {

    //省略部分代码...
    /**
     * 编译期间生成订阅者索引,通过订阅者 Class 类获取到 @EasySubscribe 的方法
     */
    private List<SubscriberInfoIndex> subscriberInfoIndexList;

    //省略部分代码...

    /**
     * 添加订阅者索引
     *
     * @param subscriberInfoIndex
     */
    public void addIndex(SubscriberInfoIndex subscriberInfoIndex) {
        if (subscriberInfoIndexList == null) {
            subscriberInfoIndexList = new ArrayList<>();
        }
        subscriberInfoIndexList.add(subscriberInfoIndex);
    }

    public void register(Object subscriber) {
        Class<?> subscriberClass = subscriber.getClass();
        List<SubscriberMethod> subscriberMethods = new ArrayList<>();
        //使用反射获取 onEvent 方法
        if (subscriberInfoIndexList == null) {
            Method[] methods = subscriberClass.getDeclaredMethods();
            for (Method method : methods) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length != 1) {
                    continue;
                }
                // 这里可以修改成使用反射获取,这样就不需要求方法以 onEvent 开头
                if (method.getName().startsWith("onEvent")) {
                    subscriberMethods.add(new SubscriberMethod(method, parameterTypes[0]));
                }
            }
        } else {
            //注意这里!!!
            //使用注解解析器获取 onEvent 方法
            subscriberMethods = findSubscriberMethods(subscriberClass);
        }
        synchronized (this) {
            for (SubscriberMethod method : subscriberMethods) {
                subscribe(subscriber, method);
            }
        }
    }

    /**
     * 从索引中获取订阅者方法信息
     *
     * @param subscriberClass
     * @return
     */
    private List<SubscriberMethod> findSubscriberMethods(Class<?> subscriberClass) {
        List<SubscriberMethod> subscriberMethods = new ArrayList<>();
        for (SubscriberInfoIndex subscriberIndex : subscriberInfoIndexList) {
            SubscriberInfo subscriberInfo = subscriberIndex.getSubscriberInfo(subscriberClass);
            List<SubscriberMethod> methodList = Arrays.asList(subscriberInfo.getSubscriberMethods());
            subscriberMethods.addAll(methodList);
        }
        return subscriberMethods;
    }
    // 省略部分代码...
}

主要对 register 方法进行了改造,当 subscriberInfoIndexList 不为空时,就从索引列表中查询订阅者信息。findSubscriberMethods() 遍历索引列表并执行 subscriberIndex.getSubscriberInfo(subscriberClass) 方法得到订阅者的信息。那么 subscriberIndex 具体是怎么实现的呢?

打开 app/HomeActivity

看到以下代码

EasyBus.getInstance().addIndex(new MyEventBusIndex());

通过 addIndex() 方法将 MyEventBusIndex 实例添加到索引列表中。接下来我们看看其内部到底有何乾坤。

MyEventBusIndex.java这个类是由注解解析器生成的

/** This class is generated by EasyBus, do not edit. */
public class MyEventBusIndex implements SubscriberInfoIndex {
    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;

    static {
        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();

        putIndex(new SubscriberInfo(com.github.easybus.demo.HomeActivity.class, new SubscriberMethodInfo[] {
            new SubscriberMethodInfo("onUpdateMessage", com.github.easybus.demo.MessageEvent.class),
            new SubscriberMethodInfo("onEventNotify", com.github.easybus.demo.MessageEvent.class),
        }));

    }

    private static void putIndex(SubscriberInfo info) {
        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);
    }

    @Override
    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {
        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);
        if (info != null) {
            return info;
        } else {
            return null;
        }
    }
}

它代码逻辑很简单,首先定义一个静态 Map 变量 SUBSCRIBER_INDEX,它的 keyClass<?> 对象, valueSubscriberInfo 对象。然后 在一个静态的代码块中将订阅者的方法名称和参数类型封装成 SubscriberInfo 后添加到这个 Map 中。

SubscriberInfo.java

/**
 * 订阅者信息
 * 主要是从注解中解析出Class以及通知方法(即被@EasySubscribe标记的方法)
 */
public class SubscriberInfo {
    private Class subscriberClass;
    private SubscriberMethodInfo[] subscriberMethodInfos;

    public SubscriberInfo(Class subscriberClass, SubscriberMethodInfo[] subscriberMethods) {
        this.subscriberClass = subscriberClass;
        this.subscriberMethodInfos = subscriberMethods;
    }

    //省略代码...

    public synchronized SubscriberMethod[] getSubscriberMethods() {
        int length = subscriberMethodInfos.length;

        SubscriberMethod[] methods = new SubscriberMethod[length];
        for (int i = 0; i < length; i++) {
            SubscriberMethodInfo info = subscriberMethodInfos[i];
            SubscriberMethod method = createSubscribeMethod(info);
            if (method != null) {
                methods[i] = method;
            }
        }
        return methods;
    }

    private SubscriberMethod createSubscribeMethod(SubscriberMethodInfo info) {
        try {
            Method method = subscriberClass.getDeclaredMethod(info.getMethodName(), info.getEventType());
            return new SubscriberMethod(method, info.getEventType());
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        }
        return null;
    }
}

SubscriberMethodInfo.java

/**
 * 用于编译期间生成的订阅者信息
 */
public class SubscriberMethodInfo {
    private final String methodName;
    private final Class<?> eventType;

    public SubscriberMethodInfo(String methodName, Class<?> eventType) {
        this.methodName = methodName;
        this.eventType = eventType;
    }
    // 省略代码...
}

SubscriberInfoSubscriberMethodInfo 都是元数据类,主要是由生成的 MyEventBusIndex 类使用

如何生成代码呢?

注解解析器

我们重点看 annotationProcessor 这个项目

首先配置 build.gradle

// annotationProcessor 工程库必须使用 java 工程
// 不要使用 android lib 工程
// 本工程只会生成辅助代码,不会打包到 apk 中
apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.squareup:javapoet:1.11.1'
    implementation project(':annotation')
}

sourceCompatibility = "7"
targetCompatibility = "7"

添加 javapoet 依赖,这个框架帮助我们生成代码(注意只能生成新代码,而不能修改现有代码哦)

然后继承 AbstractProcessor

// 可以使用注解指定要解析的自定义注解以及Java版本号
// 也可以重写 AbstractProcessor 中的方法达到类似的目的
// @SupportedAnnotationTypes({"com.gitlab.annotation.EasySubscribe"})
// @SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EasyBusAnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        collectSubscribers(set, roundEnvironment, messager);
        return true;
    }

    // 省略代码...

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
//        annotations.add("com.gitlab.annotation.EasySubscribe");
        //指定要解析的注解
        annotations.add(EasySubscribe.class.getCanonicalName());
        return annotations;
    }
}

需要实现核心的几个方法

  • init 初始化方法
  • process 处理注解的核心方法
  • getSupportedSourceVersion 指定 Java 版本,一般使用 SourceVersion.latestSupported()
  • getSupportedAnnotationTypes 指定要解析的注解,有一个或多个注解,将其添加到 set中,并返回。

我们重点关注 process 方法。这里有两个参数,一个是 TypeElement 类型的 setRoundEnvironment 变量。其中 TypeElementElement 的子类。而 Element 是对包、类、接口、(构造)方法、属性、参数等对象的抽象,可以结合以下对应关系进行理解。

package com.example;            // PackageElement

public class Foo {                // TypeElement

    private int a;              // VariableElement
    private Foo other;          // VariableElement

    public Foo () {}            // ExecuteableElement

    public void setA (          // ExecuteableElement
                     int newA   // TypeElement
                     ) {}
}

RoundEnvironment 是一个接口,是对上下文信息的抽象。

我们回到 process() 方法,它在编译时会被执行,此时会将被注解标记的类、方法等信息传递过来

process() 方法会执行 collectSubscribers() 方法(此方法是从 EventBus 里中 copy 过来的)

private void collectSubscribers(Set<? extends TypeElement> annotations, RoundEnvironment env, Messager messager) {
        // 遍历要解析的注解
        for (TypeElement annotation : annotations) {
            messager.printMessage(Diagnostic.Kind.NOTE, "annotation:" + annotation.getSimpleName());
            // 获取被注解标记的对象
            Set<? extends Element> elements = env.getElementsAnnotatedWith(annotation);
            // Element 是接口,是对包括:包名、类、接口、方法、构造方法等的抽象
            for (Element element : elements) {
                // 自定义注解 EasySubscribe 是作用在方法上的
                // 所以检查一下是否是 ExecutableElement 对象
                // 它可以表示方法以及构造方法
                if (element instanceof ExecutableElement) {
                    ExecutableElement method = (ExecutableElement) element;
                    if (checkHasNoErrors(method, messager)) {
                        // 获取到这个被自定义注解的标记的方法所在类
                        TypeElement classElement = (TypeElement) method.getEnclosingElement();
                        List<ExecutableElement> list = methodsByClass.get(classElement);
                        if (list == null) {
                            list = new ArrayList<>();
                        }
                        list.add(method);
                        methodsByClass.put(classElement, list);
                    }
                } else {
                    messager.printMessage(Diagnostic.Kind.ERROR, "@EasySubscribe is only valid for methods", element);
                }
            }
        }

        if (!writeDone && !methodsByClass.isEmpty()) {
            createInfoIndexFile("com.github.easybus.MyEventBusIndex");
            writeDone = true;
        } else {
            messager.printMessage(Diagnostic.Kind.WARNING, "No @EasySubscribe annotations found");
        }
    }

Messager 对象可以用于输入打印信息。 annotations 集合是所有待解析的注解,如果你定义了两个注解,并在 getSupportedAnnotationTypes 中返回了,那么这里就是两个需要解析的注解。 遍历注解集合,并使用 RoundEnvironment 获取到被注解标记的 Element,由于 EasySubscribe 是作用在方法上,所以我们主要关注 ExecutableElement 就可以了。 然后再通过 ExecutableElement.getEnclosingElement() 方法获取方法所在的类对象 Class 信息。 最后将其保存在 key 为代表 ClassTypeElementvalue 为代表方法列表的 Map 对象 methodsByClass 中。 这样就将类信息 Class 与被 @EasySubscribe 标记的方法列表对应起来了。这样就为接下来的生成代码逻辑作好了铺垫。 有了 methodsByClass 接下来就是生成代码的逻辑了。

代码生成

代码生成的逻辑在 createInfoIndexFile() 方法中,它有个参数 index,用来指定生成文件的包和类名的。(在 EventBus 中这里是在 gradle 中配置的,本文为了展示核心流程省略了) 由于 process() 方法会被执行多次,所以这里使用一个变量 writeDone 来判断是否已经生成过代码了,避免重复执行。

private void createInfoIndexFile(String index) {
        BufferedWriter writer = null;
        try {
            JavaFileObject sourceFile = filer.createSourceFile(index);
            int period = index.lastIndexOf('.');
            String myPackage = period > 0 ? index.substring(0, period) : null;
            String clazz = index.substring(period + 1);
            writer = new BufferedWriter(sourceFile.openWriter());
            if (myPackage != null) {
                writer.write("package " + myPackage + ";\n\n");
            }
            writer.write("import com.gitlab.annotation.meta.SubscriberInfoIndex;\n");
            writer.write("import com.gitlab.annotation.meta.SubscriberInfo;\n");
            writer.write("import com.gitlab.annotation.meta.SubscriberMethodInfo;\n");
            writer.write("import java.util.HashMap;\n");
            writer.write("import java.util.Map;\n\n");
            writer.write("/** This class is generated by EasyBus, do not edit. */\n");
            writer.write("public class " + clazz + " implements SubscriberInfoIndex {\n");
            writer.write("    private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;\n\n");
            writer.write("    static {\n");
            writer.write("        SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();\n\n");
            writeIndexLines(writer, myPackage);
            writer.write("    }\n\n");
            writer.write("    private static void putIndex(SubscriberInfo info) {\n");
            writer.write("        SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);\n");
            writer.write("    }\n\n");
            writer.write("    @Override\n");
            writer.write("    public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {\n");
            writer.write("        SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);\n");
            writer.write("        if (info != null) {\n");
            writer.write("            return info;\n");
            writer.write("        } else {\n");
            writer.write("            return null;\n");
            writer.write("        }\n");
            writer.write("    }\n");
            writer.write("}\n");
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException("Could not write source for " + index, e);
        } finally {
            if (writer != null) {
                try {
                    writer.close();
                } catch (IOException e) {
                    //Silent
                    e.printStackTrace();
                }
            }
        }
    }

如果你还对前面的 MyEventBusIndex.java 的内容还有印象的话,这里的逻辑还是比较好理解的,主要是使用 javapoet 中的接口生成代码。具体就不再赘述了,阅读代码还是比较清晰的,接下来看看如何调试。

如何调试

由于代码是在编译期间执行的,如果你是刚开始接触注解解析器的编码,不能调试将是非常痛苦的过程。

要调试注解解析器需要做以下配置

1、首先在项目的根目录下 gradle.properties 添加以下配置

org.gradle.jvmargs=-Xmx1536m -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005

2、然后点击 EditConfigurations 配置 remote 填写名称,例如 processorDebug 后保存。

3、选择 processorDebug

4、添加断点后 RebuildProject

现在就可以对注解解析器进行调试了

总结

注解解析器的实现逻辑其实不是很复杂,主要有以下几步:

  • 定义注解
  • 继承 AbstractProcessor 解析注解
  • 使用 javapoet 生成代码
  • 调试

面对一个新技术首先要掌握它的使用方法,然后了解其内部实现原理,最后自己动手实践。这样一个流程下来基本上对一个技术的理解是比较深刻的了。注解解析器作为很多基础组件实现的通用技术,掌握它对实现基础框架以及理解很多开源框架是很有帮助的。

本文的源码

  • https://github.com/hylinux1024/EasyBus

本文分享自微信公众号 - 终身开发者(AngryCode),作者:hylinux1024

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-12-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 100行代码拆解EventBus核心逻辑(二)

    前面我们参考 EventBus 的实现逻辑模仿了一个最简单的 EasyBus。这个其实也是观察者模式的实现,所以整体逻辑理解起来应该是不难的。在 EasyBus...

    阳仔
  • 源码分析Retrofit请求流程

    Retrofit 是 square 公司的另一款广泛流行的网络请求框架。前面的一篇文章《源码分析OKHttp执行过程》已经对 OkHttp 网络请求框架有一个大...

    阳仔
  • 从数据结构的角度上看区块链到底是什么

    自从最近央视提出要发展自主区块链技术的号召以来,区块链领域又骚动了起来。程序猿是学习能力很强的群体,了解新技术是日常工作生活的一部分。作为一个从事区块链相关产品...

    阳仔
  • 反射基础之Class

    Java中每个类型要么是引用类型,要么是原生类型。类,枚举,数组(他们都继承于java.lang.Object)和接口都是引用类型。例如:java.lang.S...

    代码拾遗
  • 一个闪回区报警的数据恢复(r11笔记第62天)

    今天在火车上接到一个电话说,数据库有个报警,让我看看是怎么回事。 看着报警信息一直重复出现,看来是有些问题了。 这是一个统计库,出现了DG相关的...

    jeanron100
  • HTML5之placeholder属性以及如何更改placeholder属性中文字颜色

    在HTML5中为input标签添加了一个新的属性为placeholder,此placeholder属性可以在input没有任何输入或value的属性为空的情况下...

    周俊辉
  • Apple Watch被控技术专利侵权,苹果为此曾挖走Masimo两名高管

    近日,据外媒报道,苹果被一家医疗技术公司告上了法庭,诉讼称,Apple Watch的健康监测功能侵犯了Masimo的专利,并且苹果还以工作关系为幌子从Masim...

    镁客网
  • 撩一下一些必要的js工具函数

    不管是什么项目,总有一些基本的功能函数默默的躺在你的工具库中,为你遮挡bug,提升性能,一起来复习下!

    IMWeb前端团队
  • 某音去水印视频下载小程序,有点东西

    短视频时代五花八门的内容,一不留神就被吸引过去,一划半天就过去了,好像中毒一样一样的,希望大家要控制住自已鸭。

    IT小侠公社
  • 撩一下一些必要的js工具函数

    不管是什么项目,总有一些基本的功能函数默默的躺在你的工具库中,为你遮挡bug,提升性能,一起来复习下! debounce 当监听一些scroll,resize事...

    IMWeb前端团队

扫码关注云+社区

领取腾讯云代金券