首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >100行代码拆解EventBus核心逻辑(三)

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

作者头像
阳仔
发布于 2019-12-17 18:16:09
发布于 2019-12-17 18:16:09
8750
举报
文章被收录于专栏:终身开发者终身开发者

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

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

项目结构

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

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

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

它们之间的关系为

代码语言:javascript
复制
# 符号 “->” 表示库依赖
# 符号 “=>” apt 依赖,并不会打包到 app 中
app -> easybuslib -> annotation
app => annotationProcessor
annotationProcessor -> annotation
annotation

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

代码语言:javascript
复制
# annotation 主要的项目结构
└── src/main/java
    └── com.gitlab.annotation
        ├── EasySubscribe.java
        ├── SubscriberMethod.java
        ├── Subscription.java
        └── meta
            ├── SubscriberInfo.java
            ├── SubscriberInfoIndex.java
            └── SubscriberMethodInfo.java

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

annotationProcessor
代码语言:javascript
复制
# annotationProcessor 主要的项目结构
└── src/main/java
    └── com.gitlab.annotationprocessor
        └── EasyBusAnnotationProcessor.java
        └── resources/META-INF.services
            └── javax.annotation.processing.Processor

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

easybuslib
代码语言:javascript
复制
# easybuslib 主要项目结构
└── src
    └── main/java
        └── com.gitlab.easybuslib
            ├── EasyBus.java
            └── Logger.java
            └── res

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

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

改造EasyBus
定义注解

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

EasySubscribe.java

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

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

SubscriberInfoIndex.java

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

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

修改后的 EasyBus

代码语言:javascript
复制
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

看到以下代码

代码语言:javascript
复制
EasyBus.getInstance().addIndex(new MyEventBusIndex());

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

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

代码语言:javascript
复制
/** 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,它的 key 是 Class<?> 对象, value 是 SubscriberInfo 对象。然后 在一个静态的代码块中将订阅者的方法名称和参数类型封装成 SubscriberInfo 后添加到这个 Map 中。

SubscriberInfo.java

代码语言:javascript
复制
/**
 * 订阅者信息
 * 主要是从注解中解析出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

代码语言:javascript
复制
/**
 * 用于编译期间生成的订阅者信息
 */
public class SubscriberMethodInfo {
    private final String methodName;
    private final Class<?> eventType;

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

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

如何生成代码呢?

注解解析器

我们重点看 annotationProcessor 这个项目

首先配置 build.gradle

代码语言:javascript
复制
// 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

代码语言:javascript
复制
// 可以使用注解指定要解析的自定义注解以及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 类型的 set 和 RoundEnvironment 变量。其中 TypeElement 是 Element 的子类。而 Element 是对包、类、接口、(构造)方法、属性、参数等对象的抽象,可以结合以下对应关系进行理解。

代码语言:javascript
复制
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 过来的)

代码语言:javascript
复制
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 为代表 Class 的 TypeElement, value 为代表方法列表的 Map 对象 methodsByClass 中。 这样就将类信息 Class 与被 @EasySubscribe 标记的方法列表对应起来了。这样就为接下来的生成代码逻辑作好了铺垫。 有了 methodsByClass 接下来就是生成代码的逻辑了。

代码生成

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

代码语言:javascript
复制
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 添加以下配置

代码语言:javascript
复制
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
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-14,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 项目结构
    • annotation
    • annotationProcessor
    • easybuslib
  • 改造EasyBus
    • 定义注解
    • 添加索引列表
  • 注解解析器
    • 代码生成
  • 如何调试
  • 总结
  • 本文的源码
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档