通过编译时注解生成代码实现自己的ButterKnife

背景概述

注解的处理除了可以在运行时通过反射机制处理外,还可以在编译期进行处理。

Java5中提供了apt工具来进行编译期的注解处理。apt是命令行工具,与之配套的是一套描述“程序在编译时刻的静态结构”的API:Mirror API(com.sun.mirror.*)。通过Mirror API可以获取到被注解的Java类型元素的信息,从而提供自定义的处理逻辑。具体的处理工具交给apt来处理。编写注解处理器的核心是两个类:注解处理器(com.sun.mirror.apt.AnnotationProcessor)、注解处理器工厂(com.sun.mirror.apt.AnnotationProcessorFactory)。apt工具在完成注解处理后,会自动调用javac来编译处理完成后的源代码。然而,apt工具是oracle提供的私有实现(在JDK开发包的类库中是不存在的)。

在JDK6中,将注解处理器这一功能进行了规范化,形成了java.annotation.processing的API包,Mirror API则进行封装,形成javax.lang.model包。

当前注解在Android的开发中的使用越来越普遍,例如EventBus、ButterKnife、Dagger2等。这些都通过注解根据反射机制动态编译生成代码的方式来解决在运行时使用发射机制带来的效率问题,我们今天利用Android studio的来实现一下自己的ButterKnife UI注解框架。

实战项目

首先看下项目结构:
通过编译时注解生成代码实现自己的ButterKnife_第1张图片
整个项目包含四个modules,其中app主要是用于测试的Android模块,其他三个分别是:

annotations:主要用于申明app需要用到的注解

api:用于申明注解框架的api

complier:用于在编译期间通过反射机制自动生成代码

annotations模块

定义注解@BindView

这里我声明了一个BindView注解,声明周期为CLASS,作用域为成员变量

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

回顾一下RetentionPolicy的三个值含义:

  • SOURCE. 注解保留在源代码中,但是编译的时候会被编译器所丢弃。比如@Override, @SuppressWarnings

  • CLASS. 这是默认的policy。注解会被保留在class文件中,但是在运行时期间就不会识别这个注解。

  • RUNTIME. 注解会被保留在class文件中,同时运行时期间也会被识别。所以可以使用反射机制获取注解信息。比如@Deprecated

api模块

api声明框架中使用的api,比如绑定解绑,查找View控件等,这是一个android library。

向用户提供绑定方法

public class MyViewBinder {
    private static final ActivityViewFinder activityFinder = new ActivityViewFinder();//默认声明一个Activity View查找器
    private static final Map binderMap = new LinkedHashMap<>();//管理保持ViewBinder的Map集合

    /**
     * Activity注解绑定 ActivityViewFinder
     *
     * @param activity
     */
    public static void bind(Activity activity) {
        bind(activity, activity, activityFinder);
    }


    /**
     * '注解绑定
     *
     * @param host   表示注解 View 变量所在的类,也就是注解类
     * @param object 表示查找 View 的地方,Activity & View 自身就可以查找,Fragment 需要在自己的 itemView 中查找
     * @param finder ui绑定提供者接口
     */
    private static void bind(Object host, Object object, ViewFinder finder) {
        String className = host.getClass().getName();
        try {
            ViewBinder binder = binderMap.get(className);
            if (binder == null) {
                Class aClass = Class.forName(className + "$$ViewBinder");
                binder = (ViewBinder) aClass.newInstance();
                binderMap.put(className, binder);
            }
            //调用xxx$$ViewBinder类中的bindView方法,对View进行相应的赋值绑定
            if (binder != null) {
                binder.bindView(host, object, finder);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    /**
     * 解除注解绑定 ActivityViewFinder
     *
     * @param host
     */
    public static void unBind(Object host) {
        String className = host.getClass().getName();
        ViewBinder binder = binderMap.get(className);
        if (binder != null) {
            binder.unBindView(host);
        }
        binderMap.remove(className);
    }
}

下面是内部需要使用的一些接口和类

/**
 * Created by ason on 2018/1/19.
 * UI绑定解绑接口
 */
public interface ViewBinder {
    void bindView(T host, Object object, ViewFinder finder);
    void unBindView(T host);
}

我们后面生成的类会继承这个接口,并实现其中的两个抽象方法。

public interface ViewFinder {
    View findView(Object object, int d);
}
public class ActivityViewFinder implements ViewFinder {
    @Override
    public View findView(Object object, int id) {
        if (!(object instanceof Activity))
        {
            throw new ClassCastException("不能转换为Activity");
        }
        return ((Activity)object).findViewById(id);
    }
}

complier模块

complier是一个java library,作用是根据注解在编译期间自动生成java代码。

编写注解处理器类,处理器的核心接口为:javax.annotation.processing.Processor,还提供了一个此接口的实现类:javax.annotation.processing.AbstractProcessor。我们需要继承AbstractProcessor自来编写一个处理器类来,用于实现自己的处理逻辑,注解处理器核心处理方法是process()。


@AutoService(Processor.class)
public class ViewBinderProcessor extends AbstractProcessor {
    private Filer mFiler; //文件相关的辅助类
    private Elements mElementUtils; //元素相关的辅助类
    private Messager mMessager; //日志相关的辅助类
    private Map mAnnotatedClassMap;

    /**
     * @param processingEnv ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer
     */
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mFiler = processingEnv.getFiler();
        mElementUtils = processingEnv.getElementUtils();
        mMessager = processingEnv.getMessager();
        mAnnotatedClassMap = new TreeMap<>();
    }


    /**
     * process() 方法会被调用多次, 直到没有新的类产生为止.因为新生成的文件中也可能包含目标注解,它们会继续被 Processor 处理.
     * process()方法的返回值,若返回false,表示本轮注解未声明并且可能要求后续其它的Processor处理它们;
     * 若返回true,则代表这些注解已经声明并且不要求后续Processor来处理它们。
     */
    @Override
    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        mAnnotatedClassMap.clear();
        try {
            processBindView(roundEnv);
        } catch (IllegalArgumentException e) {
            e.printStackTrace();
            error(e.getMessage());
        }

        for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
            try {
                annotatedClass.generateFile().writeTo(mFiler);
            } catch (IOException e) {
                error("Generate file failed, reason: %s", e.getMessage());
            }
        }
        return true;
    }

    private void processBindView(RoundEnvironment roundEnv) throws IllegalArgumentException {

        //roundEnv.getElementsAnnotatedWith(BindView.class)拿到所有被@BindView注解的成员变量
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField bindViewField = new BindViewField(element);
            annotatedClass.addField(bindViewField);
        }
    }

    /**
     * 通过Eelment获取到注解类的全名,使用这个全名从mAnnotatedClassMap找到与其相关的AnnotatedClass类,没有则创建一个并放入mAnnotatedClassMap
     */
    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        String fullName = typeElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(typeElement, mElementUtils);
            mAnnotatedClassMap.put(fullName, annotatedClass);
        }
        return annotatedClass;
    }

    private void error(String msg, Object... args) {
        mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
    }


    /**
     * 指定使用的Java版本,默认为版本6,通常这里返回SourceVersion.latestSupported()
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }


    /**
     * 指定注解处理器可解析的注解类型,它也可能是 “name.*” 形式的名称,表示所有以 “name.” 开头的规范名称的注释类型集合。最后,”*” 自身表示所有注释类型的集合,包括空集。注意,Processor 不应声明 “*”,除非它实际处理了所有文件;声明不必要的注释可能导致在某些环境中的性能下降。
     * @return 它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。
     */
    @Override
    public Set getSupportedAnnotationTypes() {
        Set types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }
}

@AutoService:用来帮助我们自动生成META-INF,目录结构如下:
这里写图片描述
为了便于编译器注释工具执行能够运行ViewBinderProcessor,必须要用一个服务文件来注册它。文件里面就是写着Processor的位置。

auto-service需要在配置文件中配置,build.gradle文件内容如下:

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    //auto-service:用来帮助我们自动生成META-INF
    compile 'com.google.auto.service:auto-service:1.0-rc2'
    compile 'com.squareup:javapoet:1.8.0'
    implementation project(":annotations")
}

sourceCompatibility = "1.8"
targetCompatibility = "1.8"

其中 javapoet 提供了各种 API 让你去生成 Java 代码文件。

然后处理器使用到的工具类Filer、Elements和Messager。

  • Filter 注解处理器可用此创建新文件(源文件、类文件、辅助资源文件)。由此方法创建的源文件和类文件将由管理它们的工具(javac)。
  • Element 程序的元素, 例如包, 类、方法、字段. 每个 Element 代表一个静态的,语言级别的构件。
  • Messager
    注解处理器用此来报告错误消息、警告和其他通知的方式。可以为它的方法传递元素、注解、注解值,以提供消息的位置提示,不过,这类位置提示可能是不可用的,或者只是一个大概的提示。打印错误种类的日志将会产生一个错误。

AnnotedClass用于处理生成对应类的信息。

public class AnnotatedClass {
    private static class TypeUtil {
        static final ClassName BINDER = ClassName.get("com.dkw.api", "ViewBinder");
        static final ClassName PROVIDER = ClassName.get("com.dkw.api", "ViewFinder");
    }

    private TypeElement mTypeElement;
    private ArrayList mFields;
    private Elements mElements;

    AnnotatedClass(TypeElement typeElement, Elements elements) {
        mTypeElement = typeElement;
        mElements = elements;
        mFields = new ArrayList<>();
    }

    void addField(BindViewField field) {
        mFields.add(field);
    }

    /**
     * 通过javapoet生成
     */
    JavaFile generateFile() {
        //generateMethod
        MethodSpec.Builder bindViewMethod = MethodSpec.methodBuilder("bindView")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(mTypeElement.asType()), "host")
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtil.PROVIDER, "finder");

        for (BindViewField field : mFields) {
            // find views
            bindViewMethod.addStatement("host.$N = ($T)(finder.findView(source, $L))", field.getFieldName(), ClassName.get(field.getFieldType()), field.getResId());
        }

        MethodSpec.Builder unBindViewMethod = MethodSpec.methodBuilder("unBindView")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(TypeName.get(mTypeElement.asType()), "host")
                .addAnnotation(Override.class);
        for (BindViewField field : mFields) {
            unBindViewMethod.addStatement("host.$N = null", field.getFieldName());
        }

        //generaClass
        TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$$ViewBinder")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtil.BINDER, TypeName.get(mTypeElement.asType())))
                .addMethod(bindViewMethod.build())
                .addMethod(unBindViewMethod.build())
                .build();

        String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();

        return JavaFile.builder(packageName, injectClass).build();
    }
}

BindViewField对象用于被注解的成员变量

public class BindViewField {
    private VariableElement mVariableElement;
    private int mResId;

    BindViewField(Element element) throws IllegalArgumentException {
        if (element.getKind() != ElementKind.FIELD) {
            throw new IllegalArgumentException(String.format("Only fields can be annotated with @%s",
                    BindView.class.getSimpleName()));
        }
        mVariableElement = (VariableElement) element;
        BindView bindView = mVariableElement.getAnnotation(BindView.class);
        mResId = bindView.value();
        if (mResId < 0) {
            throw new IllegalArgumentException(
                    String.format("value() in %s for field %s is not valid !", BindView.class.getSimpleName(),
                            mVariableElement.getSimpleName()));
        }
    }

    /**
     * 获取变量名称
     *
     * @return
     */
    Name getFieldName() {
        return mVariableElement.getSimpleName();
    }

    /**
     * 获取变量id
     *
     * @return
     */
    int getResId() {
        return mResId;
    }

    /**
     * 获取变量类型
     *
     * @return
     */
    TypeMirror getFieldType() {
        return mVariableElement.asType();
    }
}

app测试模块

在app模块的build.gradle文件中添加如下依赖:

    compile project(':api')
    //使用注解
    implementation project(':annotations')
    //对注解的处理
    annotationProcessor project(':compiler')

在Activity中使用

public class MainActivity extends Activity {

    @BindView(R.id.hello)
    TextView hello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyViewBinder.bind(this);
        hello.setText("butterknife");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        MyViewBinder.unBind(this);
    }
}

对应生成的类:

public class MainActivity$$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bindView(MainActivity host, Object source, ViewFinder finder) {
    host.hello = (TextView)(finder.findView(source, 2131165237));
  }

  @Override
  public void unBindView(MainActivity host) {
    host.hello = null;
  }
}

最后说明一下整个执行路程:

  1. 首先在需要使用注解的地方(MainActivity的成员变量)加上注解,在onCreate方法中调用bind方法。

  2. 文件编译时,找到全部包含注解的类,根据类的信息生成相应的文件(MainActivity$$ViewBinder),这个过程在注解处理器process方法是需要被多次处理的,直到没有新的类生成。

  3. 在上一步中已经将需要绑定的view的id生成到了相应方法当中,在Android系统执行onCreate方法时,调用我们的bind方法将我们的view都实例化上。

你可能感兴趣的:(android)