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才对。
在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 extends Activity> 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功能。
前面说到的反射是在运行时处理注解,有些人可能担心反射对应用的性能有大的影响。其实通过注解处理器可以在编译时扫描注解,处理注解,包括生成代码等,这样就不会对应用的性能造成影响。前面我们提到的ButterKnife就是通过注解处理器来的。
在介绍AbstractProcessor前,先介绍Element与TypeMirror俩个接口。
在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表示 Java 编程语言中的类型。这些类型包括基本类型、声明类型(类和接口类型)、数组类型、类型变量和 null 类型。还可以表示通配符类型参数、executable 的签名和返回类型,以及对应于包和关键字 void 的伪类型。
看到这里,可能读者会跟我一样分不清Element和TypeMirror的区别。我一开始也很混淆,后来网上查了很久,又看了下编程思想的介绍,总结如下。
Element代表的是源代码。举个栗子,TypeElement代表源代码中的类或接口的元素,是未经编译的。也就是说我们无法根据TypeElement获取类的信息,例如类里面的方法、域、超类等信息。而通过TypeElement.asType()得到的TypeMirror就可以知道。
AbstractProcessor有四个比较重要的方法:
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
// ...
return false;
}
@Override
public Set getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
初始化处理器,提供以下工具类
- Elements:一个用来处理Element的工具类,例如获取元素的包和注释等。
- Types:一个用来处理TypeMirror的工具类,例如获取类型的超类型等。
- Filer:顾名思义,使用Filer你可以创建文件。
- Messager:用来打印输出日志信息。
扫描处理注解,生成代码,必须重载。入参介绍:
Set extends TypeElement> annotations
需要被处理的注解集合
RoundEnvironment roundEnv
通过这个可以查询出包含特定注解的被注解元素
如果process返回true,说明不需要后续的Processor处理它;返回false则需要。
返回你需要处理的注解集合,可以@SupportedAnnotationTypes代替。
指定Java版本,可以用@SupportedSourceVersion代替。
@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes({"com.jerry.annotation.ProcessorParam"})
public class Processor extends AbstractProcessor {
public boolean process(Set extends TypeElement> 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;
}
}
public class MainActivity extends Activity {
@ProcessorParam("jerry")
private String param;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}
万事俱备只欠东风,我们需要将AbstractProcessor注册到编译器中。由于AbstractProcessor是属于javax.annotation.processing包的,因此需要新建一个依赖的Java Library。在这个library里,在src/main/resources/META-INF/services里需要有javax.annotation.processing.Processor这个文件,里面放了注解处理器的路径,例如:
com.jerry.annotation.Processor
在编译工程时,可以在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