转载请标明出处:http://blog.csdn.net/zhaoyanjun6/article/details/119874435
本文出自【赵彦军的博客】
文章目录
- Java注解
- 元注解说明
-
- @Retention
- @Target
- 通过反射获取注解信息
- 方法使用注解
- 方法的参数使用注解
- Android 自带的注解
-
- @LayoutRes
- @MainThread
- @IntDef
- @StringRes
- @ColorInt
- @IdRes
- @DrawableRes
- @NotNull
- @Nullable
- @Keep
- @RequiresPermission
- @Deprecated
- CallSuper
- @IntRange
- @FloatRange
- @CheckResult
- @size
- 总结:
Java注解
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。
Java内置了多种标准注解,其定义在java.lang中。
元注解说明
@Retention
表示需要在什么级别保留该注解信息
- RetentionPolicy.SOURCE:只在源代码中保留 一般都是用来增加代码的理解性或者帮助代码检查之类的,比如我们的Override
- RetentionPolicy.CLASS : 默认的选择,能把注解保留到编译后的字节码class文件中,仅仅到字节码文件中,运行时是无法得到的
- RetentionPolicy.RUNTIME :注解不仅能保留到class字节码文件中,还能在运行通过反射获取到,这也是我们最常用的
@Target
表示该注解可以用在什么地方
- ElementType.FIELD : 能修饰成员变量
- ElementType.METHOD:能修饰方法
- ElementType.CONSTRUCTOR : 能修饰构造器
- ElementType.PACKAGE : 能修饰包
- ElementType.PARAMETER : 能修饰方法参数
- ElementType.TYPE : 能修饰类、接口或枚举类型
- ElementType.ANNOTATION_TYPE : 能修饰注解
- ElementType.LOCAL_VARIABLE:能修饰局部变量
- ElementType.MODULE :
通过反射获取注解信息
Annotation是被动的元数据,永远不会有主动行为,所以我们需要通过使用反射,才能让我们的注解产生意义。
首先我们定义一个端口注解:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Port {String value() default "8080";
}
RetentionPolicy.RUNTIME 在运行时保留注解,作用于是字段。
反射获取字段
object BindPort {/*** 绑定的目的* 1、通过反射获取注解的值* 2、通过反射给目标设置值*/fun bind(activity: Activity) {//获取所有字段val fields = activity.javaClass.declaredFieldsfields.forEach { field ->//获取所有注解val ans = field.annotationsans.forEach {if (it is Port) {//获取注解值var port = it.value//通过属性反射给属性注入值field.isAccessible = truefield.set(activity, port)}}}}}
上面代码的逻辑很简单:
首先遍历循环所有的属性,如果当前属性被指定的注解所修饰,那么就将当前属性的值修改为注解中成员变量的值。
这里setAccessible(true)的使用时因为,我们在声明port变量时,其类型为private,为了确保可以访问这个变量,防止程序出现异常。
注解的使用:
class MainActivity : AppCompatActivity() {@Portvar port: String? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)Log.d("yy--", "反射前:$port")BindPort.bind(this)Log.d("yy--", "反射后:$port")}
}
运行结果:
com.example.myapplication D/yy--: 反射前:null
com.example.myapplication D/yy--: 反射后:8080
当然,我们也可以在注解时,自定义我们的属性值,比如:
class MainActivity : AppCompatActivity() {@Port("8090")var port: String? = nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)Log.d("yy--", "反射前:$port")BindPort.bind(this)Log.d("yy--", "反射后:$port")}
}
运行结果:
com.example.myapplication D/yy--: 反射前:null
com.example.myapplication D/yy--: 反射后:8090
方法使用注解
我们来模拟一个http请求, 定义一个 Request
注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Request {Method value();enum Method {GET, POST}
}
注解的方式简单,@Retention(RetentionPolicy.RUNTIME)
在运行时保留,@Target(ElementType.METHOD)
作用域在方法上。
下面我们编写,反射的方法,定义 HttpBind
object HttpBind {/*** 绑定的目的* 1、通过反射获取注解的值* 2、通过反射给目标设置值*/fun bind(activity: Activity) {//获取所有方法val methods = activity.javaClass.methodsmethods.forEach { method ->//获取所有注解val ans = method.annotationsans.forEach {if (it is Request) {//获取注解值var requestMethod = it.valueif (requestMethod == Request.Method.GET) {//发起get请求method.invoke(activity, requestMethod.name)} else if (requestMethod == Request.Method.POST) {//发起post请求method.invoke(activity, requestMethod.name)}}}}}}
注解使用
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)//使用反射,解析注解HttpBind.bind(this)}@Request(Request.Method.GET)fun http(method: String) {Log.d("yy--", "网络请求:$method")}
}
我们运行一下,看看效果
D/yy--: 网络请求:GET
方法的参数使用注解
先定义一个参数注解 Path
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface Path {String value() default "";
}
这个注解也很简单,在运行时保留,作用域在参数上
下面我们使用反射来获取参数注解的值
object HttpBind {/*** 绑定的目的* 1、通过反射获取注解的值* 2、通过反射给目标设置值/反射调用方法*/fun bind(activity: Activity) {//获取所有方法val methods = activity.javaClass.methodsmethods.forEach { method ->//获取所有参数注解,一个方法有多个参数,一个参数有多个注解,所以类型是二维数组val ans = method.parameterAnnotationsans.forEach { annotationArray ->annotationArray.forEach { parameterAnnotation ->if (parameterAnnotation is Path) {var parameter = parameterAnnotation.valuemethod.invoke(activity, parameter)}}}}}
}
使用如下:
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)HttpBind.bind(this)}fun http(@Path("zhaoyanjun") user: String) {Log.d("yy--", "参数注解:$user")}
}
运行结果如下:
D/yy--: 参数注解:zhaoyanjun
Android 自带的注解
Android 系统已经帮我内置了很多有用的注解,在我们的开发过程中可以很方便的使用。
implementation 'androidx.annotation:annotation:1.2.0'
资源限制类
- @AnimatorRes :animator资源类型
- @AnimRes:anim资源类型
- @AnyRes:任意资源类型
- @ArrayRes:array资源类型
- @AttrRes:attr资源类型
- @BoolRes:boolean资源类型
- @ColorRes:color资源类型
- @DimenRes:dimen资源类型。
- @DrawableRes:drawable资源类型。
- @FractionRes:fraction资源类型
- @IdRes:id资源类型
- @IntegerRes:integer资源类型
- @InterpolatorRes:interpolator资源类型
- @LayoutRes:layout资源类型
- @MenuRes:menu资源类型
- @PluralsRes:plurals资源类型
- @RawRes:raw资源类型
- @StringRes:string资源类型
- @StyleableRes:styleable资源类型
- @StyleRes:style资源类型
- @TransitionRes:transition资源类型
- @XmlRes:xml资源类型
线程限制类
Thread annotations 线程执行限制类:用于限制方法或者类必须在指定的线程执行。如果方法代码运行线程和标注的线程不一致,则会导致警告。
- @AnyThread
- @BinderThread
- @MainThread
- @UiThread
- @WorkerThread
数值限制类
Value Constraint Annotations 类型范围限制类:用于限制标注值的值范围
- @FloatRang
- @IntRange
@LayoutRes
这个是layout
资源类型,我们看一下 Activity
的 setContentView
源码:
@Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);}
本质上,layoutResID
是一个 int 类型,如果不做限定的话,可以传入任意整形,但是有 @LayoutRes
注解的限制,值只能传入 R.layou.xx
, 如果传入其他的类型就会报错。举例如下:
需要注意的是,报错只是编译器的检查出错,提醒开发者改正错误用法,提前规避风险,并不影响编译运行
@MainThread
限定方法执行的线程,如果方法代码运行线程和标注的线程不一致,不会报错,更多是起一个提醒作用
@MainThreadfun run() {}
@IntDef
IntDef 的源码如下:
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {/** Defines the allowed constants for this element */int[] value() default {};/** Defines whether the constants can be used as a flag, or just as an enum (the default) */boolean flag() default false;/*** Whether any other values are allowed. Normally this is* not the case, but this allows you to specify a set of* expected constants, which helps code completion in the IDE* and documentation generation and so on, but without* flagging compilation warnings if other values are specified.*/boolean open() default false;
}
可以看到 Target
是 ANNOTATION_TYPE
说明 IntDef
是作用在注解上的。
还有一个 value
是 int
数组。
下面我们定义一个注解 MOBILE_TYPE
, 并且用 IntDef
修饰,如下:
import androidx.annotation.IntDef;
import kotlin.annotation.AnnotationRetention;
import kotlin.annotation.Retention;/*** @author : zhaoyanjun* @time : 2021/7/29* @desc :*/
public class Util {public static final int TYPE_MI = 1;public static final int TYPE_MEIZU = 2;public static final int TYPE_HUAWEI = 3;@Retention(AnnotationRetention.SOURCE)@IntDef({TYPE_MI, TYPE_MEIZU, TYPE_HUAWEI})public @interface MOBILE_TYPE {}//使用public void doSomething(@MOBILE_TYPE int mobile){}
}
使用方法很简单,首先定义你需要的常量,然后用 @IntDef
包住这些常量,这样别人在使用你的方法时如果输入的值不在枚举的范围内,编译器就会给出提示了。
同理,@StringDef
也是同样的用法
import androidx.annotation.StringDef;import kotlin.annotation.AnnotationRetention;
import kotlin.annotation.Retention;/*** @author : zhaoyanjun* @time : 2021/7/29* @desc :*/
public class Util {public static final String TYPE_HD = "720p";public static final String TYPE_SHD = "1080p";public static final String TYPE_FHD = "4k";@Retention(AnnotationRetention.SOURCE.SOURCE)@StringDef({TYPE_HD, TYPE_SHD, TYPE_FHD})public @interface DISPLAY_TYPE {}public void doSomething(@DISPLAY_TYPE String display) {}
}
还有一个 @LongDef
也是同样的用法,这里就不举例了。
总结 :IntDef
@StringDef
@LongDef
可以限制变量的类型,可以代替枚举类型
我们来看一个系统例子,Toast 的源码:
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {return makeText(context, null, text, duration);}
@Duration
是一个自定义的注解:
/** @hide */@IntDef(prefix = { "LENGTH_" }, value = {LENGTH_SHORT,LENGTH_LONG})@Retention(RetentionPolicy.SOURCE)public @interface Duration {}
看到这里,我们已经很熟悉了,也是用的 @IntDef
注解,除此之外,我们还发现了一个细节 ,在 android
的注解包里,@IntDef
带有 prefix
属性,但是在 androidx
的注解包里却没有。
下面贴一下两个的源码,大家看看:
//android 的源码,包名:android.annotation
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {/** Defines the constant prefix for this element */String[] prefix() default {};/** Defines the constant suffix for this element */String[] suffix() default {};/** Defines the allowed constants for this element */int[] value() default {};/** Defines whether the constants can be used as a flag, or just as an enum (the default) */boolean flag() default false;
}//androidx 的源码,包名:androidx.annotation
@Retention(SOURCE)
@Target({ANNOTATION_TYPE})
public @interface IntDef {/** Defines the allowed constants for this element */int[] value() default {};/** Defines whether the constants can be used as a flag, or just as an enum (the default) */boolean flag() default false;/*** Whether any other values are allowed. Normally this is* not the case, but this allows you to specify a set of* expected constants, which helps code completion in the IDE* and documentation generation and so on, but without* flagging compilation warnings if other values are specified.*/boolean open() default false;
}
这是两个包下面的 IntDef 的差异,我想知道的是 prefix 有什么用?
其实也很简单,规范 value 数组元素的命名前缀。
@StringRes
这个其实很好理解,限制字符的来源,必须是 R.string.xx , StringRes 源码如下:
/*** Denotes that an integer parameter, field or method return value is expected* to be a String resource reference (e.g. {@code android.R.string.ok}).*/
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface StringRes {
}
举个系统的例子:
public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)throws Resources.NotFoundException {return makeText(context, context.getResources().getText(resId), duration);}
@ColorInt
限定颜色的取值范围 R.color.xx , 源码如下:
/*** Denotes that the annotated element represents a packed color* int, {@code AARRGGBB}. If applied to an int array, every element* in the array represents a color integer.* <p>* Example:* <pre>{@code* public abstract void setTextColor(@ColorInt int color);* }</pre>*/
@Documented
@Retention(CLASS)
@Target({PARAMETER, METHOD, LOCAL_VARIABLE, FIELD})
public @interface ColorInt {
}
举个系统的例子:
public void setTextColor(@ColorInt int color) {mTextColor = ColorStateList.valueOf(color);updateTextColors();
}
@IdRes
限制id 的取值范围:R.id.xx , 源码码如下:
/*** Denotes that an integer parameter, field or method return value is expected* to be an id resource reference (e.g. {@code android.R.id.copy}).*/
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface IdRes {
}
举个系统的例子:
@Overridepublic <T extends View> T findViewById(@IdRes int id) {return getDelegate().findViewById(id);}
@DrawableRes
限定资源的取值类型是一个 drawable 类型:android.R.attr.alertDialogIcon
@Documented
@Retention(CLASS)
@Target({METHOD, PARAMETER, FIELD, LOCAL_VARIABLE})
public @interface DrawableRes {
}
举个系统的例子:
public void setImageResource(@DrawableRes int resId) {...
}
@NotNull
定义个变量不能为空, 如果真的为空,不会影响编译,只是编译器会报错,提醒开发者注意。
public class Util {//参数不能为nullpublic void run(@NotNull String name) {}
}
@Nullable
限定一个参数,一个方法的返回值可以为null
public class Util {@Nullablepublic String aa() {return null;}
}
@Keep
哪里不想被混淆就注解哪里。
@Keep
public class Test {
}public class TestA {
}
开始混淆打包,查看混淆后的结果:
我们发现TestA不见了而Test保留了下来,说明我们的配置起作用了,下面我们在Test 类中增加点内容看看混淆后会变成什么样子,修改后的类内容如下:
@Keep
public class Test {int age = 20;protected String sex = "m";public String name = "CodingMaster";
}
查看混淆后的结果:
不幸的是虽然类名保留下来了,但是里面的内容却被混淆了,如果我们想把name变量不被混淆怎么办呢?
我们继续修改Test类,这次我们多加了点东西,会在后面用到,内容如下:
@Keep
public class Test {int age = 20;@Keepprotected String sex = "m";@Keeppublic String name = "CodingMaster";public int getAge() {return age;}public void setAge(int age) {this.age = age;}private void cry(){}
}
重新混淆查看结果:
我们的name变量被成功的保留了,同理如何保留被sex变量呢?这里就不买关子了,直接给出答案,为sex添加@Keep注解就可以了,持怀疑态度的同学👨🎓可以自己去验证。
细心的同学可能已经发现,Test类里面的方法都被混淆了,怎样指定某个方法不被混淆呢?
然后为cry()方法添加@Keep注解,重新混淆查看结果:
有没有很简单的感觉呢?哪里不混淆@Keep哪里,再也不用为混淆头疼了!
@RequiresPermission
限定字段,方法需要某个权限,如果没有,编译器会提醒
public class Util1 {@RequiresPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
public String run() {return null;
}
使用:
看到编译器报错,我们点击一下 Add permission check , 编译器会自动帮我们补全代码
class MainActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)if (ActivityCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {// TODO: Consider calling// ActivityCompat#requestPermissions// here to request the missing permissions, and then overriding// public void onRequestPermissionsResult(int requestCode, String[] permissions,// int[] grantResults)// to handle the case where the user grants the permission. See the documentation// for ActivityCompat#requestPermissions for more details.return}Util1().run()}}
- @RequiresPermission(permision)
- @RequiresPermission(allOf={permision1,perminsion2})
- @RequiresPermission(anyOf={permision1,perminsion2})
@Deprecated
CallSuper
子类重写某个方法时,要求调用super,可以使用该注解
@IntRange
//限定只能传1-4fun run(@IntRange(from = 1, to = 4) num: Int) {}
@FloatRange
用法上和 IntRange 一样,
//限定只能传1-4fun run(@FloatRange(from = 1.0, to = 4.0) num: Float) {}
源码如下:
其中,fromInclusive 是否包含 from ,toInclusive 是否包含 to , 其实就是左包含,右包含的意思。
@CheckResult
假设你定义了一个方法返回一个值,你期望调用者用这个值做些事情,那么你可以使用@CheckResult注解标注这个方法,强制用户定义一个相应的返回值,使用它!
首先定义 CallSuperT ,定义一个retrunI方法返回一个int类型
public class CallSuperT {@CheckResultpublic int retrunI(){return 1;}
}
正确调用:
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);CallSuperT callSuperT = new CallSuperT();int returns = callSuperT.retrunI();}
}
如果这里去掉返回类型的定义对象:int returns则会抛出异常
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);CallSuperT callSuperT = new CallSuperT();callSuperT.retrunI();}
}
错误提示结果:
@size
定义长度大小,可选择最小和最大长度使用
public class MainActivity extends AppCompatActivity {@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);testDo("");testDo("111");testDo("1");}private void testDo(@Size(min = 1,max = 2)String s){Log.e("tag","-------->"+s);}
}
错误提示结果:
这里size定了一个最小和最大长度,所以只有testDo(“1”)符合条件,其他调用都抛出了异常
总结:
注解的作用:
- 提高我们的开发效率
- 更早的发现程序的问题或者错误
- 更好的增加代码的描述能力
- 更加利于我们的一些规范约束
- 提供解决问题的更优解