关于注解的理解

178 阅读7分钟
  • 首先理解反射(很必要) 
反射就是在运行状态下,可以知道这个类的属性和方法。
即动态获取信息以及动态调用方法的功能成为java语言的反射机制 
  • 注解分为两种类型
1 运行时注解
2 编译时注解
需要深刻理解两者的区别
1)保留阶段不同。运行时注解可保留到运行时,可在运行时访问,而编译时注解保留到编译时,运行时无法访问
2)原理不同。运行时注解是Java反射机制,而编译时注解通过APT,AbstractProcessor
3)性能不同。运行时注解由于Java反射,因此对性能有影响,编译时注解对性能没有影响。
4)产物不同。运行时注解只需要自定义注解处理器即可,不会产生其他文件,而编译时注解会产生新的Java源文件

核心类

@Target

表明当前注解可以使用在哪种元素上。
ElementType有以下几种:

  • CONSTRUCTOR 构造器声明
  • FIELD 域声明(包括enum实例)
  • LOCAL_VARIABLE 局部变量声明
  • METHOD 方法声明
  • PACKAGE 包声明
  • PARAMETER 参数声明
  • TYPE 类、接口、注解类型、enum类型

@Retention

表示需要在什么级别保存该注解信息。

  • SOURCE 源码级别,注解将被编译器丢弃,只存在源码中,其功能是与编译器交互,用于代码检测,如@Override,@SuppressWarings,许多框架如Dragger就是使用这个级别的注解,这个级别的框架额外效率损耗发生在编译时。
  • CLASS 字节码级别,注解存在源码与字节码文件中,主要用于编译时生成而外的文件,如XML,Java文件等,这个级别需要添加JVM加载时候的代理(javaagent),使用代理来动态修改字节码文件(由于Android虚拟机并不支持所以本专题不会再做介绍,在Android中可以使用aspectJ来实现类似这个级别的功能)。
  • RUNTIME 运行时级别,注解存在源码,字节码与Java虚拟机中,主要用于运行时反射获取相关信息,许多框架如OrmLite就是使用这个级别的注解,这个级别的框架额外的效率损耗发生在程序运行时。
  •  运行时注解详解

运行时注解,定义好注解,在源代码中通过反射拿到注解信息,解析即可

举一个简单的例子,项目中有的activity支持滑动返回,而有的页面不支持,可以通过自定义注解实现,定义一个ban的字段参数
  1. 定义注解

@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface BanDragBack {
    boolean ban() default false;
}

  2. 将注解加在类上

@BanDragBack(ban = true)
class SampleActivity 

 3. 运行时解析注解

BanDragBack banDragBack = (BanDragBack) activity.getAnnotation(BanDragBack.class);if(banDragBack.ban())

以上是一个运行时注解的简单例子

  • 编译时注解详解   
我的理解是在编译期,自动根据注解信息动态的生成包含我们需要的处理逻辑Java文件,交给编译器,大部分分人都用过ButterKnife,Retrofit等框架,这些框架只需要在用的时候使用注解,就可以直接使用了,非常方便。并且这些框架并没有减少性能。

参考网图,apt流程



以butterknife为例,给大家做介绍

编译时注解的实现需要以下两个库支持。

 1、com.squareup:javapoet 提供注解处理器API

 2、com.google.auto.service:auto-service javapoet,auto-service提供自动注册处理器的功能。

 若要自动生成代码,需要先创建一个注解管理器。

 继承AbstractProcessor 有四个方法需要重载。 

 @Override void init(ProcessingEnvironment) 获取处理注解的上下文信息,一般用于获取文件句柄,自定义参数等逻辑。

@Override void getSupportedSourceVersion() 直接return SourceVersion.latestSupported();,固定套路。 

@Override Set getSupportedAnnotationTypes() 告诉注解处理器支持哪些注解。 

@Override boolean process(Set<? extends TypeElement>, RoundEnvironment) 第一个参数Set即为扫描出来的被标记注解的类/字段的集合,在这个方法中根据这些信息生成代码。 这个方法的返回值和事件分发的意义比较类似。如果return true,表示这些注解扫描结果被『我这个』注解处理器『消化』掉了,其他处理器不需要再处理。 > 如果多个Module中的代码都标记了注解,那么每扫描一个Module中的代码,注解处理器就会被回调一次。

butterKnife为例,大家都在使用,便于理解

  • 在annotation中写上注解

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

创建类processor继承AbstractProcessor

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
    private Elements elementUtils;
    private Filer filer;

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

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

 指定我们所需要的annotation

// 指定processortype
public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    Set<Class<? extends Annotation>> supportedAnnotations = getSupportedAnnotations();
    for (Class<? extends Annotation> supportedAnnotation : supportedAnnotations) {
        types.add(supportedAnnotation.getCanonicalName());
    }
    return types;
}

private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
    annotations.add(BindView.class);
    return annotations;
}

在process中处理annotation

Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(BindView.class);

// 将获取到的bindview细分到每个class
Map<Element, List<Element>> elementMap = new LinkedHashMap<>();

for (Element element : elements) {
    // 返回activity
    Element enclosingElement = element.getEnclosingElement();

    List<Element> bindViewElements = elementMap.get(enclosingElement);
    if (bindViewElements == null) {
        bindViewElements = new ArrayList<>();
        elementMap.put(enclosingElement, bindViewElements);
    }
    bindViewElements.add(element);
}

生成所需的代码

// 生成代码
for (Map.Entry<Element, List<Element>> entrySet : elementMap.entrySet()) {
    Element enclosingElement = entrySet.getKey();
    List<Element> bindViewElements = entrySet.getValue();

    // public final class xxxActivity_ViewBinding implements Unbinder
    // 获取activity的类名
    String activityClassNameStr = enclosingElement.getSimpleName().toString();
    System.out.println("------------->" + activityClassNameStr);
    ClassName activityClassName = ClassName.bestGuess(activityClassNameStr);
    ClassName unBinderClassName = ClassName.get("com.fastaoe.butterknife", "Unbinder");
    TypeSpec.Builder classBuilder =
            TypeSpec.classBuilder(activityClassNameStr + "_ViewBinding")
                    .addModifiers(Modifier.FINAL, Modifier.PUBLIC)
                    .addSuperinterface(unBinderClassName)
                    // 添加属性 private MainActivity target;
                    .addField(activityClassName, "target", Modifier.PRIVATE);

    // unbind()
    ClassName callSuperClassName = ClassName.get("android.support.annotation", "CallSuper");
    MethodSpec.Builder unbindMethodBuilder = MethodSpec.methodBuilder("unbind")
            .addAnnotation(Override.class)
            .addAnnotation(callSuperClassName)
            .addModifiers(Modifier.PUBLIC, Modifier.FINAL);

    // 构造函数
    MethodSpec.Builder constructorMethodBuilder = MethodSpec.constructorBuilder()
            .addParameter(activityClassName, "target")
            .addModifiers(Modifier.PUBLIC)
            // this.target = target
            .addStatement("this.target = target");

    for (Element bindViewElement : bindViewElements) {
        // textview
        String fieldName = bindViewElement.getSimpleName().toString();
        // Utils
        ClassName utilsClassName = ClassName.get("com.fastaoe.butterknife", "Utils");
        // R.id.textview
        int resourceId = bindViewElement.getAnnotation(BindView.class).value();
        // target.textview = Utils.findViewById(target, R.id.textview)
        constructorMethodBuilder.addStatement("target.$L = $T.findViewById(target, $L)", fieldName, utilsClassName, resourceId);
        // target.textview = null
        unbindMethodBuilder.addStatement("target.$L = null", fieldName);
    }


    classBuilder.addMethod(unbindMethodBuilder.build())
            .addMethod(constructorMethodBuilder.build());

    // 获取包名
    String packageName = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();

    try {
        JavaFile.builder(packageName, classBuilder.build())
                .addFileComment("自己写的ButterKnife生成的代码,不要修改!!!")
                .build().writeTo(filer);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

在butterknife中编写注入代码 

public class ButterKnife {    
public static Unbinder bind(Activity activity) {
        try {
            Class<? extends Unbinder> bindClazz = (Class<? extends Unbinder>)
                    Class.forName(activity.getClass().getName() + "_ViewBinding");
            // 构造函数
            Constructor<? extends Unbinder> bindConstructor = bindClazz.getDeclaredConstructor(activity.getClass());

            Unbinder unbinder = bindConstructor.newInstance(activity);
            return unbinder;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return Unbinder.EMPTY;
    }
}

最后使用

public class MainActivity extends AppCompatActivity {

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

    private Unbinder bind;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bind = ButterKnife.bind(this);

        textview.setText("修改后的文字");
    }

    @Override
    protected void onDestroy() {
        bind.unbind();
        super.onDestroy();
    }
}

以上是butterknife的原理

下面在给大家介绍在跳转协议框架中的使用,根据注解自动生成跳转字典 ,其实就是解藕模块调用,让组件间、模块间是实现完全的独立。ARouter是:阿里巴巴自研路由框架,主要解决组件间、模块间的 界面跳转 问题。这部分具体可参考阿里巴巴的跳转协议框架Arouter,下面是一个简易版本的介绍

使用介绍

1、注册跳转协议(@MyRoute) 代码示例:

@MyRoute(
        pageType = "main",
        des = "首页"
)
public class CenterActivity {
    ···
}

对支持跳转协议的落地页(Activity)加上@MyRoute,参数pageType为必填,值为该落地页对应的pageType。参数des为选填,值为该落地页说明。 MyRouter会在编译时扫描@WbuRoute,然后生成跳转目录代码。(此部分代码为自动生成,每次编译时可生成一遍)

MyRouter生成的目录代码:

public class LibRouteCatalog {
  public static Map<String, String> ROUTE = new HashMap<String, String>();

  static {
    //"首页"
    ROUTE.put("main", "com.sample.CenterActivity");
    //...
  }
}

在我们的代码中调用生成的代码即可

String targetLib = LibRouteCatalog.ROUTE.get(pagetype);

那么为了实现此功能,我们需要做哪些工作呢

首先,我们需要自定义注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)public 
@interface MyRoute
 {    
 String pageType();  
 String des() default "";
}

接下来编写注解处理器

public class MyRouteProcessor extends AbstractProcessor {
private Builder mRouteMapFieldSpec;
private String mModuleName;
private String mOutputDir;public MyRouteProcessor() {}public synchronized void init(ProcessingEnvironment processingEnvironment) {  
  super.init(processingEnvironment);   //这里是配置生成文件的目录
 this.mModuleName = (String)processingEnvironment.getOptions().get("module_name");
 this.mOutputDir = (String)processingEnvironment.getOptions().get("route_catalog_output_path");   
 this.mOutputDir = this.mOutputDir + "/src/main/java";}}
public SourceVersion getSupportedSourceVersion()  //固定套路
{    
   return SourceVersion.latestSupported();
}public Set<String> getSupportedAnnotationTypes() {   //告诉注解器支持哪些注解,这里是我们的Myroute注解
 Set<String> annotations = new HashSet(); 
  annotations.add(MyRoute.class.getCanonicalName()); 
return annotations;
}

//重点来了,,,自动生成我们需要的类信息
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {  
//从当前环境中拿到带有注解信息的元素
  Set<? extends Element> elements = roundEnvironment.getElementsAnnotatedWith(MyRoute.class);   
 if (elements.isEmpty()) 
  {  
     return false; 
  } 
else {        
  ClassName mapClassName = ClassName.get("java.util", "Map", new String[0]);      
  ClassName stringClassName = ClassName.get("java.lang", "String", new String[0]);     
  TypeName mapTypeName = ParameterizedTypeName.get(mapClassName, new TypeName[]{stringClassName, stringClassName});     
   if (this.mRouteMapFieldSpec == null) {  
     ClassName hashMapClassName = ClassName.get("java.util", "HashMap", new String[0]);    
     TypeName hashMapTypeName = ParameterizedTypeName.get(hashMapClassName, new TypeName[]{stringClassName, stringClassName});      
     this.mRouteMapFieldSpec = FieldSpec.builder(mapTypeName, "ROUTE", new Modifier[0]);         
     this.mRouteMapFieldSpec.addModifiers(new Modifier[]{Modifier.PUBLIC, Modifier.STATIC}).initializer("new $T()", new Object[]{hashMapTypeName}).build();       
   }        
   com.squareup.javapoet.CodeBlock.Builder mapDataCodeBlock = CodeBlock.builder();    
    Iterator var17 = elements.iterator();    
    while(var17.hasNext()) {            
    Element e = (Element)var17.next();         
    if (e instanceof TypeElement) {            
         TypeElement typeElement = (TypeElement)e;       
         String className = typeElement.getQualifiedName().toString(); 
         MyRoute myRoute = (MyRoute)e.getAnnotation(MyRoute.class);    
         String pageType = myRoute.pageType();   
         String des = myRoute.des();   
         mapDataCodeBlock.add("//$S\nROUTE.put($S, $S);\n", new Object[]{des, pageType, className});       
     }      
  }      
  TypeSpec classType = TypeSpec.classBuilder(this.mModuleName + "RouteCatalog")
                               .addModifiers(new Modifier[]{Modifier.PUBLIC})
                               .addField(this.mRouteMapFieldSpec.build())
                               .addStaticBlock(mapDataCodeBlock.build()).build();      
  JavaFile javaFile = JavaFile.builder("com.example.route", classType).build();   
  File file = new File(this.mOutputDir);      
  try {        
    javaFile.writeTo(file);     
    } catch (IOException var15) {  
          var15.printStackTrace();  
      }  
      return true;  
  }}


总结 

编译时注解给项目带来很大的想象空间,可以将一些具有机械化,可以合并同类项等特征的代码通过注解自动生成。提升开发效率,增加代码容错率。在享受编译时注解带来的便利的同时,也会一定程度上拖累本地编译速度。不过这就是一个取舍的问题了。