转载

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

关于我

一个有思想的程序猿,终身学习实践者,目前在一个创业团队任team lead,技术栈涉及Android、Python、Java和Go,这个也是我们团队的主要技术栈。

Github: 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、然后点击 Edit Configurations 配置 remote 填写名称,例如 processorDebug 后保存。

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

3、选择 processorDebug

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

4、添加断点后 Rebuild Project

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

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

原文  https://juejin.im/post/5df4e5696fb9a01648716be7
正文到此结束
Loading...