Android进阶 注解
1.注解是什么
注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。
注解有许多用处,主要如下:
- 提供信息给编译器: 编译器可以利用注解来探测错误和警告信息
- 编译阶段时的处理: 软件工具可以用来利用注解信息来生成代码、Html文档或者做其它相应处理。
- 运行时的处理: 某些注解可以在程序运行的时候接受代码的提取
Annotation
(注解)就是Java提供了一种元程序中的元素关联任何信息和着任何元数据(metadata
)的途径和方法。Annotion
(注解)是一个接口,程序可以通过反射来获取指定程序元素的Annotion
对象,然后通过Annotion
对象来获取注解里面的元数据。
Annotation
(注解)是JDK5.0及以后版本引入的。它可以用于创建文档,跟踪代码中的依赖性,甚至执行基本编译时检查。从某些方面看,annotation
就像修饰符一样被使用,并应用于包、类 型、构造方法、方法、成员变量、参数、本地变量的声明中。这些信息被存储在Annotation
的“name=value”
结构对中。
经过上面一些解释,我们来定义一个注解@FindWrong
public @interface FindWrong {}
public class SortZero {
@FindWrong
static void sort(int[] res) {}
}
这个时候我们发现IDE能否识别@FindWrong
了
当然,上面那个例子IDE能够识别,但是它不能被使用。
IDE傻傻的,它并没有检测注解定义的合理性。它差了哪些呢?
我们常常这么写:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
查看一下常用的@Override
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
其他注解类似。可以看到一个注解至少有两个元素,@Target
和@Retention
也就是说,有几个注解是最基础的注解,被称之为元注解
2.元注解
元注解有四个,1.8添加一个到五个。
- @Target
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
ElementType[] value();
}
表示该注解用于什么地方
public enum ElementType {
/** 类,接口(包括注解类型)或enum声明 */
TYPE,
/** 域声明(包括 enum 实例) */
FIELD,
/** 方法声明 */
METHOD,
/** 参数声明 */
PARAMETER,
/** 构造器声明 */
CONSTRUCTOR,
/** 局部变量声明 */
LOCAL_VARIABLE,
/** Annotation type declaration */
ANNOTATION_TYPE,
/** 包声明 */
PACKAGE,
/**
* 类型参数声明
*
* @since 1.8
*/
TYPE_PARAMETER,
/**
* 类型的使用
*
* @since 1.8
*/
TYPE_USE
}
- @Retention
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
RetentionPolicy value();
}
表示该注解可以保存的范围
public enum RetentionPolicy {
/**
* 源代码:即此注解只能保存在源代码中
* 当编译时,会被丢弃
*/
SOURCE,
/**
* class文件:即此注解可以在class文件中保留
* 但会被jvm丢弃
*/
CLASS,
/**
* 运行期:即此注解可以在运行时保留
* 可以通过反、反射获得
*/
RUNTIME
}
- @Documented
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}
即拥有这个注解的元素可以被javadoc此类的工具文档化。它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@return,@param 等。
- @Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}
允许子类继承父类中的注解。即拥有此注解的元素其子类可以继承父类的注解。
- @Repeatable
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Repeatable {
Class<? extends Annotation> value();
}
Repeatable是可重复的意思。这个是JDK1.8新增的,表明一个注解可以多次应用。
3.注解如何被使用
在给出代码示例之前,要求大家
- 对反射有一点点了解
- isAnnotationPresent(Class<? extends Annotation> annotationClass)判断是不是有这个注解
- getAnnotation(Class<T> annotationClass)获取注解对象
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FindWrong {
}
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class SortZero {
@FindWrong()
public void prit(){
System.out.println("hello world");
}
public static void main(String[] args) {
SortZero obj = new SortZero();
Method[] methods = obj.getClass().getMethods();
for(Method method: methods){
if(method.isAnnotationPresent(FindWrong.class)){
try {
method.invoke(obj,null);
}catch (IllegalAccessException e){
e.printStackTrace();
}catch (IllegalArgumentException e){
e.printStackTrace();
}catch (InvocationTargetException e){
e.printStackTrace();
}
}
}
}
}
如果我们要传参数怎么办,可以这样
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface FindWrong {
String say();
}
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class SortZero {
@FindWrong(say = "hello world haha")
public void prit(String str){
System.out.println(str);
}
public static void main(String[] args) {
SortZero obj = new SortZero();
Method[] methods = obj.getClass().getMethods();
for(Method method: methods){
if(method.isAnnotationPresent(FindWrong.class)){
FindWrong value = method.getAnnotation(FindWrong.class);
try {
method.invoke(obj,value.say());
}catch (IllegalAccessException e){
e.printStackTrace();
}catch (IllegalArgumentException e){
e.printStackTrace();
}catch (InvocationTargetException e){
e.printStackTrace();
}
}
}
}
}
注解的默认方法是value()
注解参数的可支持数据类型
:
- 1.所有基本数据类型(int,float,boolean,byte,double,char,long,short)
- 2.String类型
- 3.Class类型
- 4.enum类型
- 5.Annotation类型
- 6.以上所有类型的数组
3.我们不妨实现一下butterknife###
通过以上的知识学习,我们不妨考虑一下一个简单的butterknife
框架该如何实现。
首先,我们事先成员变量的注解 @BindView
package com.example.sleepydragon.myapplication;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
再事先一下方法的注解 @OnClick
package com.example.sleepydragon.myapplication;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnClick {
int[] id();
}
实现一下他们对应的绑定方法bind()
package com.example.sleepydragon.myapplication;
import android.app.Activity;
import android.view.View;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MyButterKnife {
static void bind(final Activity activity){
bindView(activity);
bindOnClick(activity);
}
private static void bindOnClick(final Activity activity) {
Method[] methods = activity.getClass().getMethods();
for(Method method: methods){
final Method thisMethod = method;
if(thisMethod.isAnnotationPresent(OnClick.class)){
final OnClick value = thisMethod.getAnnotation(OnClick.class);
for(int id:value.id()){
final int thisId = id;
final View thisView = activity.findViewById(thisId);
thisView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
try {
thisMethod.invoke(activity,thisView);
}catch (IllegalAccessException e){
e.printStackTrace();
}catch (IllegalArgumentException e){
e.printStackTrace();
}catch (InvocationTargetException e){
e.printStackTrace();
}
}
});
}
}
}
}
private static void bindView(final Activity activity) {
Field[] fields = activity.getClass().getFields();
for(Field field: fields){
final Field thisFiled = field;
if(thisFiled.isAnnotationPresent(BindView.class)){
final BindView value = thisFiled.getAnnotation(BindView.class);
thisFiled.setAccessible(true);
try {
thisFiled.set(activity,activity.findViewById(value.value())) ;
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
我们写个方法测一下。
package com.example.sleepydragon.myapplication;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
@BindView(R.id.hello)
TextView mTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButterKnife.bind(this);
}
@OnClick(id={R.id.hello})
public void click(View view){
Toast.makeText(this,"o "+mTextView.toString(),Toast.LENGTH_LONG).show();
}
}
请大家思考一下,实际工程中,能够用上面这种实现方式吗?
以上方式灵活,但是存在效率问题。通过反射去获取对象和直接生成对象现在有两到三倍的耗时差距。
4.APT和annotationProcessor
APT(Annotation Processing Tool)是一种处理注释的工具,它对源代码文件进行检测找出其中的Annotation,使用Annotation进行额外的处理。 Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
随着Android Gradle 插件 2.2 版本的发布,Android Gradle 插件提供了名为 annotationProcessor 的功能来完全代替 android-apt ,自此android-apt 作者在官网发表声明证实了后续将不会继续维护 android-apt ,并推荐大家使用 Android 官方插件annotationProcessor。
事实上,你现在用apt,Androidstudio会警告你。
5.怎么实现一个编译注解框架
首先,我们了解一下编译注解框架的功能。他是在apk编译的时候,生成辅助的class文件,打包进对应的包,以实现相应功能。
我说明一下,就不演示了。用ButterKnife标注一个view。编译之后他会在module_build_generated_source_apt_debug_包名_对应的Activity(或是其他)后面加上标志(_ViewBinding)的一个类。每次调用的ButterKnife.bind()就是触发这个类的构造函数,来进行绑定动作。
为了达到类似效果,我们构建一个工程。
工程结构javalib myanonation
实现自己的注解
javalib myprocessor
实现自己的annotationProcessor。目的是根据注解生成对应class并注入。
Androidlib mycompiler
实现自己的实际类与生成类的关联方式。
我们来看一下各个类实现:
与上面的BindView
不同,现在我们申明它的生命周期是CLASS
package com.phicomm.myanonation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value();
}
关联类应该做什么呢?我们可以从ButterKnife抄一抄。下面是抽出来的主体方法。
package com.phicomm.mycompiler;
import android.app.Activity;
import android.view.View;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class MyButterKnife {
public static void bind(Activity activity){
View sourceView = activity.getWindow().getDecorView();
Class<?> targetClass = activity.getClass();
Constructor<?> bindingCtor;
try {
//尝试找对应Activity叫上后缀_ViewBinding的类
Class<?> bindingClass = targetClass.getClassLoader().loadClass(targetClass.getName() + "_ViewBinding");
//找它的构造器
bindingCtor = (Constructor<?>) bindingClass.getConstructor(activity.getClass(), View.class);
}catch (ClassNotFoundException e) {
throw new RuntimeException("Unable to find binding constructor for " + targetClass.getName(), e);
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + targetClass.getName(), e);
}
try {
//通过构造器生成类
bindingCtor.newInstance(activity, sourceView);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + bindingCtor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + bindingCtor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
}
关于anonationProcessor,它是生成对应类。我们先看看调用@BindView
的Activity是什么样子。
package com.phicomm.testanonation;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import com.phicomm.myanonation.BindView;
import com.phicomm.mycompiler.MyButterKnife;
public class MainActivity extends AppCompatActivity {
@BindView(R.id.hello)
TextView mHelloTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButterKnife.bind(this);
Log.e("xionglong",mHelloTextView.toString());
}
}
那么,bind方法实现的功能其实就一句话。
mHelloTextView = (TextView)findViewById(R.id.hello);
我们其实就想把这个话放到生成的MainActivity_ViewBinding.class的构造函数里。大家可以考虑下怎么写这个构造函数。我这里给出一种方式。
package com.phicomm.testanonation;
import android.view.View;
import com.phicomm.testanonation.MainActivity;
public class MainActivity_ViewBinding{
public MainActivity_ViewBinding(com.phicomm.testanonation.MainActivity host,android.view.View $view){
host.mHelloTextView=(android.widget.TextView)$view.findViewById(2131165239);
}
}
我们要手动生成这个class。注入操作有google的框架完成。
首先,在myprocessor
的build.gradle里面添加google的框架。
apply plugin: 'java-library'
apply plugin: 'java'
sourceCompatibility =JavaVersion.VERSION_1_8
targetCompatibility =JavaVersion.VERSION_1_8
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':myanonation')
//AutoService 主要的作用是注解 processor 类,并对其生成 META-INF 的配置信息。
implementation 'com.google.auto.service:auto-service:1.0-rc3'
}
生成下面这个文件就算好。需要有下面那个自定义的注解类。
[图片上传失败...(image-5086c7-1530629574099)]
然后,我们考虑一下生成辅助class文件哪些是必须的。
在我们这个示例Demo中,有这些东西我们必须得知道:
- MainActivity的包名
com.phicomm.testanonation
- MainActivity的类名
MainActivity
- 成员变量的mHelloTextView的名称
mHelloTextView
- 成员变量的mHelloTextView的类型
android.widget.TextView
- id我们可以通过注解的值拿到
package com.phicomm.myprocessor;
import com.google.auto.service.AutoService;
import com.phicomm.myanonation.BindView;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
@SupportedAnnotationTypes("com.phicomm.myanonation.BindView")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
//生成文件的帮助类
private Filer mFiler;
//Element的处理类,能够得到Element的各种属性
private Elements mElementUtils;
//编译时不能打出log,Messager辅助打出log
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
mFiler = env.getFiler();
mElementUtils = processingEnv.getElementUtils();
mMessager = processingEnv.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
mMessager.printMessage(Diagnostic.Kind.NOTE, "process...");
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
int id = element.getAnnotation(BindView.class).value();
String srcName = element.getSimpleName().toString();
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
String qualifiedName = enclosingElement.getQualifiedName().toString();
String simpleName = enclosingElement.getSimpleName().toString();
String packageName = mElementUtils.getPackageOf(element).getQualifiedName().toString();
String typeName = element.asType().toString();
try {
JavaFileObject sourceFile = mFiler.createSourceFile(
qualifiedName+"_ViewBinding", element);
Writer writer = sourceFile.openWriter();
writer.write(new StringBuilder()
.append("package "+packageName+";\n\n")
.append("import android.view.View;\n")
.append("import "+qualifiedName+";\n\n")
.append("public class "+simpleName+"_ViewBinding"+"{\n")
.append(" public "+simpleName+"_ViewBinding"+"("+qualifiedName+" host,android.view.View $view){"+"\n")
.append(" host."+srcName+"=("+typeName+")$view.findViewById("+id+");"+"\n")
.append(" }\n")
.append("}")
.toString());
writer.flush();
writer.close();
} catch (IOException e) {
error(element, "Unable to write binding for type %s: %s", element, e.getMessage());
}
}
return false;
}
private void error(Element element, String message, Object... args) {
printMessage(Diagnostic.Kind.ERROR, element, message, args);
}
private void printMessage(Diagnostic.Kind kind, Element element, String message, Object[] args) {
if (args.length > 0) {
message = String.format(message, args);
}
processingEnv.getMessager().printMessage(kind, message, element);
}
}
有以上这些,我们的Demo就可以跑起来了。示例的Demo附在附件里。
最后,我们考虑一下,Android的注解框架可以做到什么,不可以做到什么?
???
参考文档:
1.秒懂,Java 注解 (Annotation)你可以这样学
2.注解Annotation实现原理与自定义注解例子
3.APT注释工具
4.使用编译时注解简单实现类似 ButterKnife 的效果
网友评论