ButterKnife核心技术揭秘 - Spring-Xu/AnnotationProcessorDemo GitHub Wiki

ButterKnife核心技术揭秘

ButterKnife是来自Android届大牛JakeWharton的项目,这里我就不再赘述了,想必大家都有了解过,这里我主要给大家介绍这个项目的核心技术点,以及基于这个技术点,我给大家展示一下类似于ButterKnife的Demo。废话不多说项目地址

项目原理:

是一个基于注解的项目,但是他与普通的注解方式不一样,普通注解:代码执行的过程中解析相关注解,按照注解的意义做相应的处理;该项目的方式是采用了:annotation.processing.Processor技术,在编译阶段apt介入,将注解部分生成相应代码,执行的时候是执行相关代码,不需要再进行注解解析。

优势:

本质上讲和手动编写那些代码没有什么本质区别,但是这样的意义在于代码是自动生成的!我们也不需要关心这部分代码,这部分代码也不需要我们去维护,降低了我们日常的一些代码,维护的代码降低,不再去做findViewById或者setOnClickListener这样的重复工作;另外,该项目采用的annotation.processing.Processor技术很大程度上避免了使用注解的时候需要在运行时解析注解代理的性能消耗。所以学习该项目应该更关注annotation.processing.Processor技术,它是一个很好的技术点。

创建一个类似于ButterKnife的项目

有需要的看下,可滤过直接看技术讲解,这里注意是个人在实践的时候发现了一些坑,特意在这里Mark一下。
  • 创建一个普通的Android项目AnnotationProcessorDemo
  • 创建一个Module类型是Android Library:AnnotationBind
  • 创建一个专门annotation的Java Library:Annotations
  • 创建一个核心的预编译处理Module,特别要注意这个Module是Java Library:Complie

注意点:

  • app的Module里边Gradle配置apt plugin gradle apply plugin: 'com.neenbedankt.android-apt' dependencies { compile project(':annotations') compile project(':annotationbind') apt project(':compiler') }
  • project的build.gradle配置classpath中添加apt gradle dependencies { classpath 'com.android.tools.build:gradle:2.1.2' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }
  • Complie这个module需要配置 gradle apply plugin: 'java' apply plugin: 'checkstyle' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile project(':annotations') // Square compile 'com.squareup:javapoet:1.4.0' // Google project to get M compile 'com.google.auto.service:auto-service:1.0-rc2' }

Annotation Processor 技术

这是一个JavaC的工具,也就是Java编译源码到字节码的一个预编译工具,会在代码编译的时候调用到。他有一个抽象类,只需实现抽象类,就可以在预编译的时候被编译器调用,就可以在预编译的时候完成一下你想完成工作。比如代码注入!!

public abstract class BaseProcessor extends AbstractProcessor {
    protected Messager messager;
    protected Elements elements;
    protected Filer filer;

	 @Override
    public boolean process(Set<? extends TypeElement> annotations, 			RoundEnvironment roundEnv) {
    }
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> supports = new LinkedHashSet<>();
        supports.add(BindString.class.getCanonicalName());
        supports.add(BindInt.class.getCanonicalName());

        return supports;
    }

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

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        messager = processingEnv.getMessager();
        elements = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
    }

    protected void printLog(Diagnostic.Kind kind, String message) {
        messager.printMessage(kind, message);
    }

    protected void printLog(Element element, Diagnostic.Kind kind, String message) 	 {
        messager.printMessage(kind, message, element);
    }

    protected void printLog(Element element, String message, Object... args) {
        if (args.length > 0) {
            message = String.format(message, args);
        }
        messager.printMessage(ERROR, message, element);
    }
}
  • Set getSupportedAnnotationTypes():制定需要预编译器处理的注解,这里返回的是String的集合,必须是注解类型的合法全称。这里可以理解为过滤器中的过滤条件,制定后,编译器会根据制定的过滤类型扫描代码。
  • SourceVersion getSupportedSourceVersion():Jave的版本控制,建议使用SourceVersion.latestSupported()。
  • void init(ProcessingEnvironment processingEnv):初始化处理入口,是编译器调用的方法,这里提供了ProcessingEnvironment引用,这个可是个好东西,我是用再做一个基类的形式封装基于ProcessingEnvironment的操作,我们需要通过它获得Messager(log打印的类),Elements(注解扫描处理),以及Filer(代码注入类)。
  • boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):这是编译器扫描以后的结果处理入口,也是由编译器调用的,这里能获取到annotations扫描到的注解类型列表,roundEnv提供了查询扫描到的使用注解的类的信息。

注册你的注解处理器

Android开发者会在这时候疑惑,我实现了AbstractProcessor就可以了吗?比如增加一个Activity,我继承了Activity基类是不够的,还得在AndroidManifest.xml文件中注册Activity,程序才能调用这个Activity或者说找到这个程序的Activity入口。 这里Google提供了一个解决方案,只需使用 com.google.auto.service:auto-service这个项目,在继承AbstractProcessor类添加注解@AutoService(Processor.class),这个解决方案就会自动帮我们完成这个注解处理器的注册。

注解的扫描结果的处理

处理器工作的原理:1.扫描代码以后调用process方法返回给处理入口;2.处理入口需要根据注册的注解查询使用了该注解的类,以及注解中获取配置的值;3.生成代码注入。

AutoService(Processor.class)
public class BindingProcessor extends BaseProcessor {
    private static final String BIND_CLASS_SUFFIX = "$$AnnotationBinder";

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        printLog(Diagnostic.Kind.OTHER, "BindingProcessor process.... ");

			// 处理注解类
        Map<TypeElement, BindingClass> bindingClassMap = findAndParseTargets(roundEnv);
        
        for (Map.Entry<TypeElement, BindingClass> entry : bindingClassMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingClass bindingClass = entry.getValue();

            try {
                printLog(Diagnostic.Kind.OTHER, "BindingProcessor code:" + bindingClass.brewJava().toString());
                //处理结果注入代码
                bindingClass.brewJava().writeTo(filer);
            } catch (IOException e) {
                printLog(typeElement, "Unable to write view binder for type %s: %s", typeElement,
                        e.getMessage());
            }
        }
        return true;
    }

    ```

1.核心的扫描处理方法使用roundEnv查询然后逐个处理:
```Java
private Map<TypeElement, BindingClass> findAndParseTargets(RoundEnvironment roundEnv) {
        ...
        //其中:根据注解类获得使用了这个注解的类列表。
        Set<? extends Element> bindStringSet = roundEnv.getElementsAnnotatedWith(BindString.class);
        ...
    }

2.然后处理这个注解我们要做的操作,记录使用了BindString.class这个注解的一个类,解析注解的配置或者值,保存注解信息,和所在的类的信息:

private void parseResourceString(Element element, Map<TypeElement, BindingClass> targetClassMap,Set<String> erasedTargetNames) {
			...
        // 使用了这个注解的类的信息
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
        ...
        // 获得注解里边的值或者说配置
        String value = element.getAnnotation(BindString.class).value();
        String name = element.getSimpleName().toString();
        ...
    }

3.代码注入:

for (Map.Entry<TypeElement, BindingClass> entry : bindingClassMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingClass bindingClass = entry.getValue();

            try {
                printLog(Diagnostic.Kind.OTHER, "BindingProcessor code:" + bindingClass.brewJava().toString());
                //处理结果注入代码
                bindingClass.brewJava().writeTo(filer);
            } catch (IOException e) {
                printLog(typeElement, "Unable to write view binder for type %s: %s", typeElement,
                        e.getMessage());
            }
        }

Java代码的生成

代码的生成可以是刀耕火种式的,字符串拼接,也可以采用square公司提供的工具com.squareup:javapoet,里边封装了Java生成工具,很方便,可以灵活配置。这里我就介绍一下类的生成和方法的生成:

//生成完整类
JavaFile brewJava() {
        TypeSpec.Builder result = TypeSpec.classBuilder(className)
                .addModifiers(PUBLIC)
                .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

        if (parentViewBinder != null) {
            result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentViewBinder),
                    TypeVariableName.get("T")));
        } else {
            result.addSuperinterface(ParameterizedTypeName.get(BINDER, TypeVariableName.get("T")));
        }

        result.addMethod(createBindMethod());

        return JavaFile.builder(classPackage, result.build())
                .addFileComment("This is a class, create by annotation processor!")
                .build();
    }

	//生成方法
    private MethodSpec createBindMethod() {
        MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
                .addAnnotation(Override.class)
                .addModifiers(PUBLIC)
                .addParameter(TypeVariableName.get("T"), "target", FINAL)
                .addParameter(Object.class, "source");

        // Emit a call to the superclass binder, if any.
        if (parentViewBinder != null) {
            result.addStatement("super.bind(target, source)");
        }

        for (StringBinding binding : stringBindings) {
            result.addStatement("target.$L = \"$L\"", binding.getName(), binding.getValue());
        }

        for (IntBinding binding : intBindings) {
            result.addStatement("target.$L = $L", binding.getName(), binding.getValue());
        }

        return result.build();
    }

最后的结果是JavaFile,然后用上边提到的Filer工具去完成代码注入。

出错处理

其实在基类里边已经给大家提到了Messager,在log打印的时候必须输入级别,如果是Error的话预编译器会报错,预编译会中断,编译失败!

  messager.printMessage(ERROR, message, element);

也会提供别的级别的日志:

enum Kind {
        /**
         * Problem which prevents the tool's normal completion.
         */
        ERROR,
        /**
         * Problem which does not usually prevent the tool from
         * completing normally.
         */
        WARNING,
        /**
         * Problem similar to a warning, but is mandated by the tool's
         * specification.  For example, the Java&trade; Language
         * Specification mandates warnings on certain
         * unchecked operations and the use of deprecated methods.
         */
        MANDATORY_WARNING,
        /**
         * Informative message from the tool.
         */
        NOTE,
        /**
         * Diagnostic which does not fit within the other kinds.
         */
        OTHER,
    }
⚠️ **GitHub.com Fallback** ⚠️