一、简介
注解是对程序信息的一种补充标记,本质上是一个特殊的接口,接口里面定义的方法实际上是注解的属性。单独使用注解没有任何意义,需要配合程序来使用。
就是一个元数据,即描述数据的数据。
1、注解和注释的区别
-
定义不同
- 注解:元数据,它是一种描述数据的数据。所以,可以说注解就是源代码的元数据。
- 注释:是对源代码说明的文字
-
作用对象不同
- 注解:是给编译器看的。
- 注释:是给人看的。
-
书写范围不同
- 注解:遵守一定的书写规范,以@开头,与工具一起使用
- 注释:可以在代码的任何地方书写
-
运行范围不同
- 注解:可以参与编译器的任何阶段,对数据有一定的操作作用
- 注释:被编译器忽略,不参与编译
2、Java文件的程序运行过程
注解的生命周期是和程序允许过程绑定的,所以有必要先了解Java文件的程序运行过程
-
Source(源代码阶段)
-
Class(类对象阶段)
-
Runtime(运行时)
这个其实和元注解 @Retention 的生命周期对应
@Retention
- 源码时注解(SOURCE):仅保留在源码阶段,在编译期就会被丢弃,一般是编译器来解析相关注解
- 编译器注解(CLASS):保留在编译阶段,在类的加载阶段会被丢弃,一般使用APT技术来解析
- 运行时注解(RUNTIME):全程保留,从源码到App运行过程中,一般使用反射来解析
特别注意:RUNTIME是包含CLASS和SOURCE,CLASS包含SOURCE
二、基础定义
1、元数据
注解上的注解
(1)、@Target
注解对象的作用范围
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Target {
ElementType[] value();
}
public enum ElementType {
TYPE, /** Class, interface (including annotation type), or enum declaration */
FIELD, /** Field declaration (includes enum constants) */
METHOD, /** Method declaration */
PARAMETER, /** Formal parameter declaration */
CONSTRUCTOR, /** Constructor declaration */
LOCAL_VARIABLE, /** Local variable declaration */
ANNOTATION_TYPE, /** Annotation type declaration */
PACKAGE, /** Package declaration */
TYPE_PARAMETER, /**
* Type parameter declaration
*
* @since 1.8
*/
TYPE_USE;/**
* Use of a type
*
* @since 1.8
*/
private ElementType() {
}
}
(2)、@ Retention
注解保留的生命周期
package java.lang.annotation;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.ANNOTATION_TYPE})
public @interface Retention {
RetentionPolicy value();
}
public enum RetentionPolicy {
SOURCE, //源码级别
CLASS, //字节码级别
RUNTIME; //运行时级别
private RetentionPolicy() {
}
}
- 源码时注解(SOURCE):仅保留在源码阶段,在编译期就会被丢弃,一般是编译器来解析相关注解
- 编译器注解(CLASS):保留在编译阶段,在类的加载阶段会被丢弃,一般使用APT技术来解析
- 运行时注解(RUNTIME):全程保留,从源码到App运行过程中,一般使用反射来解析
(3)、 @Documented
作用在类上,被@Documented标记的类,使用javadoc命令执行一下对应的类就会生成文档
(4)、@Inherited
作用在子类上,被@Inherited标记的子类会继承父类的注解
2、注解的属性
实际注解的本质就是接口,接口可以定义方法,这些方法名称就是属性
(1)默认属性
注解默认的是value
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ValueAnnotation {
int value();
}
@TestAnnotation.ValueAnnotation(123)
public class MainActivity extends AppCompatActivity
(2)其它自定义属性,不能直接传值,是要以key value的形式
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface ValueAnnotation {
int value() ;
String name() default "周瑜";
int[] ids();
}
@TestAnnotation.ValueAnnotation(@TestAnnotation.ValueAnnotation(value = 123,name="123",ids={1,2,3}))
(3)、default
可以给属性赋默认值
三、作用
1、编译检查(主要是 SOURCE级别)
举几个例子就明白了
(1)、@Override
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这个Target目标是方法,Retention对应SOURCE级别
方法重载的作用主要是:检查是否正确重写了父类中的方法,表明这是一个重写的方法
同样类似的有
(2)、@LayoutRes
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface LayoutRes {
}
可以指明 参数类型 必须是布局资源类型
ReportBrandAdapter(var context: Context, @LayoutRes layoutResId: Int)
还有 类似
(3)、@Nullable
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE})
public @interface Nullable {
}
可以指明参数可以为null
ReportBrandAdapter(var context: Context, @Nullable layoutResId: Int)
(4)、自己定义检查
public class TestAnnotation {
private static final int Monday = 1;
private static final int Sun = 2;
@IntDef({Monday, Sun})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface WeekAnnotation{
}
@WeekAnnotation
private static int day;
private static void setDay(@WeekAnnotation int day){
TestAnnotation.day = day;
}
public static void main(String[] args) {
setDay(Monday);
}
}
这时候 setDay只能传入 Monday和Sun,不然会提示红色,但不影响编译和允许,就是IDE语法检查,在SOURCE级别
2、框架的应用,自动生成java代码,减轻开发者的工作量(APT工具的使用,编译时注解)
(1)ButterKnife
/**
* Bind a field to the view for the specified ID. The view will automatically be cast to the field
* type.
* <pre><code>
* {@literal @}BindView(R.id.title) TextView title;
* </code></pre>
*/
@Retention(RUNTIME) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
上面的作用是绑定控件,减少findViewById,真正核心的不是这块。ButterKnife
是ButterKnife.bind(this)
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, 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);
}
}
主要有三步骤:
1.Class<?> targetClass = target.getClass(); 拿到传入的Activity的Class类对象
2.Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);寻找第一步的Class类对象的构造方法
3.return constructor.newInstance(target, source);//调用构造方法
这里我们还要从findBindingConstructorForClass(targetClass)跟进去看看他是怎么寻找的构造方法
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls); //重点一
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");//重点二
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
这里需要看的地方就两个,代码里已经加了注释
重点一:BINDINGS是一个Map对象,而且是个静态常量(生命周期等同于App),这个保证了我们如果是已经被绑定过的对象,不需要二次绑定,可以直接从Map里面取出来,主要是为了性能优化考虑。
重点二: cls.getClassLoader().loadClass(clsName + "_ViewBinding");这里以我们上面的例子为例,clsName就等于PosLoginActivity,这里就是用cls的类加载器去加载了一个名叫PosLoginActivity_ViewBinding的java类,下面我们就要找一下MainActivity_ViewBinding这个java类。上面我们提到过,Butterknife是通过APT技术来解析注解并生成相关代码,我们可以推断出这个PosLoginActivity_ViewBinding这个java类就是Butterknife生成的代码,通过APT生成的代码都在如图所示目录下
然后看PosLoginActivity_ViewBinding文件
@UiThread
public PosLoginActivity_ViewBinding(final PosLoginActivity target, View source) {
this.target = target;
View view;
target.ll_clear_phone = Utils.findRequiredViewAsType(source, R.id.ll_clear_phone, "field 'll_clear_phone'", ImageView.class);
target.cb_remember_account = Utils.findRequiredViewAsType(source, R.id.cb_remember_account, "field 'cb_remember_account'", CheckBox.class);
target.cb_remember_password = Utils.findRequiredViewAsType(source, R.id.cb_remember_password, "field 'cb_remember_password'", CheckBox.class);
view = Utils.findRequiredView(source, R.id.btn_login, "field 'btn_login' and method 'doLogin'");
target.btn_login = Utils.castView(view, R.id.btn_login, "field 'btn_login'", Button.class);
view7f0a0072 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.doLogin();
}
});
然后跟踪findRequireView,最终还是调用了source.findViewById(id)
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}
public static <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
try {
return cls.cast(view);
} catch (ClassCastException e) {
String name = getResourceEntryName(view, id);
throw new IllegalStateException("View '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was of the wrong type. See cause for more info.", e);
}
}
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(id);
if (view != null) {
return view;
}
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
+ " (methods) annotation.");
}
我们发现点击事件会有比较重要的两步,第一步找到view,第二步类型转换,最后会设置点击事件,点击事件回调会调用 target.doLogin()
view = Utils.findRequiredView(source, R.id.btn_login, "field 'btn_login' and method 'doLogin'"); //第一步找到view
target.btn_login = Utils.castView(view, R.id.btn_login, "field 'btn_login'", Button.class);//第二步类型转换
view = Utils.findRequiredView(source, R.id.btn_login, "field 'btn_login' and method 'doLogin'");
target.btn_login = Utils.castView(view, R.id.btn_login, "field 'btn_login'", Button.class);
view7f0a0072 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.doLogin();
}
});
注意:这里也解决了一个我们在使用Butterknife的疑问,就是我们使用注解的变量或者方法不能被private和protected修饰,因为我们的target的对象要直接调用对应的变量和方法,比如 target.textView ; target.doLogin();
看看如果用 private会报下面错误,权限错误
注解的保留期。
- 源码时注解(SOURCE):仅保留在源码阶段,在编译期就会被丢弃,一般是编译器来解析相关注解
- 编译器注解(CLASS):保留在编译阶段,在类的加载阶段会被丢弃,一般使用APT技术来解析
- 运行时注解(RUNTIME):全程保留,从源码到App运行过程中,一般使用反射来解析
了解了前面的知识,我们就可以知道Butterknife肯定不是用反射来解析注解的,因为反射的效率比较低,也不是通过编译器来解析的,如果是这样的话,压根也就没有了Butterknife这个库了。Butterknife是用APT技术来解析注解的,APT的全称是Android Annotation Processor Tool,Android注解解析工具。它是在编译期阶段来解析注解并生成对应的代码,然后在使用的时候再去调生成的代码,这样就跟我们自己写好的代码自己调用一下,不会产生任何性能问题,并且会减少大量的模板类代码。
(2)ARouter也是应用APT技术
感兴趣的可以翻翻ARouter的源码
(3)APT技术
Android在编译期阶段会扫描所有继承自AbstractProcess的子类,并调用其中的process()方法,我们只要在process()方法里拿到我们所有的注解并生成对应的代码即可。下面我来介绍一下AbstractProcess以及它相关的几个方法
AbstractProcessor是一个抽象类,常用的方法有四个
- init(ProcessingEnvironment processingEnv):注解处理器的初始化,一般在这里获取我们需要的工具类
- process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv):必须要重写的方法,也是实际入口有点类似与main()方法,我们要在这写我们实际的业务代码
- getSupportedAnnotationTypes():指定注解处理器是注册给哪个注解的,返回指定支持的注解类集合
- getSupportedSourceVersion():返回支持的jdk版本,一般都直接使用SourceVersion.latestSupported();
ButterKnifeProcessor这个类。
ButterKnifeProcessor中的源码很多,这里我们只看重点,直接到process方法中
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);//解析所有的Butterknife注解并生成一个Map对象
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);//这里用到了JavaPoet库中的类,生成对应的java类
try {
javaFile.writeTo(filer);//把java类写入到文件里面
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
Butterknife内部还引入了一个Java库叫JavaPoet,这个是专门用来编写java文件的。BindingSet这个类是对JavaPoet里面的一些属性的封装,它包含了一个Java文件里面包含的各种注解信息,由它生成java文件再写入即可。
Butterknife不存在性能问题是利用了APT技术,在代码编译阶段就对注解做了解析并生成了相关代码,Butterknife.bind(this);实际上就是对生成代码的调用。
3、根据注解生成帮助文档
主要使用 @Documented 元注解,被@Documented的类 使用javadoc命令执行一下对应的类就会生成文档
4、字节码增强技术(主要在Class级别)
比如要统计带@InjectTime 的注解 的方法执行时间,就要修改Class,所以是Class级别
(1)、插桩技术
5、运行时注解作用(反射)
早期的 AndroidAnnotation框架,AFinal框架
(1)模仿旧的的ButterKnife使用反射
package com.androidtv.pos.annotation;
import androidx.annotation.IdRes;
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 InjectView {
@IdRes int value();
}
package com.androidtv.pos.annotation;
import android.app.Activity;
import android.view.View;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class InjectUtil {
public static void injectView(Activity activity) {
Class<? extends Activity> cls = activity.getClass();
try {
Method method = cls.getMethod("onClick", Integer.TYPE); //得到方法
method.setAccessible(true);
Method[] methods = cls.getMethods();
Field field = cls.getField("name"); //获得此类和父类所有公共成员
Field declareField = cls.getDeclaredField("name"); //得到次类所有成员
//如果要拿到父类所有成员
Field[] superField = cls.getSuperclass().getDeclaredFields();
Field[] declaredFields = cls.getDeclaredFields();
for(Field decField : declaredFields){
if(decField.isAnnotationPresent(InjectView.class)){
InjectView injectView = field.getAnnotation(InjectView.class);
int id = injectView.value();
View view = activity.findViewById(id);
decField.setAccessible(true);
decField.set(activity, view);
}
}
}catch (Exception e){
}
}
}
@InjectView(R.id.tv_title)
private TextView tv_title;
(2)、 通过反射拿到泛型的真实类型
参考文章:https://www.cnblogs.com/hahayixiao/p/13772011.html#jieandshi
https://www.jianshu.com/p/6c06568b2919