Android编译期注解实践

什么是注解

注解(Annotation),也叫元数据(即描述数据的数据),一种代码级别的说明。

它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

起初作用基本类似是加强型的注释语言,为了编译检查,增强可阅读性,后续也可以用来生成代码,起到辅助项目的作用。

基本注解

内置注解

java内置注解,举个🌰:

@Override:让编译器检查被标记的方法,保证其重写了父类的某一个方法。此注解只能标记方法。源码如下:


@Target(ElementType.METHOD)

@Retention(RetentionPolicy.SOURCE)

public @interface Override {

}

元注解

用来给其他注解打标签的注解,即用来解释其他注解的注解,举两个🌰:

@Retention:用于指定被此元注解标注的注解的保留时长,即作用域

RetentionPolicy.SOURCE: :在源文件中有效(即源文件保留),例如@Override

RetentionPolicy.CLASS::注解信息保留在class文件中,但是虚拟机不会持有其信息,可用来编译期注解

RetentionPolicy.RUNTIME::注解信息保留在class文件中,而且虚拟机也会持有此注解信息,所以可以通过反射的方式获得注解信息,用来运行时注解

编译期注解: 在class文件中可用,能够存于编译之后的字节码之中,该注解的注册信息会保留在.java源码里和.class文件里,在执行的时候,会被Java虚拟机丢弃,不会加载到虚拟机中。

运行期注解:Java虚拟机在运行期保留注解信息,可以通过反射机制读取注解的信息(.java源码,.class文件和执行的时候都有注解的信息)

@Target:注解对象的作用范围

CONSTRUCTOR:用于描述构造器

FIELD:用于描述字段

LOCAL_VARIABLE:用于描述局部变量

METHOD:用于描述方法

PACKAGE:用于描述包

PARAMETER:用于描述参数

TYPE:用于描述类、接口(包括注解类型) 或enum声明

自定义注解

编译期注解总体结构

编译时注解 + APT (Annotation Processing Tool)+ JavaPoet(自定义Java源文件) + auto-service(处理器注册)

APT(Annotation Processing Tool) 是一种处理注释的工具, 它对源代码文件进行检测找出其中的 Annotation,使用 Annotation 进行额外的处理。处理器在处理 Annotation 时可以根据源文件中的 Annotation 生成额外的源文件和其它的文件 (文件具体内容由 Annotation 处理器的编写者决定),APT 还会编译生成的源文件和原来的源文件,将它们一起生成 class 文件。

JavaPoet是一款可以自动生成Java文件的第三方库,简洁易懂的API,让繁杂、重复的Java文件,自动化生成,简化流程。

auto-service:编译时注解处理器AbstractProcessor需要注册到JVM中。使用@Autoservice注解就完成了注册,替代以前的手动配置(🌰:在Java Library项目中,在resources资源文件夹下创建META-INF.services,然后在该路径下创建名为javax.annotation.processing.Processor的文件,在该文件中配置(处理器的完整路径,每行一个)需要启用的注解处理器)。

编译期注解处理流程

screenshot-20220904-203719.png

编译期注解实践

写了ShadowAptDemo,实现类似butterknife @BindView方法,根据注解自动获取view对象

c7101025-1714-43cc-bf29-2d232875ccbd.png

app:主项目

apt_knife_api:功能API,给主项目依赖调用,Android lib

apt_complier:注解处理器,AbstractProcessor是javax.annotation.processing包下的一个抽象类,Android平台是基于OpenJDK的,而OpenJDK中不包含AbstractProcessor的相关代码,所以是一个Java lib

apt_annotations:注解的定义,需要给到注解处理器以及app主项目使用,所以也是Java lib

JDK的版本要一致,不然编译出错

注解的定义

apt_annotations:


@Retention(RetentionPolicy.CLASS)

@Target(ElementType.FIELD)

public @interface ShadowBindView {

    int value();

}

定义一个编译期作用域的注解 ShadowBindView,用于描述字段

里面包含一个方法用于接收ResID

注解处理器

apt_complier:

需要配置相关三方库的依赖

dependencies{

    //处理器注册

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

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

    implementation 'com.google.auto:auto-common:0.10'

    //java文件生成

    implementation 'com.squareup:javapoet:1.11.1'

    //注解依赖

    implementation project(':apt_annotations')

}

自定义注解处理器

进行相关变量的初始化


@AutoService(Processor.class) //使用auto进行注册注解处理器,只需要在这里声明即可

@SupportedSourceVersion(SourceVersion.RELEASE_7) //指定jdk版本

//声明需要处理的注解

@SupportedAnnotationTypes("com.example.apt_annotations.ShadowBindView")

public class ShadowProcessor extends AbstractProcessor {

//Processor工具元素

private Elements processorElements;

//生成文件

private Filer filer;

//自定义注解类集合

private Map<String, AnnotationClass> mAnnotatedClassMap;

//注解处理器初始化的时候回调

@Override

public synchronized void init(ProcessingEnvironment processingEnv) {

    super.init(processingEnv);

    //返回包含用于操作Element的工具方法元素

    processorElements = processingEnv.getElementUtils();

    filer = processingEnv.getFiler();

    mAnnotatedClassMap = new HashMap<>();

}

....

AbstractProcessor是抽象类,有一个必须实现的抽象方法process方法,是注解处理器处理注解时候进行回调,需要在这里进行扫描和处理注解。

返回值逻辑

true,则这些注解已声明,且不要求后续Processor处理它们

false,则这些注解未声明,且可能要求后续Processor处理它们

RoundEnvironment,是指这轮处理注解所需要的元素信息,常用方法有:

getElementsAnnotatedWith,返回包含指定注解类型的元素的集合

errorRaised,上一轮注解处理器是否产生错误

Element,基础元素, 以下是继承它的元素

VariableElement ,代表成员变量

ExecutableElement ,代表类中的方法

TypeElement ,代表类

PackageElement ,代表Package


ShadowProcessor

@Override

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

System.out.println("process: " + roundEnv.toString());

    mAnnotatedClassMap.clear();

    //创建自定义注解处理类

    try {

        processBindView(roundEnv);

    } catch (Exception e) {

        e.printStackTrace();

    }

    //将自定义注解处理类,写入文件

    for (AnnotationClass annotationClass : mAnnotatedClassMap.values()) {

        try {

            annotationClass.generateFiler().writeTo(filer);

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

    return true;

}

private void processBindView(RoundEnvironment roundEnv) {

    //遍历包含ShadowBindView注解类型的元素的集合

    for (Element element : roundEnv.getElementsAnnotatedWith(ShadowBindView.class)) {

        //注解类,AnnotationClass后续会详细讲到

        AnnotationClass annotationClass = createAnnotationClass(element);

        //构建注解变量类,后续会详细讲到

        BindViewField bindViewField = new BindViewField(element);

        //添加对应的变量

        annotationClass.addField(bindViewField);

System.out.println("processBindView annotatedClass: " + annotationClass);

System.out.println("processBindView bindViewField: " + bindViewField);

    }

}

//组建注解类

private AnnotationClass createAnnotationClass(Element element) {

    //获取包含ShadowBindView注解的类元素

    TypeElement typeElement = (TypeElement) element.getEnclosingElement();

    String fullName = typeElement.getQualifiedName().toString();

System.out.println("createAnnotationClass typeElement: " + typeElement);

    AnnotationClass annotationClass = mAnnotatedClassMap.get(fullName);

    //集合中不存在,才添加到集合中,去重

    if (annotationClass == null) {

        //创建注解类 将类元素typeElement,工具元素processorElements传入

        annotationClass = new AnnotationClass(typeElement, processorElements);

        mAnnotatedClassMap.put(fullName, annotationClass);

    }

    return annotationClass;

}

创建注解变量类,用来存储注解变量数据


public class BindViewField {

    //变量元素

    private VariableElement mVariableElement;

    private int mResId;

    public BindViewField(Element element) {

        //判断类型,是否是变量元素

if (element.getKind() != ElementKind.FIELD) {

            return;

        }

        mVariableElement = (VariableElement) element;

        //获取自定义注解类型的变量

        ShadowBindView bindView = mVariableElement.getAnnotation(ShadowBindView.class);

        //获取view对象Rid

        mResId = bindView.value();

    }

    public int getResId() {

        return mResId;

    }

    public Name getFieldName() {

        return mVariableElement.getSimpleName();

    }

  //变量对应的类型

    public TypeMirror getFieldType() {

        return mVariableElement.asType();

    }

}

在注解类之前,先创建一个接口IViewBinder,它是用来衔接api和生成类文件的通信

就定义了两个需要实际使用的方法,后面构建类的时候会实现这个接口

apt_knife_api

public interface IViewBinder<T> {

    void bindView(T host);

    void unbindView(T host);

}

创建注解类,用来存储类信息和构建类文件


public class AnnotationClass {

static final ClassNameINTERFACE = ClassName.get("com.example.apt_knife_api", "IViewBinder");

    //注解类的BindView变量集合

    private final ArrayList<BindViewField> mFields;

    //注解类的元素

    private final TypeElement mTypeElement;

    //Processor元素

    private final Elements mProcessorElements;

    public AnnotationClass(TypeElement typeElement, Elements elements) {

        mFields = new ArrayList<>();

        mTypeElement = typeElement;

        mProcessorElements = elements;

    }

/**

    * 添加BindView变量

    *

    * @param field

    */

    public void addField(BindViewField field) {

        mFields.add(field);

    }

/**

    * 利用javaPoet生成对应的.java代码

    *

    * @return

    */

    public JavaFile generateFiler() {

        //生成java方法bindView

MethodSpec.Builder bindViewBuidler = MethodSpec.methodBuilder("bindView")

.addModifiers(Modifier.PUBLIC)//public

                .addAnnotation(Override.class)//接口的复写方法

.addParameter(TypeName.get(mTypeElement.asType()), "host");//参数

        //添加bindView方法的处理解析

        for (BindViewField field : mFields) {

            //方法参数

          bindViewBuidler.addStatement("host.$N = ($T)(host.findViewById($L))"

                    , field.getFieldName()

, ClassName.get(field.getFieldType())

                    , field.getResId());

        }

        //生成java方法unbindView:

MethodSpec.Builder unbindViewBuilder = MethodSpec.methodBuilder("unbindView")

.addModifiers(Modifier.PUBLIC)//public

                .addAnnotation(Override.class)//接口的复写方法

.addParameter(TypeName.get(mTypeElement.asType()), "host");//添加参数

        //添加unbindView方法的处理解析

        for (BindViewField field : mFields) {

      unbindViewBuilder.addStatement("host.$N = null", field.getFieldName());

        }

        //生成java的类文件(.java的文件),

TypeSpec injectClass = TypeSpec.classBuilder(mTypeElement.getSimpleName() + "$ShadowViewBinder")

.addModifiers(Modifier.PUBLIC)

.addSuperinterface(ParameterizedTypeName.get(INTERFACE,

TypeName.get(mTypeElement.asType())))//类实现的接口名IViewBinder

                .addMethod(bindViewBuidler.build())//添加bindView方法

                .addMethod(unbindViewBuilder.build())//添加unbindView方法

                .build();

        //添加包名 使用传入的工具类元素获取包名

        String packageName = mProcessorElements.getPackageOf(mTypeElement).getQualifiedName().toString();

        //生成JavaFile 然后会用JavaFile.writeTo(filer)写入文件

return JavaFile.builder(packageName, injectClass).build();

    }

}

主要有两个方法,一个添加构建好的变量类,用于生成类文件时候生成变量,最后一个是使用javaPoet生成类文件

使用javaPoet生成类文件的关键步骤注释都有,有个特别值得关注的点是构建方法代码的时候

//添加方法代码
bindViewBuidler.addStatement("host.$N = ($T)(host.findViewById($L))", field.getFieldName() ,
 ClassName.get(field.getFieldType()) , field.getResId());

可以看见在写配置代码的时候有多个不同的通配符,分别代表不同的含义,可以简单看一下源码

private void addArgument(String format, char c, Object arg) {

      switch (c) {

      //$N表示获取参数的name

        case 'N':

          this.args.add(argToName(arg));

          break;

      //$L表示字面意义,原样输出

        case 'L':

          this.args.add(argToLiteral(arg));

          break;

      //$S表示转成字符串

        case 'S':

          this.args.add(argToString(arg));

          break;

      //$T表示转成类型,并自动import

        case 'T':

          this.args.add(argToType(arg));

          break;

        default:

          throw new IllegalArgumentException(

              String.format("invalid format string: '%s'", format));

      }

    }

private String argToName(Object o) {

      if (o instanceof CharSequence) return o.toString();

      if (o instanceof ParameterSpec) return ((ParameterSpec) o).name;

      if (o instanceof FieldSpec) return ((FieldSpec) o).name;

      if (o instanceof MethodSpec) return ((MethodSpec) o).name;

      if (o instanceof TypeSpec) return ((TypeSpec) o).name;

      throw new IllegalArgumentException("expected name but was " + o);

    }

调用API

apt_knife_api:

创建主项目可以调用的api类ShadowKnife

public class ShadowKnife {

  //缓存类,避免重复反射创建

private static final MapbinderMap = new LinkedHashMap<>();

/**

    * 注解绑定

    *

    * @param host 表示注解 View 变量所在的类,也就是注解类 进行绑定的目标对象

    */

    public static void bind(Object host) {

        String className = host.getClass().getName();

        try {

            //看下对应的ViewBinder是否存在

IViewBinder binder =binderMap.get(className);

            if (binder == null) {

                //不存在则通过 反射 创建一个 然后存入缓存 这个类是通过javapoet生成的

Class aClass = Class.forName(className + "$ShadowViewBinder");

                binder = (IViewBinder) aClass.newInstance();

binderMap.put(className, binder);

            }

            //使用注解类的进行绑定

            if (binder != null) {

                binder.bindView(host);

            }

        } catch (Throwable e) {

            e.printStackTrace();

        }

    }

/**

    * 解除注解绑定

    *

    * @param host

    */

    public static void unBind(Object host) {

        String className = host.getClass().getName();

IViewBinder binder =binderMap.get(className);

        if (binder != null) {

            binder.unbindView(host);

        }

binderMap.remove(className);

    }

}

主项目使用

先进行模块依赖

dependencies{

    implementation fileTree(include: ['*.jar'], dir: 'libs')

    implementation 'com.android.support:appcompat-v7:28.0.0'

    //注解处理器依赖

    annotationProcessor project(':apt_complier')

    implementation project(':apt_annotations')

    implementation project(':apt_knife_api')

}

测试api代码

public class MainActivity extends AppCompatActivity {

@ShadowBindView(R.id.text1)

    TextView textView;

    @Override

    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        ShadowKnife.bind(this);

        textView.setOnClickListener(new View.OnClickListener() {

            @Override

            public void onClick(View v) {

            Toast.makeText(MainActivity.this, "test", Toast.LENGTH_LONG).show();

            }

        });

    }

    @Override

    protected void onDestroy() {

        super.onDestroy();

        ShadowKnife.unBind(this);

    }

}

然后进行clean,编译,看一下是否生成预期所想的代码

编译过程打印的日志

7bd686bd-0a44-4358-bab9-1cf8a6d14078.png

查看所生成的代码

695aaaba-22db-47ef-9c33-c05ced0e732f.png

大功告成!

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

推荐阅读更多精彩内容