Android注解框架

简介

注解处理器(Annotation Processor)是javac内置的一个用于编译时扫描和处理注解(Annotation)的工具。也就是说,在编译阶段我们就可以获取到源文件内注解(Annotation)相关内容。

作用

由于注解处理器是在编译时期工作,因此我们可以在编译时期通过注解处理器进行某些行为。比较常用的就是在编译时期访问注解的数据,然后动态生成java文件。典型案例就是butterknife注解工具类。

Annotation Processor用法

使用Annotation Processor之前我们先来简单了解一下元注解。

注解

元注解是java提供的最基础的注解,负责注解其他的注解,它们是用来解释其他注解的,在java.lang.annotation包下。
元注解有:
@Retention:注解保留的生命周期
@Target:注解对象的作用范围。
@Inherited:@Inherited标明所修饰的注解,在所作用的类上,是否可以被继承。
@Documented:如其名,javadoc的工具文档化,基本不关心。

@Retention
Retention标明了注解的生命周期,对应RetentionPolicy的枚举,表示注解在何时生效:
SOURCE:只在源码中有效,编译时抛弃。
CLASS:编译class文件时生效。
RUNTIME:运行时才生效。

@Target
Target标明了注解的适用范围,对应ElementType枚举,明确了注解的有效范围。
TYPE:类、接口、枚举、注解类型。
FIELD:成员变量
METHOD:方法
PARAMETER:参数
CONSTRUCTOR:构造器
LOCAL_VARIABLE:局部变量
ANNOTATION_TYPE:注解类型
PACKAGE:
TYPE_PARAMETER:类型参数
TYPE_USE:类型使用声明

@Inherited
注解所作用的类,在继承时默认无法继承父类的注解。除非注解声明了 @Inherited。同时Inherited声明出来的注,只对类有效,对方法/属性无效。

自定义注解

了解了元注解后,看看如何实现和使用自定义注解。下面实现BindView 注解作用在成员变量上面,并且只在源码中生效,编译时丢弃。

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

更改适用范围和生命周期只需要更改对应Target和Retention注解的值即可。

Annotation Processor使用

使用有两个步骤:

1.自定义注解类型。
2.注册注解处理器完成对注解的处理

步骤一前面已经看过不在多说,下面我们直接来看第二个步骤:
注册注解处理器前,我们需要完成注解处理器的编码工作:

1.继承AbstractProcessor类并重写对应的方法
public class CusComplier extends AbstractProcessor {
    
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;
    private Types typeUtils;

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

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //Element代表程序的元素,例如包、类、方法。
        elementUtils = processingEnvironment.getElementUtils();
       //文件生成处理的类
        filer = processingEnvironment.getFiler();
       //日志信息
        messager = processingEnvironment.getMessager();
       ////处理TypeMirror的工具类,用于取类信息
        typeUtils = processingEnvironment.getTypeUtils();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> set = new LinkedHashSet<>();
        set.add(BindView.class.getCanonicalName());
        return set;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
2.注册注解处理器

注册方式有2中,第一种是手动在\resources\META-INF.services\javax.annotation.processing.Processor文件下输入全名称的处理器名。
第二种是比较推荐的自动注册方式
gradle中引入一下依赖

implementation 'com.google.auto.service:auto-service:1.0-rc6'

然后在类名前加入一下注解,如果不想重写getSupportedSourceVersiongetSupportedAnnotationTypes,也可以通过注解方式完成。

@AutoService(Processor.class)//自动注册
@SupportedAnnotationTypes({"com.xj.annotation.BindView","com.xj.annotation.OnClick"})//支持注解类型
@SupportedSourceVersion(SourceVersion.RELEASE_7)//支持版本
public class CusComplier extends AbstractProcessor {}

编译时,会自动去查找AutoService注解,从而完成注册。
下面我们来看一下process方法:

public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) 

process方法是处理器实际处理逻辑入口,该方法能够获取到支持注解的所有数据。其中set是支持注解类型的集合,RoundEnvironment 是所有扫描到的元素的数据集合。因此我可以通过这两个参数操作注解数据,大致操作的流程如下:

1.遍历获取到的源码集合,获取需要的注解数据。
2.判断元素是否满足条件
3.组织数据结构相关参数
4.依据元素j组织ava类型文件,并写入到文件中
5.相关的错误信息。

下面是仿BindView的注解处理类:

@AutoService(Processor.class)
@SupportedAnnotationTypes({"com.xj.annotation.BindView","com.xj.annotation.OnClick"})
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class AnnotationComplier extends AbstractProcessor {
    ...
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        messager.printMessage(Diagnostic.Kind.NOTE,"process");
        //支持的注解类型
        for (TypeElement typeElement: set){
            messager.printMessage(Diagnostic.Kind.NOTE,"process"+typeElement.toString());

            //返回使用对应注解的元素
            Set<? extends Element> elementsAnnotateds = roundEnvironment.getElementsAnnotatedWith(typeElement);
            for (Element element : elementsAnnotateds){

                //返回元素的里层元素 此时返回的是类元素
                Element enclosingElement = element.getEnclosingElement();
                TypeMirror typeMirror = enclosingElement.asType();

                //获取类名
                String className = typeMirror.toString();

                //业务处理代码 将元素依据类进行分类,便于生成代码的时候直接写在同一个类中
                ArrayList<Element> elements = mapElements.get(className);
                if (null == elements){
                    elements = new ArrayList<>();
                    elements.add(element);
                    mapElements.put(className,elements);
                } else {
                    elements.add(element);
                }
            }
        }
        writeJavaClass();
        return false;
    }

    /**
     *编写java代码
     */
    private void writeJavaClass() {
        //APT 方式写代码
        Set<Map.Entry<String, ArrayList<Element>>> mapSet = mapElements.entrySet();
        for (Map.Entry<String, ArrayList<Element>> entrys : mapSet){
            String className = entrys.getKey();
            ArrayList<Element> elements = entrys.getValue();

            TypeElement classElement = elementUtils.getTypeElement(className);
            String pageName = elementUtils.getPackageOf(classElement).getQualifiedName().toString();
            StringBuilder builder = new StringBuilder();

            builder.append("package ").append(pageName).append(";").append("\n").append("\n");
            builder.append("import android.support.annotation.UiThread;").append("\n");
            builder.append("import android.view.View;").append("\n");
            builder.append("import java.lang.Override;").append("\n");
            builder.append("import com.xj.annotationcore.Unbinder;").append("\n").append("\n");

            builder.append("public ").append("class ").append(classElement.getSimpleName()).append("_ViewBinding").append(" implements")
                    .append(" Unbinder,").append(" View.OnClickListener").append("{").append("\n").append("\n");

            builder.append("private").append(" ").append(classElement.getSimpleName()).append(" ").append("target;").append("\n").append("\n");
            builder.append("@UiThread").append("\n");
            builder.append("public void ").append(classElement.getSimpleName()).append("_ViewBinding(").append("final ").append(classElement.getSimpleName())
                    .append(" target , ").append("View source) {").append("\n");
            builder.append("this.target = target;").append("\n");
            ArrayList<String> mElementName = new ArrayList<>();
            //依据元素生成不同的代码
            for (Element element: elements){
                messager.printMessage(Diagnostic.Kind.NOTE,"======write======"+element.getSimpleName());

                OnClick onClick = element.getAnnotation(OnClick.class);
                if (null != onClick){
                    int[] ids = onClick.value();
                    for (int id : ids ){
                        builder.append("source.findViewById(").append(id).append(")")
                                .append(".setOnClickListener(this);").append("\n");
                    }
                }
                BindView bindView = element.getAnnotation(BindView.class);
                if (null != bindView){
                    messager.printMessage(Diagnostic.Kind.NOTE,"======write======bindView ");
                    //注解的值,此时就是id
                    int id = bindView.value();
                    //元素名称,此时就是生命的变量名称
                    Name simpleName = element.getSimpleName();
                    mElementName.add(simpleName.toString());
                    builder.append("target.").append(simpleName).append(" = ").append("source.findViewById(").append(id).append(")").append(";").append("\n");
                }
            }
            builder.append("}").append("\n");
            //onClick
            builder.append("@Override\n").append("public void onClick(View view) {\n").append("this.target.onClick(view);\n").append("}").append("\n");
            //unbind
            builder.append("public void unbind() {").append("\n").append("MainActivity target = this.target;").append("\n")
            .append("this.target = null;").append("\n");
            for (String name : mElementName){
                builder.append("target.").append(name).append(" = null;").append("\n");
            }
            builder.append("}");
            builder.append("}");
            try {
                messager.printMessage(Diagnostic.Kind.NOTE,"string "+builder.toString());
                //新建类,通过Writer将写好的builder代码字串输入进去从而完成代码书写、
                JavaFileObject object = filer.createSourceFile(pageName+"."+classElement.getSimpleName()+"_ViewBinding");
                Writer writer = object.openWriter();
                writer.append(builder.toString());
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

其中生成代码过程比较繁琐,也可使用javapoet工具来替代,javapoet工具是封装了生成代码的过程,极大的减少编写压力。javapoet中简单代码介绍:

//节点元素,用来生成类
typeSpecBuilder = TypeSpec.classBuilder(classTypeElement.getSimpleName() + "_ViewBinding");
//生成参数
ParameterSpec targetParameterSpec = ParameterSpec
                        .builder(ClassName.get(classTypeElement), "target", Modifier.FINAL)
                        .build();
//生成构造函数
constructorMethodBuilder = MethodSpec.constructorBuilder()
           .addParameter(targetParameterSpec)
           .addParameter(viewParameterSpec)
           .addAnnotation(ClassName.bestGuess("android.support.annotation.UiThread"))
            .addModifiers(Modifier.PUBLIC);
// 添加代码块
constructorMethodBuilder.addStatement("this.target = target");
//普通方法生成
MethodSpec.methodBuilder("unbind")
                        .addModifiers(Modifier.PUBLIC);
 //生成MainActivity_ViewBinding类
TypeSpec typeSpec = typeSpecBuilder.addField(ClassName.get(classTypeElement), "target", Modifier.PRIVATE)
                        .addMethod(constructor)
                        .addMethod(unbindMethodSpec.build())
                        .addSuperinterface(ClassName.bestGuess("com.xj.annotationcore.Unbinder"))
                        .addModifiers(Modifier.PUBLIC)
                        .addSuperinterface(ClassName.bestGuess("View.OnClickListener"))
                        .build();
JavaFile javaFile = JavaFile.builder(packageName, typeSpec)
                        .build();
                try {
                    //写入java文件
                    javaFile.writeTo(filer);
                } catch (IOException e) {
                    e.printStackTrace();
                }

详细参数介绍请查看GitHub相关库信息。

Butterknife总结

使用Butterknife

1.gradle引入Butterknife库
dependencies {
  implementation 'com.jakewharton:butterknife:10.2.0'
  annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
}
2.注册当前界面到Butterknife
ButterKnife.bind(this);

bind参数有activity,view,object...

3.使用
 @BindView(R.id.tv)
 TextView textView;
 @OnClick({R.id.tv})
 public void onClick(View view){}
 等等
4.解绑
 Unbinder bind = ButterKnife.bind(this);
 bind.unbind();
5.原理

Butterknife基于APT技术,在编译时处理对应的注解信息动态生成相应的代码写入到xxx__ViewBinding文件中。然后通过bind方法完成绑定。

@BindView(R.id.tv)

上面的注解会在对应文件中生成:

View view = source.findViewById(id);

动态生成的方法和原生方式并没有不同,但因为是自动生成的所以极大地简化我们书写这类重复的代码。

  ButterKnife.bind(this);

  static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();

  @NonNull @UiThread
  public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return bind(target, sourceView);
  }

  @NonNull @UiThread
  public static Unbinder bind(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
    ...
    if (constructor == null) {
      return Unbinder.EMPTY;
    }
   return constructor.newInstance(target, source);
   ...
    }
  }

  @Nullable @CheckResult @UiThread
  private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null || BINDINGS.containsKey(cls)) {
      if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      return bindingCtor;
    }
    String clsName = cls.getName();
    try {
      Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
      bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
    } ...
    INDINGS.put(cls, bindingCtor);
    return bindingCtor;
  }

Bind方法调用流程:

1.从静态Map中获取构造方法实例,有则说明已经绑定过直接返回。没有则说明需要重新绑定,因此会获取xxx_ViewBinding类,获取其构造方法实例,设置到Map中并返回。
2.拿到构造方法实例,通过反射传入界面与当前根布局,完成绑定。

自动生成的类中获取到了对应的界面与根布局就可以做它想做的操作了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容