AS 注解处理器 APT ButterKnife MD

Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱
MyAndroidBlogs baiqiantao baiqiantao bqt20094 [email protected]

AS 注解处理器 APT ButterKnife MD


目录

实现一个轻量的 ButterKnife

参考

APT(Annotation Processing Tool) 即注解处理器,是一种注解处理工具,用来在编译期扫描和处理注解,通过注解来生成 Java 文件。即以注解作为桥梁,通过预先规定好的代码生成规则来自动生成 Java 文件。此类注解框架的代表有 ButterKnife、Dragger2、EventBus

Java API 已经提供了扫描源码并解析注解的框架,开发者可以通过继承 AbstractProcessor 类来实现自己的注解解析逻辑。APT 的原理就是在注解了某些代码元素(如字段、函数、类等)后,在编译时编译器会检查 AbstractProcessor 的子类,并且自动调用其 process() 方法,然后将添加了指定注解的所有代码元素作为参数传递给该方法,开发者再根据注解元素在编译期输出对应的 Java 代码

这里以 ButterKnife 为实现目标,在讲解 Android APT 的内容的同时,逐步实现一个轻量的控件绑定框架,即通过注解来自动生成如下所示的 findViewById() 代码

package hello.leavesc.apt;

public class MainActivityViewBinding {
    public static void bind(MainActivity _mainActivity) {
        _mainActivity.tvName = (android.widget.TextView) (_mainActivity.findViewById(2131165333));
        _mainActivity.btnSend = (android.widget.Button) (_mainActivity.findViewById(2131165219));
        _mainActivity.etName = (android.widget.EditText) (_mainActivity.findViewById(2131165246));
    }
}

控件绑定的方式如下所示

@BindView(R.id.tv_name) TextView tvName;
@BindView(R.id.btn_send) Button btnSend;
@BindView(R.id.et_name) EditText etName;

建立 Module

首先在工程中新建一个 Java Library,命名为 apt_processor,用于存放 AbstractProcessor 的实现类。再新建一个 Java Library,命名为 apt_annotation ,用于存放各类注解

当中,apt_processor 需要导入如下依赖

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.google.auto.service:auto-service:1.0-rc2' //Google 开源的注解注册处理器
    implementation 'com.squareup:javapoet:1.10.0' //square 开源的 Java 代码生成框架
    implementation project(':apt_annotation') //需要依赖定义的注解
}

当中,JavaPoet 是 square 开源的 Java 代码生成框架,可以很方便地通过其提供的 API 来生成指定格式(修饰符、返回值、参数、函数体等)的代码。
auto-service 是由 Google 开源的注解注册处理器

实际上,上面两个依赖库并不是必须的,可以通过硬编码代码生成规则来替代,但还是建议使用这两个库,因为这样代码的可读性会更高,且能提高开发效率

app Module 需要依赖这两个 Java Library

implementation project(':apt_annotation') //需要依赖定义的注解
annotationProcessor project(':apt_processor') //使用apt

这样子,我们需要的所有基础依赖关系就搭建好了

编写代码生成规则

首先观察自动生成的代码,可以归纳出几点需要实现的地方:

  • 1、文件和源 Activity 处在同个包名下
  • 2、类名以 Activity名 + ViewBinding 组成
  • 3、bind() 方法通过传入 Activity 对象来获取其声明的控件对象来对其进行实例化,这也是 ButterKnife 要求需要绑定的控件变量不能声明为 private 的原因

注解声明

BindView 注解的声明如下所示,放在 apt_annotation 中,注解值 value 用于声明 viewId

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

AbstractProcessor

apt_processor Module 中创建 BindViewProcessor 类并继承 AbstractProcessor 抽象类,该抽象类含有一个抽象方法 process() 以及一个非抽象方法 getSupportedAnnotationTypes() 需要由我们来实现

getSupportedAnnotationTypes() 方法用于指定该 AbstractProcessor 的目标注解对象
process() 方法则用于处理包含指定注解对象的代码元素

要自动生成 findViewById() 方法,则需要获取到控件变量的引用以及对应的 viewid,所以需要先遍历出每个 Activity 包含的所有注解对象

当中,Element 用于代表程序的一个元素,这个元素可以是:包、类、接口、变量、方法等多种概念。这里以 Activity 对象作为 Key ,通过 map 来存储不同 Activity 下的所有注解对象

获取到所有的注解对象后,就可以来构造 bind() 方法了

MethodSpecJavaPoet 提供的一个概念,用于抽象出生成一个函数时需要的基础元素,直接看以下方法应该就可以很容易理解其含义了

通过 addCode() 方法把需要的参数元素填充进去,循环生成每一行 findView 方法

完整的代码声明如下所示

@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {

    private Elements elementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        elementUtils = processingEnv.getElementUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> hashSet = new HashSet<>();
        hashSet.add(BindView.class.getCanonicalName());
        return hashSet;
    }

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment environment) {
        Map<TypeElement, Map<Integer, VariableElement>> typeElementMap = getTypeElementMap(environment);
        for (TypeElement key : typeElementMap.keySet()) {
            Map<Integer, VariableElement> elementMap = typeElementMap.get(key);
            TypeSpec typeSpec = generateCodeByPoet(key, elementMap);
            String packageName = elementUtils.getPackageOf(key).getQualifiedName().toString();
            JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
            try {
                javaFile.writeTo(processingEnv.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    //以 Activity 对象作为 Key ,通过 map 来存储不同 Activity 下的所有注解对象
    private Map<TypeElement, Map<Integer, VariableElement>> getTypeElementMap(RoundEnvironment environment) {
        Map<TypeElement, Map<Integer, VariableElement>> typeElementMap = new HashMap<>();
        Set<? extends Element> elementSet = environment.getElementsAnnotatedWith(BindView.class);
        //Element 代表程序的一个元素,这个元素可以是:包、类、接口、变量、方法等,这里是获取所有包含指定注解的元素

        //遍历所有包含 BindView 的注解对象
        for (Element element : elementSet) {
            VariableElement varElement = (VariableElement) element;//因为 BindView 的作用对象是 FIELD,因此可以直接强转
            TypeElement typeElement = (TypeElement) varElement.getEnclosingElement();//返回封装此 Element 的最里层元素
            //如果 Element 直接封装在另一个元素的声明中,则返回该封装元素,此处表示的即 Activity 类对象

            Map<Integer, VariableElement> varElementMap = typeElementMap.get(typeElement);
            if (varElementMap == null) {
                varElementMap = new HashMap<>(); //看指定的key(即Activity)是否已经存在,如果不存在的话创建并添加到map中
                typeElementMap.put(typeElement, varElementMap);
            }

            BindView bindAnnotation = varElement.getAnnotation(BindView.class); //获取注解
            int viewId = bindAnnotation.value();//获取注解值
            varElementMap.put(viewId, varElement);//将每个包含了 BindView 注解的字段对象以及其注解值保存起来
        }
        return typeElementMap;
    }

    /**
     * 生成 Java 类,以 Activity名 + ViewBinding 进行命名
     *
     * @param typeElement   注解对象上层元素对象,即 Activity 对象
     * @param varElementMap Activity 包含的注解对象以及注解的目标对象
     */
    private TypeSpec generateCodeByPoet(TypeElement typeElement, Map<Integer, VariableElement> varElementMap) {
        return TypeSpec.classBuilder(typeElement.getSimpleName().toString() + "ViewBinding")
            .addModifiers(Modifier.PUBLIC)
            .addMethod(generateMethodByPoet(typeElement, varElementMap))
            .build();
    }

    /**
     * 生成方法
     *
     * @param typeElement   注解对象上层元素对象,即 Activity 对象
     * @param varElementMap Activity 包含的注解对象以及注解的目标对象
     */
    private MethodSpec generateMethodByPoet(TypeElement typeElement, Map<Integer, VariableElement> varElementMap) {
        ClassName className = ClassName.bestGuess(typeElement.getQualifiedName().toString());
        String parameter = "_" + toLowerCaseFirstChar(className.simpleName());//方法参数名
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
            .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
            .returns(void.class)
            .addParameter(className, parameter);

        for (int viewId : varElementMap.keySet()) {
            VariableElement element = varElementMap.get(viewId);
            String name = element.getSimpleName().toString();//被注解的字段名
            String type = element.asType().toString();//被注解的字段的对象类型的全名称
            String text = "{0}.{1}=({2})({3}.findViewById({4}));";
            methodBuilder.addCode(MessageFormat.format(text, parameter, name, type, parameter, String.valueOf(viewId)));
        }
        return methodBuilder.build();
    }

    //将首字母转为小写
    private static String toLowerCaseFirstChar(String text) {
        if (text == null || text.length() == 0 || Character.isLowerCase(text.charAt(0))) return text;
        else return String.valueOf(Character.toLowerCase(text.charAt(0))) + text.substring(1);
    }
}

使用效果

首先在 MainActivity 中声明几个 BindView 注解,然后 Rebuild Project,使编译器根据 BindViewProcessor 生成我们需要的代码

Rebuild 结束后,就可以在 generatedJava 文件夹下看到 MainActivityViewBinding 类自动生成了

其源码为

package leavesc.hello.apt;

public class MainActivityViewBinding {
    public static void bind(MainActivity _mainActivity) {
        _mainActivity.btnSend = (android.widget.Button) (_mainActivity.findViewById(2131165218));
        _mainActivity.tvName = (android.widget.TextView) (_mainActivity.findViewById(2131165327));
        _mainActivity.etName = (android.widget.EditText) (_mainActivity.findViewById(2131165240));
    }
}

此时有两种方式可以用来触发 bind() 方法

  • MainActivity 方法中直接调用 MainActivityViewBindingbind() 方法
  • 调用ButterKnife.bind(this);并通过反射来触发 MainActivityViewBindingbind() 方法
public class ButterKnife {

    public static void bind(Activity activity) {
        Class clazz = activity.getClass();
        try {
            Class bindViewClass = Class.forName(clazz.getName() + "ViewBinding");
            Method method = bindViewClass.getMethod("bind", activity.getClass());
            method.invoke(bindViewClass.newInstance(), activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

两种方式各有优缺点。第一种方式在每次 build project 后才会生成代码,在这之前无法引用到对应的 ViewBinding 类。
第二种方式可以用固定的方法调用方式,但是相比方式一,反射会略微多消耗一些性能。
但这两种方式的运行结果是完全相同的。

2019-1-10

猜你喜欢

转载自www.cnblogs.com/baiqiantao/p/10250713.html