深刻了解注解及Android APT

注解是什么

注解是一种能被添加到java代码中的元数据,类、方法、变量、参数和包均可以用注解来修饰。注解对于它所修饰的代码并无直接的影响。java

一、什么是元注解

用于对注解类型进行注解的注解类,称之为元注解。JDK1.5中提供了4个标准元注解。android

@Target: 描述注解的使用范围,说明被它所注解的注解类可修饰的对象范围 @Retention: 描述注解保留的时期,被描述的注解在它所修饰的类中可保留到什么时候 @Documented: 描述在使用Javadoc工具为类生成帮助文档时是否要保留其注解信息 @Inherited: 使被它修饰的注解修饰的注解类的子类能继承到注解markdown

二、元注解@Target的取值及其含义

@Target({ElementType.FIELD, ElementType.METHOD})
public @interface Event {
}
复制代码

@Target描述的是注解的使用范围,携带的值为枚举,做用是标明它修饰的注解能够用在哪些地方。好比上述例子中的@Event只能做用于属性和方法上app

ElementType的取值和意义以下:框架

public enum ElementType {
	//做用在类上
	TYPE,	
	
	//做用在属性上
	FIELD,	
	
	//做用在方法上
	METHOD,	
	
	//做用在参数上
	PARAMETER,	
	
	//做用在构造器上
	CONSTRUCTOR,	
	
	//做用在局部变量上
	LOCAL_VARIABLE,	
	
	//做用在注解上
	ANNOTATION_TYPE,
	
	//做用在包名上
	PACKAGE,	
	private ElementType(){...}
}
复制代码

注意:每一个注解能够跟n个ElementType关联。当无指定时,注解可用于任何地方。ide

三、元注解@Retention的取值及其含义

@Target({ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Event {
}
复制代码

@Retention描述的是注解的存在时期。如上述例子中@Event为运行时注解,在源码,字节码及运行时皆存在工具

RetentionPolicy取值和意义以下:gradle

public enum RetentionPolicy {
	//源码时注解, 只在源码中存在,编译后便不存在了
	SOURCE, 
	
	//编译时注解,源码和编译时存在,运行时不存在
	CLASS,
	
	//运行时注解,源码,编译时,运行时都存在
	RUNTIME;
	
	private RetentionPolicy(){...}
}
复制代码

注意:每一个注解只能和一个RetentionPolicy关联。当无指定时,默认为RetentionPolicy.CLASSui

四、其余元注解介绍

@Documented: 类和方法的Annotation在缺省状况下是不出如今javadoc中的。若是使用@Documented修饰该注解,则表示它能够出如今javadoc中。this

@Inheried: 当使用该注解的类有子类时,注解在子类仍然存在。经过反射其子类可得到父类相同的注解

五、自定义注解的参数

public @interface Person {
	public String name();
	//默认值
	int age() default 18;
	int[] array();
}
复制代码

注解可以携带的参数类型有:基本数据类型,String, Class, Annotation, enum

注解的使用

注解目前比较常见的使用场景有

a、编译时动态检查,好比某参数的取值只能为某些int值,如颜色。则可使用编译时注解在编译时对参数进行检查

b、编译时动态生成代码,使用注解处理器在编译时生成class文件。如ButterKnife实现

c、运行时动态注入,用注解实现IOC,许多框架将原有配置文件xml改为注解用的即是IOC注入。

一、编译时注解-Apt注解处理器使用

下面以实际案例讲解。案例目标: 实现注解绑定控件,效果以下

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    public TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        Toast.makeText(this, tv.getText().toString(), Toast.LENGTH_SHORT).show();
    }
    
}
复制代码

一、工程结构

在这里插入图片描述

  1. app模块, 一个android模块,是demo的主模块,内容是demo的演示部分MainActivity
  2. annotation模块,一个java Library模块,用于放置注解
  3. compile模块,一个java Library模块,注解处理器主要实如今这个模块中实现
  4. library模块,一个android Library模块,配合注解处理器生成的代码,实现注解绑定控件功能

app模块内容和上述实现目标一致,相信都看得懂。下面逐一介绍其余模块

二、annotation模块

注解模块存放注解,本案例中的注解为@BindView。因为要编译期获取注解,生成相关代码,因此该注解为编译时代码(@Retention(RetentionPolicy.CLASS);又由于要做用在属性上,因此该注解的做用目标为@Target(ElementType.FIELD);而且@BindView具备一个参数表明控件id,类型为int。由此可得出以下注解声明

//做用在属性上
@Target(ElementType.FIELD)
//编译时注解
@Retention(RetentionPolicy.CLASS)
public @interface BindView {

    //传递参数,此处为控件id
    int value();

}
复制代码

三、compile模块

一、本模块要使用注解处理器,首先在build.gradle中引入相关库,build.gradle内容以下

apply plugin: 'java-library'

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    
    //google出品,注解处理器库
    compileOnly 'com.google.auto.service:auto-service:1.0-rc6'
    annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
	
	//javapoet用于生成java类
    implementation 'com.squareup:javapoet:1.10.0'
    implementation project(':annotation')
}

sourceCompatibility = "7"
targetCompatibility = "7"
复制代码

注解类:

第一步:扫描出代码中被注解的属性及其对应的activity,存放到map中

//做用是声明注解处理器
@AutoService(Processor.class)
//声明生成代码是基于java1.7
@SupportedSourceVersion(SourceVersion.RELEASE_7)
//声明注解处理器支持的注解
@SupportedAnnotationTypes("com.sq.annotation.BindView")
public class ButterKnifeProcessor extends AbstractProcessor {

	//用于打印日志
    private Messager mMessager;
	
	//存放activity和activity内注解的控件
    private Map<TypeElement, List<VariableElement>> mTargetMap;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mMessager = processingEnvironment.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    	//得到被BindView注解的全部元素
        Set<Element> views = (Set<Element>) roundEnvironment.getElementsAnnotatedWith(BindView.class);
        if (views != null && views.size() > 0) {
            //将activity和对应的注解的控件放到map中
            mTargetMap = new HashMap<>();
            for (Element view : views) {
                if (view instanceof VariableElement) {
                    //得到所属类元素,即Activity
                    TypeElement activityElement = (TypeElement) view.getEnclosingElement();
                    if (mTargetMap.get(activityElement) == null) {
                        ArrayList targetList = new ArrayList<VariableElement>();
                        targetList.add(view);
                        mTargetMap.put(activityElement, targetList);
                    } else {
                        mTargetMap.get(activityElement).add((VariableElement) view);
                    }
                }
            }

            //遍历对应activity
            if (mTargetMap.size() > 0) {
                for (Map.Entry<TypeElement, List<VariableElement>> entry : mTargetMap.entrySet()) {
                    String activityName = entry.getKey().getSimpleName().toString();
                    mMessager.printMessage(Diagnostic.Kind.NOTE,"activity类名为:" + activityName);
                    for (VariableElement view : entry.getValue()) {
                        mMessager.printMessage(Diagnostic.Kind.NOTE, "被注解的属性为: " + view.getSimpleName().toString());
                    }
                    //为每个activity生成代码
                    generateCode(entry.getKey(), entry.getValue());
                }
            }
        }
        return false;
    }

}
复制代码

上述代码,打印出来的日志为: 注: activity类名为:MainActivity 注: 被注解的属性为: tv

第二步:生成代码 因为是为activity绑定控件,生成的代码以下:

public class MainActivity$ViewBinder implements ViewBinder<MainActivity> {
  @Override
  public void bind(final MainActivity target) {
    target.tv = target.findViewById(2131165359);
  }
}
复制代码

其中,ViewBinder是接口,其代码放于library模块中,代码以下:

public interface ViewBinder<T> {
    void bind(T target);
}
复制代码

一般在生成代码前,首先也是要先想明白生成的代码是怎样的,先有模板再开始写生成的逻辑。

如下开始写generateCode()方法内容

private void generateCode(TypeElement activityElement, List<VariableElement> viewElements) {

        //用于得到activity类名在javapoet中的表示
        ClassName className = ClassName.get(activityElement);

        //生成的类实现的接口
        TypeElement viewBinderType = mElementUtils.getTypeElement("com.sq.library.ViewBinder");
        //实现的接口在javapoet中的表示
        ParameterizedTypeName typeName = ParameterizedTypeName.get(ClassName.get(viewBinderType), className);

        //bind方法参数,即MainActivity target
        ParameterSpec parameterSpec = ParameterSpec.builder(className, "target", Modifier.FINAL).build();
        //方法声明:public void bind(final MainActivity target)
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("bind")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(parameterSpec);
        //方法体
        for (VariableElement viewElement : viewElements) {
            //获取属性名
            String fieldName = viewElement.getSimpleName().toString();
            //获取@BindView注解的值
            int annotationValue = viewElement.getAnnotation(BindView.class).value();
            //target.tv = target.findViewById(R.id.tv);
            String methodContent = "$N." + fieldName + " = $N.findViewById($L)";
            //加入方法内容
            methodBuilder.addStatement(methodContent, "target", "target", annotationValue);
        }

        //生成代码
        try {
            JavaFile.builder(className.packageName(),
                    TypeSpec.classBuilder(className.simpleName() + "$ViewBinder")
                            .addSuperinterface(typeName)
                            .addModifiers(Modifier.PUBLIC)
                            .addMethod(methodBuilder.build())
                            .build())
                    .build()
                    .writeTo(mFiler);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
复制代码

四、library模块

library模块的做用是配合compile模块生成的代码,提供给app模块使用。实现的是MainActivity中ButterKnife.bind(this)以及compile模块生成的MainActivity&MainActivityViewBinder实现的ViewBinder接口 ButterKnife类代码以下:

public class ButterKnife {

    public static void bind(Activity activity) {

        try {
        	//找到对应activity的ViewBinder类,调用bind方法并将activity做为参数传入
            Class viewBinderClass = Class.forName(activity.getClass().getName() + "$ViewBinder");
            ViewBinder viewBinder = (ViewBinder) viewBinderClass.newInstance();
            viewBinder.bind(activity);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}
复制代码

五、app模块使用compile模块

build.gradle配置以下:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.sq.aptdemo"
        minSdkVersion 19
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation project(':annotation')
    implementation project(':library')
    //引用注解处理模块的方式以下:
    annotationProcessor project(":compile")
}
复制代码

如何触发? 在这里插入图片描述 make Module 'app'即可触发编译,使得compile模块开始执行。

查看生成的代码: 在这里插入图片描述 运行app模块后运行正常,控件成功和id绑定。

至此,使用apt注解处理器生成代码完成控件注入开发完成。

二、运行时注解实现控件注入

案例目标,效果以下:

public class MainActivity extends AppCompatActivity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //注入控件
        InjectUtils.bind(this);
        Toast.makeText(this, tv.getText().toString(), Toast.LENGTH_SHORT).show();
    }
}
复制代码

能够看出,使用时和利用编译时注解生成代码并没有差异,不过这里的属性TextView 能够是私有成员。由于注入使用的是反射实现的。

一、工程结构

在这里插入图片描述 比使用编译时注解少了compile模块。下面一一介绍。

二、annotation模块

注解模块内容依然是存放注解,这里作演示,只用了一个BindView注解

@Target(ElementType.FIELD)
//运行时注解
@Retention(RetentionPolicy.RUNTIME)
public @interface BindView {
    int value();
}
复制代码

三、library模块

public class InjectUtils {

    public static void bind(Activity target) {
        //获取activity的Class
        Class activityClass = target.getClass();
        //获取到activity全部属性
        Field[] fields =  activityClass.getDeclaredFields();
        if (fields != null) {
            //遍历全部属性,找到有注解的属性
            for (Field field : fields) {
                field.setAccessible(true);
                BindView annotation = field.getAnnotation(BindView.class);
                if (annotation != null) {
                    //获取到注解带的id
                    int id = annotation.value();
                    //找到id对应的view
                    View targetView = target.findViewById(id);
                    try {
                        //设置属性的值为对应的view,完成绑定
                        field.set(target, targetView);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
    }
}
复制代码

至此,便完成了控件注入。 能够看到实际上运行时注解实现控件注入相对简单些,但因为这种方式使用了反射,运行效率上相对差一些。

相关文章
相关标签/搜索