ButterKnife实现之Android注解处理器使用教程

ButterKnife实现之Android注解处理器使用教程

1、新建一个注解

1.1、编译时注解

创建注解所需的元注解@Retention包含3个不同的值,RetentionPolicy.SOURCE、RetentionPolicy.CLASS、RetentionPolicy.RUNTIME。这3个值代表注解不同的保留策略。

使用RetentionPolicy.RUNTIME的注解为运行时注解,能在程序运行时通过反射获取注解的信息并进行逻辑处理;使用RetentionPolicy.CLASS的注解为编译时注解,能在程序编译时进行预处理操作,比如生成一些辅助代码;使用RetentionPolicy.SOURCE的注解能做一些检查性的操作,比如@Override和@SuppressWarning。

1.2、新建注解

编译时注解能够帮助我们生成辅助代码,能够满足在编译时获取注解信息生成带有findViewById的代码。所以我们新建一个编译时注解。新建注解前,我们新建一个名为annotation的Java Library类型的Module。然后在这个Module新建这个注解,命名为BindView,代码如下:

java 复制代码
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

2、新建注解处理器

注解处理器是处理注解的类,处理编译时注解时我们需要编写一个注解处理器。注解处理器类需要继承AbstractProcessor类。本节我们来学习编写注解处理器,跟上一节一样我们再次新建一个Java Libary的module,这个module命名为processor,并依赖包含注解的annotation Module:在processor module的build.gradle添加如下代码:

groovy 复制代码
dependencies{
	implementation project(':annotation')
}

接着,在这个module中我们新建一个注解处理器-MainProcessor,它继承AbstractProcessor并实现AbstractProcess的4大方法,我们来学习这4大方法。

2.1、AbstractProcessor的4大方法

java 复制代码
/**
* 注解处理器MainProcessor
*/
public class MainProcessor extends AbstractProcessor {

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

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

继承AbstractProcessor需要重写上述代码段的4个方法,依次介绍它们的作用:

1、init方法:被注解处理工具调用,并输入ProcessingEnviroment参数。ProcessingEnviroment提供了很多有用的工具类,比如Elements、Types、Filer和Messager等。

2、process方法:相当于每个处理器的主函数main(),在这里编写扫描、评估和处理注解的代码以及生成Java文件。输入参数RoundEnviroment,可以让你查询包含特定注解的被注解元素。

3、getSupportedAnnotationTypes:这是必须指定的方法,指定这个注解处理器是注册给哪个注解的,注意,它的返回值是一个字符串的集合,包含该处理器想要处理的注解类型的合法全称。

4、getSupportedSourceVersion:用来指定你使用的Java版本,通常这里返回SourceVersion.latestSupported()。

可以将MainProcessor的getSupportAnnotationTypes方法和getSupportedSourceVersion方法更新成如下:

java 复制代码
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> typeSet = new HashSet<>();
        typeSet.add(BindView.class.getCanonicalName());
        return typeSet;
    }

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

2.2、JavaPoet的使用

前面说到过,在程序编译时根据注解信息生成辅助文件,JavaPoet是一个可以生成Java代码的第三方框架,所以我们要利用它生成辅助文件。

1.添加JavaPoet依赖
groovy 复制代码
    implementation 'com.squareup:javapoet:1.7.0'
2.JavaPoet Api使用

1、生成方法

以ButterKnife的bind方法为例,初始化一个id为R.id.tv_hello的TextView,代码如下:

java 复制代码
//这个MainActivity是个例子,实际上使用的是注解所对应的Activity
public void bind(MainActivity activity){
	activity.tvHello = (TextView)(((android.app.Activity)activity).findViewById(R.id.tv_hello));
}

使用JavaPoet生成这个方法:

java 复制代码
MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind") //方法名为bind
                .addModifiers(Modifier.PUBLIC) //方法修饰符:Public
                .addParameter(MainActivity,"activity")//方法的参数:如MainActivity activity
                .returns(void.class); //返回值:void
      
String code = String.format("activity.%s=(%s)(((android.app.Activity)activity).findViewById(%s));\n","tvHello","android.widget.TextView",R.id.tv_hello);
bindMethodBuilder.addCode(code);

2、生成类

以生成MainActivity的辅助类MainActivity_ViewBinding为例,类的内容:

java 复制代码
public class MainActivity_ViewBinding{
	//bind方法就是上面生成的方法
    public void bind(MainActivity activity){
       tvTest = (android.widget.TextView) ((android.app.Activity)activity).findViewById(R.id.tv_test);
    }
}

使用JavaPoet生成该类:

java 复制代码
TypeSpec.classBuilder("MainActivity_ViewBinding")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(bindMethodBuilder.build())
                .build();

这样的话,完整的类就使用JavaPoet生成出来了。还有更多的JavaPoet的用法,推荐看这篇文章:基于JavaPoet自动生成java代码文件

2.3、编写process方法

接下来就是注解处理器的核心部分了,我们通过process方法实现注解解析,生成源码的功能。process方法中需要用到ProcessingEnviroment参数,所以我们先处理init方法,保存变量:

java 复制代码
public class MainProcessor extends AbstractProcessor {

    private Elements elementUtils;
    private ProcessingEnvironment processingEnvironment;

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

process方法的逻辑主要是解析注解和生成代码,我就直接上代码了:

java 复制代码
 @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        /*
         *生成的代码
         * 类:MainActivity_ViewBinding,包名:com.wei.annotation_processor_demo
         * 内容:
         * public class MainActivity_ViewBinding{
         *      public void bind(MainActivity activity){
         *          tvTest = (TextView) ((Activity)activity).findViewById(R.id.tv_test);
         *      }
         * }
         */
        Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(BindView.class);
        Map<VariableElement, Integer> elementMap = new HashMap<>();
        for (Element element : elementsAnnotatedWith) {
            //获取被注解的字段
            VariableElement variableElement = (VariableElement) element;
            //获取被注解的字段的类
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String className = enclosingElement.getSimpleName().toString();
            //获取注解
            BindView bindView = variableElement.getAnnotation(BindView.class);
            int id = bindView.value();
            //保存所有被注解的字段和注解的成员变量值,用于生成代码
            elementMap.put(variableElement, id);
            //获取被注解的字段所在类的包名
            String packageName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();

            //生成代码
            TypeSpec typeSpec = generateCode(className, ClassName.bestGuess(enclosingElement.getQualifiedName().toString()), elementMap);
            //生成javaFile
            JavaFile javaFile = JavaFile.builder(packageName, typeSpec).build();
            try {
                //生成java代码
                javaFile.writeTo(processingEnvironment.getFiler());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return true;
    }

    private TypeSpec generateCode(String className, ClassName parameterClass, Map<VariableElement,Integer> elementMap){
        //生成bind方法
        MethodSpec.Builder bindMethodBuilder = MethodSpec.methodBuilder("bind")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(parameterClass,"activity")
                .returns(void.class);
        for (Map.Entry<VariableElement, Integer> entry : elementMap.entrySet()) {
            String fieldName = entry.getKey().getSimpleName().toString();
            String fieldType = entry.getKey().asType().toString();
            String code = String.format("activity.%s=(%s)(((android.app.Activity)activity).findViewById(%s));\n"
            ,fieldName,fieldType,String.valueOf(entry.getValue()));
            processingEnvironment.getMessager().printMessage(Diagnostic.Kind.NOTE,"fieldName:"+fieldName+",fieldType:"+fieldType+",code:"+code);
            bindMethodBuilder.addCode(code);
        }

        return TypeSpec.classBuilder(className+"_ViewBinding")
                .addModifiers(Modifier.PUBLIC)
                .addMethod(bindMethodBuilder.build())
                .build();
    }

2.4、注册注解处理器

为了能使用注解处理器,需要用一个服务文件来注册它。文件路径为:processor module的根目录/resources/META-INF.services/javax.annotation.processing.Processor。在javax.annotation.processing.Processor中添加内容:com.wei.processor.MainProcessor。这样就成功注册了注解处理器,同时需要注意2点:1.文件路径中的文件夹可能不存在,需要手动创建;2.文件内容是注解处理器的包名+类名,不要照抄我的。

AutoService

如果不想手动添加服务文件,就使用AutoService框架来生成服务文件。

使用步骤:

1、添加依赖

groovy 复制代码
//google autoService
implementation 'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor "com.google.auto.service:auto-service:1.0-rc4"

2、使用

在注解处理器的类上添加@AutoService注解即可:

java 复制代码
@AutoService(Processor.class)
public class MainProcessor extends AbstractProcessor {
//省略内容
//...
}

这样就实现了刚才我们手动创建服务文件同样的功能。

3、使用

注解处理器编写结束了,我们需要验证是否能够实现ButterKnife同样的效果。验证方法:我们在app module中添加annotation、processor两个库的依赖,在MainActivity中使用BindView注解,看看app module根目录/build/ap_generated_sources/debug/out/有无MainActivity_ViewBinding文件生成。

添加依赖:
groovy 复制代码
 implementation project(":annotation")
//    implementation project(":processor")
 annotationProcessor project(":processor")

使用annotationProcessor代替implementation有以下好处:

1、annotationProcessor引用的库只会在编译期间被依赖使用,不会打包进入apk,因为注册处理器是在编译期间使用的,打包进入apk会占用空间

2、为注解处理器生成的代码设置好路径,以便Android Studio能找到它

使用BindView注解
java 复制代码
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_hello)
    TextView tvHello;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
查看生成文件
使用

生成ViewBinding类后,可以通过反射执行该类bind方法,实现findViewById逻辑:

java 复制代码
private void bind() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InstantiationException, InvocationTargetException {
    Class<?> clazz = Class.forName(getClass().getName() + "_ViewBinding");
    System.out.println(getClass().getName());
    Method bind = clazz.getDeclaredMethod("bind", getClass());
    bind.invoke(clazz.newInstance(), this);
}

调用这个方法也就实现了findViewById逻辑,最后:

java 复制代码
@BindView(R.id.tv_hello)
TextView tvHello;
    
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    try {
        bind();
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchMethodException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (InvocationTargetException e) {
        e.printStackTrace();
    }
    tvHello.setText("我成功了!");
}

4、参考文章

感谢一下文章提供的教程,万分感激:

1、Android APT技术学习

2、基于JavaPoet自动生成java代码文件

3、深入理解编译注解(二)annotationProcessor与android-apt

相关推荐
华仔啊42 分钟前
Stream 代码越写越难看?JDFrame 让 Java 逻辑回归优雅
java·后端
ray_liang1 小时前
用六边形架构与整洁架构对比是伪命题?
java·架构
Ray Liang2 小时前
用六边形架构与整洁架构对比是伪命题?
java·python·c#·架构设计
Java水解2 小时前
Java 中间件:Dubbo 服务降级(Mock 机制)
java·后端
砖厂小工4 小时前
用 GLM + OpenClaw 打造你的 AI PR Review Agent — 让龙虾帮你审代码
android·github
张拭心5 小时前
春节后,有些公司明确要求 AI 经验了
android·前端·人工智能
张拭心5 小时前
Android 17 来了!新特性介绍与适配建议
android·前端
SimonKing6 小时前
OpenCode AI辅助编程,不一样的编程思路,不写一行代码
java·后端·程序员
FastBean7 小时前
Jackson View Extension Spring Boot Starter
java·后端
Kapaseker7 小时前
Compose 进阶—巧用 GraphicsLayer
android·kotlin