仿照 ButterKnife 的 Android 注解实例

仿照 ButterKnife 的 Android 注解实例,第1张

概述什么是注解 java.lang.annotation,接口 Annotation,在JDK5.0及以后版本引入。 注解处理器是 javac 的一个工具,它用来在编译时扫描和处理注解(Annotatio 什么是注解

java.lang.annotation,接口 Annotation,在JDK5.0及以后版本引入。

注解处理器是 javac 的一个工具,它用来在编译时扫描和处理注解(Annotation)。你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。一个注解的注解处理器,以 Java 代码(或者编译过的字节码)作为输入,生成文件(通常是 .java 文件)作为输出。这些生成的 Java 代码是在生成的 .java 文件中,所以你不能修改已经存在的 Java 类,例如向已有的类中添加方法。这些生成的 Java 文件,会同其他普通的手动编写的 Java 源代码一样被 javac 编译。

基本的注解@ OverrIDe--限定重写父类方法@ Deprecated--标示已过时@ SuppressWarning--抑制编译器警告@ SafeVarargs--这货与Java7里面的堆污染有关,具体想了解的,到传送这里JDK的元注解

JDK除了提供上述的几种基本的注释外,还提供了几种注释,用于修饰其他的注解定义

@retention 这个是决定你注释存活的时间的,它包含一个RetationPolicy的值成员变量,用于指定它所修饰的注释保留时间,一般有:

Retationpolicy.CLASS:  编译器将把注释记录在类文件中,不过当Java的程序执行的时候,JVM将抛弃它。Retationpolicy.soURCE:  Annotation只保留在原代码中,当编译器编译的时候就会抛弃它。Retationpolicy.RUNTIME:  在Retationpolicy.CLASS的基础上,JVM执行的时候也不会抛弃它,所以我们一般在程序中可以通过反射来获得这个注解,然后进行处理。

@target 这个注解一般用来指定被修饰的注释修饰哪些元素,这个注解也包含一个值变量:

ElementType.ANNOTATION_TYPE:  指定该注释只能修饰注释。ElementType.CONSTRUCTOR:  指定只能修饰构造器。ElementType.FIELD:  指定只能成员变量。ElementType.LOCAL_VARIABLE:  指定只能修饰局部变量。ElementType.METHOD:  指定只能修饰方法。ElementType.PACKAGE:  指定只能修饰包定义。ElementType.ParaMETER:  指定只能修饰参数。ElementType.TYPE:  指定可以修饰类,接口,枚举定义。

@document 这个注解修饰的注释类可以被 javadoc 的工具提取成文档

@inherited 被他修饰的注解具有继承性


自定义注释

上面讲了一些JDK自带的注释,那么我们现在就可以用这些JDK自带的注释来实现一些我们想要的功能。先一步一步地模仿 butterknife 的实现吧。

定义一个注解:

@Target(ElementType.FIELD) // 用于成员变量@Retention(RetentionPolicy.CLASS) 注解保留在 class 文件,当Java的程序执行的时候,JVM将抛弃它public @interface BindVIEw {    int value();}
注解定义的方式就是 @interface 和接口的定义方式就少一个 @ 哦,不要搞混了。里面有一个变量值时,就是我们使用的时候 @BindVIEw (R.ID.textVIEw) 指定的 R.ID.textVIEw ID,旨在自动注入 vIEw 的 ID。注解处理器

先来说下注解处理器 AbstractProcessor。它是 javac 的一个工具,用来在编译时扫描和处理注解 Annotation,你可以自定义注解,并注册到相应的注解处理器,由注解处理器来处理你的注解。

一个注解的注解处理器,以 Java 代码(或者编译过的字节码)作为输入,生成文件(通常是.java文件)作为输出。这些由注解器生成的.java代码和普通的.java一样,可以被javac编译。

因为 AbstractProcessor 是 javac 中的一个工具,所以在 AndroID 的工程下没法直接调用。

新建一个 java library

在 build.gradle 引入相关 jar :

apply plugin: 'java-library'dependencIEs {    implementation filetree(dir: 'libs',include: ['*.jar'])    API 'com.squareup:javapoet:1.7.0'    API 'com.Google.auto.service:auto-service:1.0-rc2'    API project(':lib-annotation')}sourceCompatibility = "1.7"targetCompatibility = "1.7"指定编译的编码tasks.withType(JavaCompile){    options.enCoding = "UTF-8"}

javapoet 和 auto-service 后面会讲到,这两个在注解处理器中有着极大的作用。

引入之后,就开始编写注解处理器了。

@autoService(Processor.class)public class CustomProcessor extends AbstractProcessor {    private static final String TAG = "CustomProcessor";    // 文件相关的辅助类    private filer mfiler;    元素相关的辅助类     Elements mElements;    // 元素相关的辅助类     Elements mElementUtils;    /**     * 解析的目标注解集合     */    private Map<String,AnnotatedClass> mAnnotatedClassMap = new HashMap<>();    @OverrIDe    synchronized voID init(ProcessingEnvironment processingEnvironment) {        super.init(processingEnvironment);        mElementUtils = processingEnvironment.getElementUtils();        mfiler = processingEnvironment.getfiler();    }    核心处理逻辑,相当于java中的主函数main(),你需要在这里编写你自己定义的注解的处理逻辑    返回值 true时表示当前处理,不允许后续的注解器处理    @OverrIDe    boolean process(Set<? extends TypeElement> set,RoundEnvironment roundEnvironment) {        mAnnotatedClassMap.clear();        try {            processBindVIEw(roundEnvironment);        } catch (IllegalArgumentException e) {            return true;        }         {            for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {                annotatedClass.generateFinder().writeto(mfiler);            }        }  (Exception e) {            e.printstacktrace();        }        ;    }    @OverrIDe    public Set<String> getSupportedAnnotationTypes() {        Set<String> types = new linkedHashSet<>();
     // 标明该注解处理器是为了处理 BindVIEw 注解的 types.add(BindVIEw.
.getCanonicalname()); return types; } processBindVIEw(RoundEnvironment roundEnv) { for (Element element : roundEnv.getElementsAnnotateDWith(BindVIEw.)) { element = tv1; AnnotatedClass annotatedClass = getAnnotatedClass(element); BindVIEwFIEld fIEld = new BindVIEwFIEld(element); annotatedClass.addFIEld(fIEld);
       // 通过上面方法调用,可以获取到注解元素,以及和注解元素相关的类名,通过注解元素获得被注解的成员变量名,后续会对其进行初始化 } }
AnnotatedClass getAnnotatedClass(Element element) {
     // 通过注解元素获取其封装类,获得类的引用 TypeElement encloseElement
= (TypeElement) element.getEnclosingElement(); encloseElement.getSimplename() = MainActivity; String fullClassname = encloseElement.getQualifIEdname().toString(); com.sjq.recycletest.MainActivity AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullClassname); if (annotatedClass == null) {
       // 存到map当中,不用每次都生成一次,这样一个类里面有多个注解的时候,可以加快处理速度 annotatedClass
= AnnotatedClass(encloseElement,mElementUtils); mAnnotatedClassMap.put(fullClassname,annotatedClass); } annotatedClass; } @OverrIDe public SourceVersion getSupportedSourceVersion() { SourceVersion.latestSupported(); }}

init(ProcessingEnvironment processingEnvironment):每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,它会被注解处理工具调用,并输入 ProcessingEnviroment 参数.ProcessingEnviroment 提供很多有用的工具类 Types 和 filer。后面我们将看到详细的内容。

process(Set<? extends TypeElement> set,RoundEnvironment roundEnvironment):这相当于每个处理器的主函数main()。你在这里写你的扫描,评估和处理注解的代码,以及生成Java文件。输入参数 RoundEnviroment,可以让你查询出包含特定注解的被注解元素。后面我们将看到详细的内容。

getSupportedAnnotationTypes():这里你必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称。换​​句话说,你在这里定义你的注解处理器注册到哪些注解上。

getSupportedSourceVersion():用来指定你使用的Java版本。通常这里返回SourceVersion.latestSupported()。然而,如果你有足够的理由只支持Java 7 的话,你也可以返回 SourceVersion.RELEASE_7。推荐你使用前者。

在类前面,使用了 autoService 注解,这个在 build.gradle 中已经引入。autoService 注解处理器是 Google 开发的,用来生成 meta-inf/services/ javax.annotation.processing.Processor 文件的,你只需要在你定义的注解处理器上添加 @autoService(Processor.class) 就可以了,简直不能再方便了。注解器处理流程

首先我们先简单的说明一下 porcess 的处理流程:

遍历 env,得到我们需要的元素列表

将元素列表封装成对象,方便之后的处理,比如获取元素的各种属性等

通过 JavaPoet 库将对象以我们期望的形式生成 java 文件,来处理注解

第一步:遍历env,得到我们需要的元素列
if (element.getKind() == ElementKind.FEILD) {         显示转换元素类型        TypeElement typeElement = (TypeElement) element;         输出元素名称        System.out.println(typeElement.getSimplename());         输出注解属性值        System.out.println(typeElement.getAnnotation(BindVIEw.).value());    }}

上面的代码和 processBindVIEw 的代码是一样的。判断元素类型,在进一步处理。有些注解可能对类和方法是同时生效的,这时候,判断类型分别处理就显得非常有必要了。

getElementsAnnotateDWith 能够获取到添加该注解的所有元素列表


第二步:将元素列表封装成对象,方便之后的处理

其实不进行封装也是可以的,但是这样当我们在使用的时候,可能就需要在不同的地方写很多重复的代码,为此,可以进一步封装,当我们需要获取元素属性的时候,直接调用相关方法即可。

新建类 BindVIEwFIEld.class 用来保存自定义注解 BindVIEw 相关的属性,后续需要元素上的信息都可以从该类获取。

 BindVIEwFIEld {     VariableElement mFIEldElement;     mResID;    public BindVIEwFIEld(Element element) throws IllegalArgumentException {        if (element.getKind() != ElementKind.FIELD) {            throw new IllegalArgumentException(String.format("Only fIEld can be annotated with @%s",BindVIEw..getSimplename()));        }
     // 该注解用于成员变量的,因此需要进行转化 mFIEldElement
= (VariableElement) element;
     // 在进一步转化为注解类型 BindVIEw bindVIEw
= mFIEldElement.getAnnotation(BindVIEw.); mResID = bindVIEw.value(); if (mResID < 0) { new IllegalArgumentException(String.format("value() in %s for fIEld % is not valID".getSimplename(),mFIEldElement.getSimplename())); } } name getFIEldname() { mFIEldElement.getSimplename(); } getResID() { mResID; } TypeMirror getFIEldType() { mFIEldElement.asType(); }}

上述的 BindVIEwFIEld 只能表示一个自定义注解 bindVIEw 对象。很多时候,会同时存在很多其他注解,每一种注解都需要一个单独对象来管理属性。而一个类中很可能会有多个自定义注解,因此,对于在同一个类里面的注解,我们可以创建一个对象来进行管理,这就是 Annotation.class。

 AnnotatedClass {         TypeElement mClassElement;    类内的注解变量    public List<BindVIEwFIEld> mfiled;    元素帮助类     AnnotatedClass(TypeElement classElement,Elements elementUtils) {        this.mClassElement = classElement;        this.mElementUtils = elementUtils;        this.mfiled = new ArrayList<>();    }    添加注解变量     addFIEld(BindVIEwFIEld fIEld) {        mfiled.add(fIEld);    }    获取包名     String getPackagename(TypeElement type) {         mElementUtils.getPackageOf(type).getQualifIEdname().toString();    }    获取类名    static String getClassname(TypeElement type,String packagename) {        int packageLen = packagename.length() + 1;         type.getQualifIEdname().toString() = com.sjq.recycletest.MainActivity        return type.getQualifIEdname().toString().substring(packageLen).replace('.','$');    }}

第三步: 通过 JavaPoet 库将对象以我们期望的形式生成 java 文件

通过上述两步成功获取了自定义注解的元素对象,但是还是缺少一步关键的步骤,缺少一步 findVIEwByID(),实际上 ButterKnife 这个很出名的库也并没有省略 findVIEwByID()这一个步骤,只是在编译的时候,在 build/generated/source/apt/deBUG 下生成了一个文件,帮忙执行了findVIEwByID()这一行为而已。

同样的,我们这里也需要生成一个 java 文件,采用的是 JavaPoet 这个库。具体的使用 参考链接

 Javafile generateFinder() {        构建 inject 方法        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")                .addModifIErs(ModifIEr.PUBliC)                 .addAnnotation(OverrIDe.)                .addParameter(Typename.get(mClassElement.asType()),"host")                .addParameter(Utils.FINDER,"finder");        inject函数内的核心逻辑, host.btn1=(button)finder.findVIEw(source,2131427450);  ----生成代码         host.$N=($T)finder.findVIEw(source,$L)                 ----原始代码         对比就会发现这里执行了实际的findVIEwByID绑定事件         (BindVIEwFIEld fIEld : mfiled) {            methodBuilder.addStatement("host.$N=($T)finder.findVIEw(source,$L)" com.sjq.recycletest        String classname = getClassname(mClassElement,packagename);        Classname bindClassname = Classname.get(packagename,classname);  bindClassname.toString()  com.sjq.recycletest.MainActivity        构建类对象,注意此处的 $$Injector,生成的类名是由我们自己来控制的        typespec finderClass = typespec.classBuilder(bindClassname.simplename() + "$$Injector")                .addModifIErs(ModifIEr.PUBliC)                .addSuperinterface(ParameterizedTypename.get(Utils.INJECTOR,Typename.get(mClassElement.asType())))   继承接口                .addMethod(methodBuilder.build()) // 添加方法                .build();         Javafile.builder(packagename,finderClass).build();    }

上述代码先生成一个方法名,再添加函数体,接着把这个方法添加到一个类当中,这个类名是按照一定的规则拼接的,这也是后面采用反射获取生成类名的关键所在。

到这里,大部分逻辑都已实现,用来绑定控件的辅助类也已通关 JavaPoet 生成了,只差最后一步,宿主注册,如同 ButterKnife 一般,ButterKnife.bind(this)

编写调用接口

在 annotation-API 下新建 androID library。

注入接口 Injector

 最终会调用该方法来实现注解。

interface Injector<T> {     inject(T host,Object source,Finder finder);}
宿主通用接口Finder(方便之后扩展到vIEw和fragment)
 Finder {    Context getContext(Object source);    VIEw findVIEw(Object source,1)"> ID);}
activity实现类 ActivityFinder

 顾名思义,就是通过 activity 的 findVIEwByID 来找到某个 vIEw。

class ActivityFinder implements Finder{    @OverrIDe     Context getContext(Object source) {         (Activity) source;    }    @OverrIDe    public VIEw findVIEw(Object source, ID) {         ((Activity) (source)).findVIEwByID(ID);    }}
核心实现类 ButterKnife
 ButterKnife {  //     final ActivityFinder finder =  ActivityFinder();
  // 用于存储已经绑定的class,避免重复绑定
static Map<String,Injector> FINDER_MAP = (); bind(Activity activity) { bind(activity,activity); } bind(Object host,Object source) { bind(host,source,finder); } host.getClass().getname(); { Injector injector = FINDER_MAP.get(classname); if (injector == ) {
          // 此处拿到的类名就是通过注解生成的中间处理类,即 MainActivity$$Injector Class
<?> finderClass = Class.forname(classname + "$$Injector");
          // 通过反射拿到class实例 injector
= (Injector) finderClass.newInstance(); FINDER_MAP.put(classname,injector); } injector.inject(host,finder); } (Exception e) { e.printstacktrace(); } }}

 在 bind 方法内部,通过一定的规则拼接最后生成的 .java 文件类名,然后通过反射的方法拿到实例,最后,调用 injector.inject 的方法来完成初始化,该方法就是得通过注解来生成的。

主工程下调用 

对应的按钮可以直接使用,不需要findVIEwByID(),这样我们可以少写很多同样代码,逻辑上也变得非常清楚。

class MainActivity  AppCompatActivity {    @BindVIEw(R.ID.annotation_tv)     TextVIEw tv1;        @OverrIDe    protected  onCreate(Bundle savedInstanceState) {        .onCreate(savedInstanceState);        setContentVIEw(R.layout.activity_main);
     // tv1 的初始化过程就是在bind过程中完成的 ButterKnife.bind(
this); tv1.setText("annotation_demo"); }}

 选择 build 下的 make project,会进行编译:

最终生成的类名如下:

class MainActivity$$Injector implements Injector<MainActivity> {  @OverrIDe  voID inject(final MainActivity host,Finder finder) {
  // 最终还是通过 findVIEwByID 来对 tv1 进行初始化 host.tv1
=(TextVIEw)finder.findVIEw(source,2131165300); }}

 到此,该实例讲解结束。

总结:

下面会对上面的实例的思想进行总结,方便大家进一步理解其实现原理:

1、先看代码层面的。可以看到在主工程先对成员变量 tv1 添加了注解,获得了 vIEw 的 ID;其次先调用 bind 方法,获取到 activity 实例。接着就开始调用 tv1.setText。可是有没有发现,tv1 没有初始化。其初始化过程就是在 bind 这个过程当中。

2、注解生成代码过程。通过获取成员变量所属的类,根据类名和命名规则获取最后注解会生成的类名,通过反射的形式,调用其中的 inject 方法。inject 方法中会有 activity 的 this 引用,通过 findVIEwByID 方法,即可为 tv1 初始化。这样后面调用  tv1.setText 就不会出现空指针了。

项目源码:https://download.csdn.net/download/szengjiaqi/10629127

不能下载的,留下邮箱,发你

参考文献:

1、Android的编译时注解APT实战(AbstractProcessor)

2、浅谈Android下的注解

 

总结:其实你会发现最终被注解的 ID 也还是通过 findVIEwByID 方法来查找资源的。假设我们不使用注解,将生成的类放到外层来,直接使用的话,这样子就得在activity中引入更多的类,代码上也会更加混乱,不好维护。但是理解上可能比注解更好理解吧。

在假设,如果我们不是在 activty 使用注解呢,对于一般的 vIEw 或者 其他类来使用的话可以吗?可以的,你只需要传入持有 findVIEwByID 对象即可。比如 rootvIEw.

使用注解的方式,让代码逻辑变得更加清楚,依赖也会降低。但是对于不了解的注解的同学,可能代码看起来会比较困难,但是使用很容易。

最后,我们想想,什么时候推荐使用注解呢?个人觉得是重复工作比较多的时候,这时候是需要注解的,因为工作重复,注解可以很方便就完成。

 

总结

以上是内存溢出为你收集整理的仿照 ButterKnife 的 Android 注解实例全部内容,希望文章能够帮你解决仿照 ButterKnife 的 Android 注解实例所遇到的程序开发问题。

如果觉得内存溢出网站内容还不错,欢迎将内存溢出网站推荐给程序员好友。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1121334.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-29
下一篇 2022-05-29

发表评论

登录后才能评论

评论列表(0条)

保存