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

相关推荐
wk灬丨4 分钟前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸5 分钟前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android
激流丶14 分钟前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic
Themberfue17 分钟前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·
让学习成为一种生活方式34 分钟前
R包下载太慢安装中止的解决策略-R语言003
java·数据库·r语言
晨曦_子画40 分钟前
编程语言之战:AI 之后的 Kotlin 与 Java
android·java·开发语言·人工智能·kotlin
孤客网络科技工作室1 小时前
AJAX 全面教程:从基础到高级
android·ajax·okhttp
南宫生1 小时前
贪心算法习题其三【力扣】【算法学习day.20】
java·数据结构·学习·算法·leetcode·贪心算法
Heavydrink1 小时前
HTTP动词与状态码
java
ktkiko111 小时前
Java中的远程方法调用——RPC详解
java·开发语言·rpc