Android 注解与注解处理器简述

2022-10-24 14:51:22 浏览数 (2)

Android 注解

  • 前言
  • 正文
    • 一、注解
      • ① 注解类型
      • ② 注解生命周期
      • ③ 注解参数
    • 二、注解处理器
      • ① 注册
      • ② 配置
    • 三、使用
      • ① 接口
      • ② 反射
      • ③ 使用
      • ④ 强化
    • 四、源码

前言

  在Android开发中,注解是非常多的,如果不去了解,你可能感受不到注解的存在,一些框架用到的注解是很多的,例如Butterknife、Retrofit、Dagger2、Hilt、ViewBinding、DataBinding等等,下面简单的来了解一下注解。

正文

  这里我们先创建一个项目,命名为 StudyAnnotation。

  点击Finish完成创建,之后我们会看到MainActivity,以及一个onCreate()方法,这似乎没有什么好说明的,但是你可以看到这里onCreate()方法的上面有一个@Override注解,这表示方法重写,怎么重写我们不需要关心,需要关心的是这里面的内容。

  注解本身是用于标注信息的,例如Butterknife,之前在ViewBinding还没有出来之前,我们做Android开发绕不开的一个东西,就是findViewById,而Butterknife就是通过注解,标注了需要进行findViewById的控件,从而在编译时生成类文件,帮我们去写了这些繁琐的代码。还记得ButterKnife的用法吗?

  这个图还是我写天气App时用的,那时候Butterknife还可以用,后面Google在Jetpack中推出组件ViewBinding,至此Butterknife不再推荐你使用,改用ViewBinding。这里就不说这个ViewBinding相比于Butterknife的优势了,因为本文主要是讲述注解的用法。

  假设我们需要自己通过注解,完成对控件findViewById代码的自动生成,需要怎么做?首先我们是不是要写一个注解呢?

一、注解

  为了区别于当前项目代码,我们可以新建一个moudle来写注解,将工程切换到项目模式,右键点击项目名称 New → Module ,然后选择Java or Kotlin Library,输入名称apt_annotation,最后点击Finish按钮,完成Module创建。

Module创建好了,我们在Module中找到com.llw.annotation包,先把默认的MyClass类删除,然后右键点击 New → Java Class,出现一个弹窗,选择@Annotation,输入名称BindView,回车创建完成。

里面的代码就很简单,看起来像是一个接口,但是前面有一个@符号,表示这是一个注解。

代码语言:javascript复制
public @interface BindView {
}

① 注解类型

  但是这还没有完,我们添加一个注解告诉开发者这个注解是用在什么地方的,这里我写入@Target注解,在括号里面输入ElementType,这表示注解使用的地方类型,然后可以点出来很多类型,如下图所示:

这里的注解可以标记的类型比较多,可以在注解、构造方法、字段、局部变量、方法、模块、包、参数等类型上进行注解,而我们就注解字段就可以了,使用ElementType.FIELD

代码语言:javascript复制
@Target(ElementType.FIELD)
public @interface BindView {
}

② 注解生命周期

  是不是没想到,注解也会有生命周期呢?我们在@Target注解下面增加一个@Retention注解,里面同样需要填写参数,这里的参数就没有注解类型那么多了,只有三个。

SOURCE表示源码期,注解的文件在Javac编译Java代码生成class文件之后就找不到了,CLASS表示编译期,这一种包括前面的源码期,在编译器间有效,文件能找到。RUNTIME表示运行期,表示程序运行时注解信息依然存在。在编译期时Java虚拟机加载class文件的时候会忽略掉注解, 这里我们选择使用RetentionPolicy.RUNTIME

代码语言:javascript复制
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
}

③ 注解参数

刚才在使用@Target@Retention注解的使用我们都需要在注解中输入一个参数,这是因为他们在注解中定义方法,而我们写入的参数就这个这个方法所需要返回的参数类型。

  这里我们看到Retention注解,里面定义了一个value()方法,返回类型是RetentionPolicy,这是一个枚举类,另外我们注意到它的上面的注解类型和注解生命周期都是刚才提到过的,下面回到我们自己的BindView,因为要给控件写findViewById,这里最重要的是拿到控件的id,这是int类型,因此我们可以在BindView注解中增加一个value()方法,返回int类型,代码如下:

代码语言:javascript复制
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();
}

二、注解处理器

  注解我们知道了大概的内容,之前我们提到注解用于标注信息,那么标注之后呢?就完了吗?那不是没有意义吗?所以为了使注解的标记变的有意义,我们还需要一个东西来处理标注的信息,那就是注解处理器。

  下面再创建一个Module来写注解处理器,创建的方式和之前创建注解Module一样,名称改成apt_processor,类名由MyClass改成AnnotationProcessor,这里我们使用的apt是有含义的。

  APT(Annotation Processing Tool)即注解处理器,是一种处理注解工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。简单来说就是在编译期,通过注解生成.java文件。简单来说就是通过注解去插手编译期中的一些事情,达到我们的目的。类似于隔壁老王和阁下老六的结合体,当然这是Android中合法的手段,不是什么黑科技。

① 注册

  而注解处理器要正常使用的话需要注册,首先我们添加一个依赖库,在apt_processor模块下的build.gradle中添加如下代码

代码语言:javascript复制
dependencies {

    implementation 'com.google.auto.service:auto-service:1.0-rc7'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'

    implementation project(path: ':apt_annotation')
}

  这里前面两行代码是注册注解处理器需要的,最后一行代码,我们需要处理注解,要依赖之前的注解模块,这很好理解,然后Sync Now就可以了,下面我们修改AnnotationProcessor类的代码如下所示:

代码语言:javascript复制
@AutoService(Process.class)
public class AnnotationProcessor extends AbstractProcessor {

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

  这里我们通过注解的方式去注册这个注解处理器,里面的参数是Process.class,这是javac在编译java文件时使用的,然后继承AbstractProcessor,重写里面的process()方法,这个方法很重要,我们的注解处理就是在这里完成的。

② 配置

  要让我们的注解处理器生效还需要一些配置,你可以通过其他注解来完成配置,也可以通过代码的方式来完成,为了更好说明,这里我用代码的方式,首先我们需要配置注解处理器所支持的版本,在AnnotationProcessor中增加如下方法代码:

代码语言:javascript复制
	@Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }

这里返回的是父类所支持的版本,你也可以改成最新的,使用SourceVersion.latestSupported()

代码语言:javascript复制
	@Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

然后再配置一下注解类型,也就是我们的注解处理器能处理那些注解,在AnnotationProcessor中增加如下方法代码:

代码语言:javascript复制
	@Override
    public Set<String> getSupportedAnnotationTypes() {
        return super.getSupportedAnnotationTypes();
    }

  这里返回的是一个Set集合,里面的String类型的,那么我们可以将之前所写的注解BindView添加到集合中返回,修改getSupportedAnnotationTypes()方法代码,如下所示:

代码语言:javascript复制
	@Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }

  通过注解处理器处理注解所标识的内容时会生成一个编译时文件,一般都在build文件夹下,这里我们需要手动去生成一个文件,在AnnotationsProcessor中定义一个变量。

代码语言:javascript复制
Filer filer;

同时在注解处理器初始化的时候对这个变量进行赋值,方法代码如下所示:

代码语言:javascript复制
	@Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        filer = processingEnvironment.getFiler();
    }

最后我们修改process()方法的代码,如下所示:

代码语言:javascript复制
	@Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        //获取App中使用了BindView注解的对象
        Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        //开始对elementsAnnotatedWith进行分类
        Map<String, List<VariableElement>> map = new HashMap<>();
        for (Element element : elementsAnnotatedWith) {
            //获取所注解的变量元素
            VariableElement variableElement = (VariableElement) element;
            //通过变量元素获取所处的外部类(例如MainActivity中的TextView,TextView就是变量元素,MainActivity就是外部类)
            String activityName = variableElement.getEnclosingElement().getSimpleName().toString();
            //获取map集合中的变量元素列表,如果为空则new一个,再添加进入map集合。
            List<VariableElement> variableElements = map.get(activityName);
            if (variableElements == null) {
                variableElements = new ArrayList<>();
                map.put(activityName, variableElements);
            }
            //添加到变量元素列表中
            variableElements.add(variableElement);
        }
        //生成文件
        if (map.size() > 0) {
            //创建输入流
            Writer writer = null;
            //获取map集合的迭代器
            Iterator<String> iterator = map.keySet().iterator();
            //如果iterator.hasNext()为true,执行循环体中的代码
            while (iterator.hasNext()) {
                //获取map的键 (键:外部类名称)
                String activityName = iterator.next();
                //通过键获取到值 (值:变量元素列表)
                List<VariableElement> variableElements = map.get(activityName);
                //获取变量元素所处的外部类,这里强转一下
                TypeElement enclosingElement = (TypeElement) variableElements.get(0).getEnclosingElement();
                //得到包名
                String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();
                //准备写文件,要处理异常
                try {
                    //创建源文件
                    JavaFileObject sourceFile = filer.createSourceFile(packageName   "."   activityName   "_ViewBinding");
                    //打开文件输入流并赋值给writer
                    writer = sourceFile.openWriter();
                    //写入包名(例如:package com.llw.annotation;)
                    writer.write("package "   packageName   ";n");
                    //写入导包IBinder(例如:import com.llw.annotation.IBinder;)
                    writer.write("import "   packageName   ".IBinder;n");
                    //换行
                    writer.write("n");
                    //写入类实现IBinder接口(例如:public class MainActivity_ViewBinding implements
                    // IBinder<com.llw.annotation.MainActivity>{)
                    writer.write("public class "   activityName   "_ViewBinding implements IBinder<"  
                            packageName   "."   activityName   "> {n");
                    //写入@Override注解,注意格式(例如:@Override)
                    writer.write("n    @Override");
                    //写入bind方法(例如:public void bind(com.llw.annotation.MainActivity target) {)
                    writer.write("n    public void bind("   packageName   "."   activityName   " target) {n");
                    //遍历类中每一个需要findViewById的控件
                    for (VariableElement variableElement : variableElements) {
                        //获取控件名称
                        String variableName = variableElement.getSimpleName().toString();
                        //通过注解拿到控件的Id
                        int id = variableElement.getAnnotation(BindView.class).value();
                        //获取控件类型
                        TypeMirror typeMirror = variableElement.asType();
                        //写findViewById语句(例如:target.tvText = (android.widget.TextView) target.findViewById(2131231127);)
                        writer.write("        target."   variableName   " = ("   typeMirror   ") target.findViewById("   id   ");");
                    }
                    //换行 结束
                    writer.write("n    }n}");

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (writer != null) {
                        try {
                            writer.close();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
        return false;
    }

  这里的代码核心逻辑就是通过注解标识获取App中使用了BindView注解的对象,这是一个对象集合,可能一个也可能多个,然后遍历集合,得到每一个对象,获取对象的变量元素,再获取元素所在的外部类,意思就是我的Activity中可能有多个控件被注解,获取到这个Activity,然后通过map进行获取,通过类名作为键去获取值,值是这个类中所有的标注控件,因此得到一个列表。接下来的一部分就是遍历map集合,写入文件,手动去拼接代码,得到一个类似这样的类,代码如下:

代码语言:javascript复制
package com.llw.annotation;
import com.llw.annotation.IBinder;

public class MainActivity_ViewBinding implements IBinder<com.llw.annotation.MainActivity> {

    @Override
    public void bind(com.llw.annotation.MainActivity target) {
        target.tvText = (android.widget.TextView) target.findViewById(2131231127);
    }
}

  那么到这里我们就不用去写findViewById了,通过注解处理器在java文件,进行javac编译时处理这些注解,生成一个编译时文件,下面我们在代码中使用这个注解和注解处理器。

三、使用

  现在的情况是我们的注解处理器添加了注解模块的依赖,而我们的app模块还没有添加任何依赖,因此,我们在使用的时候首先需要在app模块下的build.gradle中dependencies{}闭包下添加如下依赖:

代码语言:javascript复制
	implementation project(path: ':apt_annotation')
    annotationProcessor project(path: ':apt_processor')

这里要注意一点,那就是注解添加依赖和注解处理器添加依赖的方式不同,添加之后点击Sync Now进行同步。

① 接口

  为了能在Activity中使用,我们需要提供一个接口可以绑定Activity,在app模块下的com.llw.annotation包下,新建一个IBinder接口,代码如下:

代码语言:javascript复制
public interface IBinder<T> {
    void bind(T target);
}

② 反射

  然后我们写一个类,通过反射的方式去得到Activity的实例,再通过接口进行绑定,在com.llw.annotation包下新建一个CustomKnife类,代码如下:

代码语言:javascript复制
public class CustomKnife {

    public static void bind(Activity activity) {
        String name = activity.getClass().getName()   "_ViewBinding";
        try {
            //通过反射生成一个类对象
            Class<?> aClass = Class.forName(name);
            //通过newInstance得到接口实例
            IBinder iBinder = (IBinder) aClass.newInstance();
            //最后调用接口bind()方法
            iBinder.bind(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

这里我们就是写了一个方法进行绑定即可,下面就是使用了。

③ 使用

修改一下activity_main.xml中的TextView控件,给它一个id为tv_text,然后回到MainActivity中,修改代码如下所示:

代码语言:javascript复制
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_text)
    TextView tvText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CustomKnife.bind(this);
        tvText.setText("Annotation Processor");
    }
}

  这里的写法就和ButterKnife很相似了,如果你之前用过它的话,这里我们通过注解去标记,注解参数填的是控件id,处理处理器通过这个id就能知道这个MainActivity,然后就可以生成编译时文件。这里布局中的TextView中的文字默认是Hello World!,在onCreate()方法中,绑定了Activity之后直接修改TextView的文字内容,这里并不会报错,因为编译时文件中写好了findViewById了,下面我们运行一下,真机或者虚拟机都可以的。

这里的文字就改变了,然后我们可以在build文件夹中找到MainActivity_ViewBinding。

  这个文件你在Clean Project时就会删除掉,你也可以Rebuild Project。

④ 强化

  虽然我们当前通过手动拼接的方式实现了编译时类的生成,只不过这样写还是有一些太Low了,所以我们需要更人性化的方式去生成编译时类,这里我们需要了解javapoet,这个很关键,下面我们将使用它,在apt_processor模块的build.gradle的dependencies{}闭包下添加如下依赖:

代码语言:javascript复制
	implementation 'com.squareup:javapoet:1.13.0'

然后Sync Now,进入到AnnotationsProcessor类中,找到process()方法,将里面这一段代码抽离出来

放到一个新的方法中,方法代码如下:

代码语言:javascript复制
	private void makefile(Map<String, List<VariableElement>> map) {
        //生成文件
        if (map.size() > 0) {
            //创建输入流
            Writer writer = null;
            //获取map集合的迭代器
            Iterator<String> iterator = map.keySet().iterator();
            //如果iterator.hasNext()为true,执行循环体中的代码
            while (iterator.hasNext()) {
                //获取map的键 (键:外部类名称)
                String activityName = iterator.next();
                //通过键获取到值 (值:变量元素列表)
                List<VariableElement> variableElements = map.get(activityName);
                //获取变量元素所处的外部类,这里强转一下
                TypeElement enclosingElement = (TypeElement) variableElements.get(0).getEnclosingElement();
                //得到包名
                String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();
                //准备写文件,要处理异常
                try {
                    //创建源文件
                    JavaFileObject sourceFile = filer.createSourceFile(packageName   "."   activityName   "_ViewBinding");
                    //打开文件输入流并赋值给writer
                    writer = sourceFile.openWriter();
                    //写入包名(例如:package com.llw.annotation;)
                    writer.write("package "   packageName   ";n");
                    //写入导包IBinder(例如:import com.llw.annotation.IBinder;)
                    writer.write("import "   packageName   ".IBinder;n");
                    //换行
                    writer.write("n");
                    //写入类实现IBinder接口(例如:public class MainActivity_ViewBinding implements
                    // IBinder<com.llw.annotation.MainActivity>{)
                    writer.write("public class "   activityName   "_ViewBinding implements IBinder<"   packageName   "."   activityName   "> {n");
                    //写入@Override注解,注意格式(例如:@Override)
                    writer.write("n    @Override");
                    //写入bind方法(例如:public void bind(com.llw.annotation.MainActivity target) {)
                    writer.write("n    public void bind("   packageName   "."   activityName   " target) {n");
                    //遍历类中每一个需要findViewById的控件
                    for (VariableElement variableElement : variableElements) {
                        //获取控件名称
                        String variableName = variableElement.getSimpleName().toString();
                        //通过注解拿到控件的Id
                        int id = variableElement.getAnnotation(BindView.class).value();
                        //获取控件类型
                        TypeMirror typeMirror = variableElement.asType();
                        //写findViewById语句(例如:target.tvText = (android.widget.TextView) target.findViewById(2131231127);)
                        writer.write("        target."   variableName   " = ("   typeMirror   ") target.findViewById("   id   ");");
                    }
                    //换行 结束
                    writer.write("n    }n}");

                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (writer != null) {
                        try {
                            writer.close();
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

这是之前的方式,我们还是先保留,这样你就可以自己对比一下新的方式,看看优劣,下面我们使用新的方式,在AnnotationsProcessor中我们再增加一个方法,代码如下:

代码语言:javascript复制
	private void makefilePlus(Map<String, List<VariableElement>> map) {
        if (map.size() > 0) {
            for (String activityName : map.keySet()) {
                //通过键获取到值 (值:变量元素列表)
                List<VariableElement> variableElements = map.get(activityName);
                //获取变量元素所处的外部类,这里强转一下
                TypeElement enclosingElement = (TypeElement) variableElements.get(0).getEnclosingElement();
                //得到包名
                String packageName = processingEnv.getElementUtils().getPackageOf(enclosingElement).toString();
                //获取类名实例方式一
                ClassName iBinderName = ClassName.get(packageName, "IBinder");
                //获取类名实例方式二
                ClassName activityClassName = ClassName.bestGuess(activityName);
                //创建类构造器,例如MainActivity_ViewBinding
                TypeSpec.Builder classBuilder = TypeSpec.classBuilder(activityName   "_ViewBinding")
                        //添加修饰符 public
                        .addModifiers(Modifier.PUBLIC)
                        //添加实现接口,例如 implements IBinder<MainActivity>
                        .addSuperinterface(ParameterizedTypeName.get(iBinderName, activityClassName));
                //创建方法构造器 方法名bind()
                MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                        //添加注解
                        .addAnnotation(Override.class)
                        //添加修饰符
                        .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                        //添加方法参数
                        .addParameter(activityClassName, "target");

                for (VariableElement variableElement : variableElements) {
                    //获取控件名称
                    String variableName = variableElement.getSimpleName().toString();
                    //通过注解拿到控件的Id
                    int id = variableElement.getAnnotation(BindView.class).value();
                    //在方法中添加代码,写findViewById语句(例如:target.tvText = target.findViewById(2131231127);)
                    //$L代表的是字面量,variableName对应第一个L,i对应第二个L
                    methodBuilder.addStatement("target.$L = target.findViewById($L)", variableName, id);
                }
                //添加方法
                classBuilder.addMethod(methodBuilder.build());
                try {
                    //写入文件
                    JavaFile.builder(packageName, classBuilder.build())
                            .build()
                            .writeTo(filer);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

代码看起来也很长是吧,只不过是因为我写了很多注释而已,通过javapoet我们就不用再去import了,它会自动完成,所以我们先写出来一个类,通过类构造器TypeSpec.classBuilder,会自动增加括号。然后写方法,通过方法构造器MethodSpec.methodBuilder,最后写findViewById就可以了,这里我们同样需要循环,方法写好之后添加到类构造器中,最后写入文件,通过

代码语言:javascript复制
JavaFile.builder(packageName, classBuilder.build()).build().writeTo(filer);

最终还是写入到filer中,下面在process()方法中,调用makefilePlus()方法,注释makefile()方法。

最后我们修改一下activity_main.xml中的代码:在里面增加一个Button,放在TextView的下面:

代码语言:javascript复制
	<Button
        android:id="@ id/btn_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Button"
        app:layout_constraintEnd_toEndOf="@ id/tv_text"
        app:layout_constraintStart_toStartOf="@ id/tv_text"
        app:layout_constraintTop_toBottomOf="@ id/tv_text" />

然后回到MainActivity中,我们为新的控件也添加注解,修改代码如下所示:

代码语言:javascript复制
public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv_text)
    TextView tvText;
    @BindView(R.id.btn_text)
    Button btnText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        CustomKnife.bind(this);
        tvText.setText("Annotation Processor");
        btnText.setText("This is Button");
    }
}

现在我们运行看看效果。

然后我们看看生成类。

四、源码

如果你觉得代码对你有帮助的话,不妨Fork或者Star一下~

StudyAnnotation

0 人点赞