butterknife源码分析:如何处理注解—反射与注解处理器

butterknife源码分析系列:
谈一谈Java的注解
http://blog.csdn.net/u012933743/article/details/54909590
如何处理注解—反射与注解处理器
http://blog.csdn.net/u012933743/article/details/54972050
代码分析
http://blog.csdn.net/u012933743/article/details/64437988

有时,我们希望在程序运行或者编译时能获取注解的值,并做一些处理。如何想运行时处理注解,需要通过反射(Reflect),此时要将@Retention设置为RUNTIME;如果是编译时,需要用到注解处理器(AbstractProcessor),并将@Retention需要设置为CLASS。


反射

反射机制允许在运行时发现和使用类的信息。Class类与java.lang.reflect一起对反射的概念进行了支持,允许在运行时利用反射达到:

  • 判断任意一个对象所属的类;
  • 构造任意一个类的对象;
  • 判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);
  • 调用任意一个对象的方法

其实我觉得反射是个很bug般的存在,连private的都能进行操作。不过,反射会对性能有影响,因为JVM无法对反射部分的代码进行优化。在http://docs.oracle.com/javase/tutorial/reflect/中有说到:

Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.

这里举个简单的栗子对反射的运用进行介绍。

注解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReflectParam {

    String value() default "nobody";

}

反射是在运行中获取类的信息,因此需要将注解的声明周期设置为RUNTIME,@Retention(RetentionPolicy.RUNTIME)。

Main函数

public class ReflectMain {

    @ReflectParam("jerry")
    public String userName;

    public static void main(String[] args) {
        ReflectMain main = new ReflectMain();
        ReflectParam annotation = null;
        try {
            Class clazz = main.getClass();
            Field field = clazz.getField("userName");
            annotation = field.getAnnotation(ReflectParam.class);
            System.out.print(annotation.value());
        } catch (NoSuchFieldException e) {
            System.out.print("no such field");
        }
    }

}

ReflectParam是修饰域变量的注解(@Target(ElementType.FIELD)),因此需要先通过反射获取Field。Field、Method、Constructor内都有getAnnotation来获取注解的方法。获取到ReflectParam注解后,直接通过注解内定义的方法来获取注解的值,这里我们定义的方法是value()。

输出结果如下, 因此通过反射可以获取到注解的值并做一些处理。

jerry
Process finished with exit code 0

在写这篇博客时,突然想到比较二的问题,这里也跟大家分享下。我们的ReflectParam注解内的value方法是没有用权限关键词修饰的,那位什么在Main函数内可以直接用annotation.value()来调用呢?不加权限关键词的默认难道不是default(包权限)吗?

其实这是一个陷阱,@interface默认继承自java.lang.annotation.Annotation接口,接口的默认权限是public才对。

使用反射来模仿ButterKnife

在Android开发中,每个Activity都会有十几行甚至几十行findViewById的代码,这些操作繁琐但又不可免。“黄油刀”框架的出现就解决了这个问题。

这里使用反射来模仿下ButterKnife来简化这些代码。当然ButterKnife不是用反射的,要不然这框架怎么如此出名?

先定义个BindView注解:

@Retention(RUNTIME)
@Target(FIELD)
public @interface BindView {
    /** View ID to which the field will be bound. */
    int value();
}

调用方法:

public class MainActivity extends Activity {

    @BindView(R.id.tv_title)
    private TextView tvTitle;

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

        NewerKnife.bind(this);
        tvTitle.setText("after bind view");
    }

}

在声明TextView时使用BindView注解,然后setContentView后,使用NewerKnife.bind(this),这样就可以避免繁琐的findViewById操作。

findViewById的操作都在NewerKnife.bind(this)这里面使用反射解决了。下面是实现的代码:


public class NewerKnife {

    public static void bind(Activity activity) {
        Class clazz = activity.getClass();
        // 获取类的所有域变量
        Field[] fields = clazz.getFields();
        for (Field field : fields) {
            // 获取BindView注解
            BindView bindView = field.getAnnotation(BindView.class);
            if (bindView != null) {
                // 存在注解,获取int值,ex:R.id.tv_title
                int viewId = bindView.value();
                try {
                    // 获取findViewById的Method实例
                    Method findViewMethod = clazz.getMethod("findViewById", int.class);
                    // 调用findViewMethod来获得View
                    Object view = findViewMethod.invoke(activity, viewId);
                    // tvTitle-private,设置可操作性
                    field.setAccessible(true);
                    // 将结果赋值给field
                    field.set(activity, view);
                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                } catch (InvocationTargetException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

}

通过上面简单的代码,我们就用反射实现了ButterKnife的BindView功能,有兴趣的读者可以实现下OnClick功能。


注解处理器-AbstractProcessor

前面说到的反射是在运行时处理注解,有些人可能担心反射对应用的性能有大的影响。其实通过注解处理器可以在编译时扫描注解,处理注解,包括生成代码等,这样就不会对应用的性能造成影响。前面我们提到的ButterKnife就是通过注解处理器来的。

Element与TypeMirror

在介绍AbstractProcessor前,先介绍Element与TypeMirror俩个接口。

Element

在Java中,将源代码看成Element,Element的子接口有ExecutableElement, PackageElement, Parameterizable, QualifiedNameable, TypeElement, TypeParameterElement, VariableElement。通过下面的栗子就可以直观的理解:

package com.jerry.practice;    // PackageElement

public class Foo {        // TypeElement

    private int a;      // VariableElement
    private Foo b;  // VariableElement

    public Foo () {}    // ExecuteableElement

    public void setA (  // ExecuteableElement
                     int newA   // TypeParameterElement
                     ) {}
}

TypeMirror

TypeMirror表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void 的伪类型。

看到这里,可能读者会跟我一样分不清Element和TypeMirror的区别。我一开始也很混淆,后来网上查了很久,又看了下编程思想的介绍,总结如下。

Element代表的是源代码。举个栗子,TypeElement代表源代码中的类或接口的元素,是未经编译的。也就是说我们无法根据TypeElement获取类的信息,例如类里面的方法、域、超类等信息。而通过TypeElement.asType()得到的TypeMirror就可以知道。

AbstractProcessor介绍

AbstractProcessor有四个比较重要的方法:

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
    super.init(processingEnv);
}

@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
    // ...
    return false;
}

@Override
public Set getSupportedAnnotationTypes() {
    return super.getSupportedAnnotationTypes();
}

@Override
public SourceVersion getSupportedSourceVersion() {
    return super.getSupportedSourceVersion();
}

init

初始化处理器,提供以下工具类

  • Elements:一个用来处理Element的工具类,例如获取元素的包和注释等。
  • Types:一个用来处理TypeMirror的工具类,例如获取类型的超类型等。
  • Filer:顾名思义,使用Filer你可以创建文件。
  • Messager:用来打印输出日志信息。

process

扫描处理注解,生成代码,必须重载。入参介绍:

Set extends TypeElement> annotations
需要被处理的注解集合
RoundEnvironment roundEnv
通过这个可以查询出包含特定注解的被注解元素

如果process返回true,说明不需要后续的Processor处理它;返回false则需要。

getSupportedAnnotationTypes

返回你需要处理的注解集合,可以@SupportedAnnotationTypes代替。

getSupportedSourceVersion

指定Java版本,可以用@SupportedSourceVersion代替。

AbstractProcessor使用

TestProcessor

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({"com.jerry.annotation.ProcessorParam"})
public class Processor extends AbstractProcessor {

    public boolean process(Set annotations, RoundEnvironment roundEnv) {
        System.out.println("-------------------in");
        Set elements = (Set) roundEnv.getElementsAnnotatedWith(ProcessorParam.class);
        for (Element element : elements) {
            System.out.println(element.getSimpleName());
            System.out.println(element.getAnnotation(ProcessorParam.class).value());
        }
        System.out.println("-------------------");
        return false;
    }

}

MainActivity

public class MainActivity extends Activity {

    @ProcessorParam("jerry")
    private String param;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }

}

注册AbstractProcessor

万事俱备只欠东风,我们需要将AbstractProcessor注册到编译器中。由于AbstractProcessor是属于javax.annotation.processing包的,因此需要新建一个依赖的Java Library。在这个library里,在src/main/resources/META-INF/services里需要有javax.annotation.processing.Processor这个文件,里面放了注解处理器的路径,例如:

com.jerry.annotation.Processor

建好后的目录结构如下:
butterknife源码分析:如何处理注解—反射与注解处理器_第1张图片

结果

在编译工程时,可以在Gradle Console里看到以下的结果:

Executing tasks: [clean, :app:generateDebugSources, :app:generateDebugAndroidTestSources, :app:mockableAndroidJar, :app:prepareDebugUnitTestDependencies, :app:compileDebugSources, :app:compileDebugAndroidTestSources, :app:compileDebugUnitTestSources]

Configuration on demand is an incubating feature.
Incremental java compilation is an incubating feature.
:clean
:app:clean
:javapractive:clean
:app:preBuild UP-TO-DATE
:app:preDebugBuild UP-TO-DATE
:app:checkDebugManifest
:app:prepareDebugDependencies
:app:compileDebugAidl
:app:compileDebugRenderscript
:app:generateDebugBuildConfig
:app:generateDebugResValues
:app:generateDebugResources
:app:mergeDebugResources
:app:processDebugManifest
:app:processDebugResources
:app:generateDebugSources
:app:preDebugAndroidTestBuild UP-TO-DATE
:app:prepareDebugAndroidTestDependencies
:app:compileDebugAndroidTestAidl
:app:processDebugAndroidTestManifest
:app:compileDebugAndroidTestRenderscript
:app:generateDebugAndroidTestBuildConfig
:app:generateDebugAndroidTestResValues
:app:generateDebugAndroidTestResources
:app:mergeDebugAndroidTestResources
:app:processDebugAndroidTestResources
:app:generateDebugAndroidTestSources
:app:mockableAndroidJar
:app:preDebugUnitTestBuild UP-TO-DATE
:app:prepareDebugUnitTestDependencies
:app:incrementalDebugJavaCompilationSafeguard
:javapractive:compileJava
警告: [options] 未与 -source 1.7 一起设置引导类路径
注: /Users/linhaiyang807/Downloads/Practice/javapractive/src/main/java/com/jerry/annotation/Processor.java使用了未经检查或不安全的操作。
注: 有关详细信息, 请使用 -Xlint:unchecked 重新编译。
1 个警告

:javapractive:processResources
:javapractive:classes
:javapractive:jar
:app:compileDebugJavaWithJavac
:app:compileDebugJavaWithJavac - is not incremental (e.g. outputs have changed, no previous execution, etc.).
-------------------in
param
jerry
-------------------
-------------------in
-------------------
:app:compileDebugNdk UP-TO-DATE
:app:compileDebugSources
:app:incrementalDebugAndroidTestJavaCompilationSafeguard
:app:compileDebugAndroidTestJavaWithJavac
:app:compileDebugAndroidTestNdk UP-TO-DATE
:app:compileDebugAndroidTestSources
:app:incrementalDebugUnitTestJavaCompilationSafeguard UP-TO-DATE
:app:compileDebugUnitTestJavaWithJavac UP-TO-DATE
:app:processDebugJavaRes UP-TO-DATE
:app:processDebugUnitTestJavaRes UP-TO-DATE
:app:compileDebugUnitTestSources UP-TO-DATE

BUILD SUCCESSFUL

Total time: 16.712 secs

从console里面我们可以看到有域变量的名字-param,以及注解的内容-jerry。
细心的读者可以看到,process被执行的两次。因为process会执行多次,每一次的输入都是经过上次生成的源代码,直到没有需要处理的注解为止。因为在处理过程中生成的代码可能又有新的注解出现。所以我们的例子,process执行了俩次。

后记

若有疑问或错误,请在评论中写出,谢谢!

  • 深入学习Java反射
    http://www.sczyh30.com/posts/Java/java-reflection-1/
  • Element与TypeMirror:
    http://docs.oracle.com/javase/7/docs/api/javax/lang/model/element/Element.html
    http://docs.oracle.com/javase/7/docs/api/javax/lang/model/type/TypeMirror.html

你可能感兴趣的:(butterknife源码分析:如何处理注解—反射与注解处理器)