探索注解之 Android 中的注解

Java 中有注解,Android 中也有注解,本文将介绍下 Android 的注解。这是探索注解系列文章的最后一篇:

引入注解库

Android 的注解库并没有包括在 framework 中,它被独立成一个单独的库。通常情况下,只需要编辑build.gradle文件就可引入:

1
2
3
dependencies {
compile 'com.android.support:support-annotations:22.2.0'
}

如果已经引入了appcompat库的话,就不需要额外引入support-annotations,因为appcompat包含了注解库。

对于 Android 应用或者 Android Library 模块的话,上面的工作就可以引入注解库。如果想要在纯粹的 Java 模块中(即在 gradle 文件中apply plugin: 'java')引入注解库的话,还要显式地标明 SDK 的 repositories,因为注解库不在 jcenter 上(Android 的 gradle plugin 默认包含这个依赖,而 Java plugin 并没有)。

1
2
3
4
repositories {
jcenter()
maven {url '<your-SDK-path>/extras/android/m2repository'}
}

Null 相关注解

1
2
@Nullable 注解的元素可能为 null
@NonNull 注解的元素不能为 null

这两个注解可以修饰下面三种元素:

  • 成员属性

  • 方法参数

  • 方法返回值

合理使用这两个注解可以增加代码的可读性,减少一些 NullPointerException。

@NonNull 注解检测生效的条件:

  • 显式地使用 null

  • 使用前已经判断了参数为 null

资源类型注解

Android 中所有的资源 id 都是 int 类型,所以代码期望 string id 但是容易传入 drawable id。而使用 @StringRes 注解的话,编译器就会帮我们检测,如果不是传入R.stringid 就会标注出来。

下面是 TextView 的一个例子:

1
2
3
public final void setText(@StringRes int resid) {
setText(getContext().getResources().getText(resid));
}

Android 中的资源注解有::AnimRes, AnimatorRes, AnyRes, ArrayRes, AttrRes, BoolRes, ColorRes, DimenRes, DrawableRes, FractionRes, IdRes, IntegerRes, InterpolatorRes, LayoutRes, MenuRes, PluralsRes, RawRes, StringRes, StyleRes, StyleableRes, TransitionRes, XmlRes。

其中有个特殊的注解 @AnyRes 表示修饰的元素必须是资源类型,但具体是哪种类型不确定。在framework中有用到Resources#getResourceName(@AnyRes int resId)

IntDef/StringDef: 替代枚举

Enum 的内存占用是静态常量的 2 倍,Google 推荐使用IntDefStringDef来代替枚举。

下面以 IntDef 说明下:

1
2
3
4
5
6
7
8
9
10
11
12
@Retention(RetentionPolicy.SOURCE)
@IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
public @interface NavigationMode {}
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;
...
public abstract void setNavigationMode(@NavigationMode int mode);
@NavigationMode
public abstract int getNavigationMode();

StringDef 的用法与 IntDef 类似,IntDef 还可以指定整型值作为标志位(即可以使用 | 和 & 操作符):

1
2
3
4
5
6
7
8
9
@IntDef(flag=true, value={
DISPLAY_USE_LOGO,
DISPLAY_SHOW_HOME,
DISPLAY_HOME_AS_UP,
DISPLAY_SHOW_TITLE,
DISPLAY_SHOW_CUSTOM
})
@Retention(RetentionPolicy.SOURCE)
public @interface DisplayOptions {}

线程注解

Android 中有四个线程相关的注解:

  • @UiThread,通常可以等同于主线程,标明方法或者构造函数需要在 UiThread 执行,例如 View 类就使用这个注解

  • @MainThread,主线程

  • @WorkerThread,工作者线程,一般为后台线程,例如 AsyncTask 中的 doInBackground 方法

  • @BinderThread,标明方法必须要在 BinderThread 执行

AsyncTask 中的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
new AsyncTask<Void, Void, Void>() {
//doInBackground is already annotated with @WorkerThread
@Override
protected Void doInBackground(Void... params) {
return null;
updateViews();//error
}
};
@UiThread
public void updateViews() {
Log.i(LOGTAG, "updateViews ThreadInfo=" + Thread.currentThread());
}

注意,下面这种情况不会出现错误提示

1
2
3
4
5
6
7
new Thread(){
@Override
public void run() {
super.run();
updateViews();
}
}.start();

因为编译器的判断规则是,updateViews的线程注解和run方法的不一致才会提示。这里 run 方法没有线程注解,所以不会提示。

RGB Color 值

上面提到了可以使用@ColorRes来限定颜色资源 id,但是当我们需要一个实际的 RGB 或 ARGB 的颜色值,就需要用@ColorInt注解了。

TextView 中的例子:

1
2
3
4
public void setTextColor(@ColorInt int color) {
mTextColor = ColorStateList.valueOf(color);
updateTextColors();
}

值约束:@Size、@IntRange、@FloatRange

对于数组,集合或者字符串,可以使用@Size来约束长度或者大小,下面有几个例子:

  • 数组长度必须为 2: @Size(2)

  • 集合不能为空: @Size(min = 0)

  • 字符串最多只能有 23 个字符: @Size(max = 23)

  • 数组的长度必须为 3 的倍数: @Size(multiple = 3)

IntRange 和 FloatRange 是用来限定区间范围的,如果参数或变量为 float 或者 double 类型,可以使用@FloatRange约束取值范围:

1
public void setAlpha(@FloatRange(from = 0.0, to= 1.0) float alpha) {...}

权限相关

@RequiresPermission注解用来表明需要的一个或多个权限。

需要一个单独权限:

1
2
@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public abstract void setWallpaper(Bitmap bitmap) throws IOException;

需要多个权限的任意一个:

1
2
3
4
@RequiresPermission(anyOf = {
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION})
public abstract Location getLastKnownLocation(String provider);

需要多个权限:

1
2
3
4
@RequiresPermission(allOf = {
Manifest.permission.READ_HISTORY_BOOKMARKS,
Manifest.permission.WRITE_HISTORY_BOOKMARKS})
public static final void updateVisitedHistory(ContentResolver cr, String url, boolean real){}

需要单独的读权限或写权限:

1
2
3
@RequiresPermission.Read(@RequiresPermission(Manifest.permission.READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(@RequiresPermission(Manifest.permission.WRITE_HISTORY_BOOKMARKS))
public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");

重写方法:@CallSuper

重写的方法必须要调用 super 方法。

使用@CallSuper注解,可以强制在重写时必须调用父类的方法,例如 Activity 的onCreate方法。

返回值:@CheckResult

@CheckResult只能注解方法,如果执行一个方法,却没有使用返回值,就会有错误提示,这也说明没有正确地使用该方法。

1
2
3
4
5
6
7
8
@CheckResult
public String trim(String s) {
return s.trim();
}
...
s.trim(); // this is probably an error
s = s.trim(); // ok

@Keep

在 Android 编译生成 APK 的环节,通常需要设置minifyEnabled为 true 来混淆代码和删除无用代码。

但是但是出于某一些目的,我们需要不混淆某部分代码或者不删除某处代码,除了配置复杂的 Proguard 文件之外,我们还可以使用 @Keep 注解。

1
2
3
4
@Keep
public static int getBitmapWidth(Bitmap bitmap) {
return bitmap.getWidth();
}

注意需要 Gradle plugin 版本在 2.2 以上。

参考文章:

END
Johnny Shieh wechat
我的公众号,不只有技术,还有咖啡和彩蛋!