使用注解(Annotations)改进代码检验

来自谷歌官方文档的翻译。原文地址

使用Lint等工具进行代码检查可以帮助你查找问题改善代码质量,但是这些工具只能进行推断并不能真正进行检查。因为Android使用一个int类型作为ID,表示字符串,图片,颜色和其他类型的资源,这时如果你在一个需要使用颜色资源ID的地方使用了一个字符串资源ID,代码检查工具是不会报错的,但是你的应用仍然会发生绘制错误或者是不能运行。

注解(Annotations)就是让你给代码检查工具Lint等给一个提示,让它们注意检查这些微妙的代码问题。这些注解就好像元数据标签(mentadata tags)一样和变量,参数,返回值绑定在一起,检查它们是否合法。当运行代码检查工具的时候,注解就会帮助你检查比如空指针,资源类型冲突等问题。

Android通过Annotations Support Library支持多种注解,你可以通过android.support.annotation包获取这些注解。

在工程中添加注解


在工程中打开注解功能,添加support-annotations依赖。你添加的任何注解都会代码检查工具运行时或者lint task时进行检查。

添加注解功能的工程依赖

Support Annotations libraryAndroid Support Repository的一部分。所以你必须先下载support repository,然后在build.gradle中添加support-annotations依赖。

  1. 在工具栏中点击 SDK Manager

    ,或者选择 Tools > Android > SDK Manager,打开 SDK Manager

  2. 点击 SDK Tools 标签.

  3. 展开 Support Repository, 并选中 Android Support Repository

  4. 点击 OK.

  5. 安装向导中点击 Continue 直到完成安装.

  6. 将以下代码添加到 build.gradle 文件中,完成添加 support-annotations 依赖:

     dependencies { compile 'com.android.support:support-annotations:24.2.0' } 
    

    这里使用的版本可能低于你下载的版本,所以这里指定的版本号必须和你在第三步中下载的版本号保持一致。

  7. 最后点击工具栏或者通知中的 Sync Now

如果你自己的库模块中使用了annotations,那么annotations就已经以XML的方式存在于AAR(Android Archive (AAR) artifact)文件的annotations.zip中了。使用了你的库的用户就没有必要再以这种添加依赖的方式添加这个模块了。

如果你想用Gradle Java plugin这种方式代替默认的Android plugin for Gradle (com.android.application 或 com.android.library),那你必须明确指定SDK库的位置,因为Android支持库并不支持JCenter

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

注意:如果你使用了appcompat库,那你同样不需要添加 support-annotations 依赖,因为 appcompat 库已经添加过这个依赖了,你可以直接使用annotations。

Android支持库中完整的注解列表你可以通过Support Annotations library reference查询,或者利用代码补全功能,在输入了import android.support.annotation.语句后出现的可用项中查看。

运行代码检查功能

在 Android Studio 的工具栏中选择 Analyze > Inspect Code ,启动代码检查功能,包括确认注解的有效性和Lint自动检查两部分。Android Studio会显示冲突信息,标记出在代码中潜藏的问题,并且给出相应的解决解决建议。
你也可以用命令行启动lint任务,这对持续集成服务器发现问题很有帮助,但是要注意,这样启动的lint任务并不能检查nullness注解,只有Android Studio才能具备这个共轭能。关于Lint检查的的问题,请看使用Lint改善你的代码

注意,即使注解冲突产生了警告,但是这些警告并不会组织代码的编译。

空值注解(Nullness Annotations)


@Nullable 注解表示变量,参数,返回值可以null, @NonNull 注解表示变量,参数,返回值不能null

如果一个值为null的变量,被传递到了一个参数被标记为@NonNull的方法中,这时编译就会产生一个non-null的警告;另外一方面,如果引用一个返回值被标记为@Nullable的方法,并且你没有对返回结果进行是否为空的检查,那么就会受到一个nullness的警告。只有当你想提醒方法的使用者,在每次使用方法前都要明确地进行非空检查时,才能使用@Nullable注解标记方法的返回值。
下面的例子使用了@NonNull注解标记了contextparameters两个参数,表示要检查传入的这两个参数的值是不能为空,同时还要检查onCreateView()方法自身的返回值不能为空

import android.support.annotation.NonNull;
...

/** Add support for inflating the <fragment> tag. **/
@NonNull
@Override
public View onCreateView(String name, @NonNull Context context,
  @NonNull AttributeSet attrs) {
  ...
  }
...

空值分析(Nullability Analysis)

Android Studio支持空值分析(nullability analysis)去自动推断并且在代买中插入空值注解(nullness annotations)。空值分析会扫描所有方法层次结构中的调用关系,去检查:

  • 调用的方法可以返回空
  • 调用的方法不能返回空
  • 变量,字段,局部变量,参数等可以为空
  • 变量,字段,局部变量,参数等不能为空

分析完后会在检查的位置自动插入适当的空值注解。

在Android Studio中选择Analyze > Infer Nullity开启空值分析。Android Studio会在检测的地方插入Android版本的 @Nullable@NonNull 注解。下面是一些很好的实践经验:

注意:当加入空值注解的时候,代码补全功能会建议我们使用 IntelliJ版本的 @Nullable and @NotNull 注解来代替Android版本的注解,同时也会自动引入相应的包。但是 Android Studio Lint 功能只会检查Android版本的注解。当确认你的注解的时候,一定要检查你的工程使用的是Android版本的注解,这样Lint功能才能正常运行。(PS:Android版本的是 @NonNull,IntelliJ的是@NotNull@Nullable写法是一样的)

资源注解(Resource Annotations)


确认资源类型非常有用,因为Android对于资源的引用,比如drawablestring,都是用整数类型进行传递的。如果代码期待接收特定的资源类型,比如Drawable,就可以把该资源引用的int值传递过去。但是实际上也有可能错传了一个R.string资源的int值过去。所以确认资源的类型很有用。

我们可以添加了一个@StringRes注解,去检查参数的是不是一个R.string类型的资源:

public abstract void setTitle(@StringRes int resId) { … }

如果参数不是一个R.string类型的资源,那么在代码检查期间就会产生一个警告。

其他[@DrawableRes][drawableres],[@DimenRes][dimenres],[@ColorRes][colorres],[@InterpolatorRes][interpolatorres]等资源注解都可以按这种格式使用。如果你的参数支持多个资源格式,你可以对其添加多个资源注解。[@AnyRes][anyres]注解表示该菜蔬可以是任意一种R资源格式。

即使你使用了[@ColorRes][colorres]指定了一个资源类型的参数,但是用RRGGBBAARRGGBB表示的颜色整数值却并不会被认可。同样用[@ColorInt][colorint]指定的资源也只认可能被解析的颜色整数值。编译工具会标记出这些不正确的代码。

[stringres]:
[drawableres]: https://developer.android.com/reference/android/support/annotation/DrawableRes.html
[dimenres]:https://developer.android.com/reference/android/support/annotation/DimenRes.html
[colorres]:https://developer.android.com/reference/android/support/annotation/ColorRes.html
[interpolatorres]:https://developer.android.com/reference/android/support/annotation/InterpolatorRes.html
[anyres]:https://developer.android.com/reference/android/support/annotation/AnyRes.html
[colorint]:https://developer.android.com/reference/android/support/annotation/ColorInt.html

线程注解(Thread Annotations)


线程注解用来检查方法是不是在一个特定的线程中被调用。支持以下注解

注意:编译工具将@MainThread@UiThread看成是可互换的,所以你可以在标注为@MainThread的方法中调用@UiThread的方法,反之亦然。但是,在不同线程上有多个视图的系统应用程序的情况下,UI线程可能不等同于主线程。因此,您应该使用@UiThread注解与应用程序视图层次结构相关的方法,并使用@MainThread注解仅与应用程序生命周期相关联的方法。

如果类中的所有方法都共享相同的线程,则可以向类添加单个线程注解,以验证类中的所有方法是否都从同一类型的线程中被调用。

线程注解的一个常见用法是验证AsyncTask类中被覆盖的方法,因为此类在后台执行,并仅在UI线程上发布结果。

值约束注解(Value Constraint Annotations)


使用@IntRange@FloatRange@Size注解验证传进来的参数的值。当用户可能输入不在范围内的参数时,@IntRange@FloatRange非常有用。

@IntRange注解验证整数或长整数参数的值是否在指定范围内。以下示例确保alpha参数包含从0到255的整数值:

public void setAlpha(@IntRange(from=0,to=255) int alpha) { … }

@FloatRange注解检查floatdouble类型参数值是否在浮点值的指定范围内。以下示例确保alpha参数包含从0.0到1.0的浮点值:

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

@Size注解检查集合或数组的大小,以及字符串的长度。 @Size注解可用于验证以下数量:

  • 最大尺寸 (如 @Size(min=2))
  • 最小尺寸 (如 @Size(max=2))
  • 精确尺寸 (如 @Size(2))
  • 尺寸的倍数(如 @Size(multiple=2))

例如,@Size(min=1)检查集合是否为空,@Size(3)验证数组是否包含有三个值。以下示例确保局部数组变量至少包含一个元素:

int[] location = new int[3];
button.getLocationOnScreen(@Size(min=1) location);

权限注解(Permission Annotations)


使用@RequiresPermission注解来验证方法调用者的权限。要从列表中检查单个权限的有效权限,请使用anyOf属性。 要检查一组权限,请使用allOf属性。以下示例注解setWallpaper()方法,以确保方法的调用者具有permission.SET_WALLPAPERS权限:

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

此示例要求copyFile()方法的调用者拥有读写外部存储的权限:

@RequiresPermission(allOf = {
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE})
public static final void copyFile(String dest, String source) {
    ...
}

对于意图(intents)的权限,将权限要求放置在定义意图操作名称的字符串字段上:

@RequiresPermission(android.Manifest.permission.BLUETOOTH)
public static final String ACTION_REQUEST_DISCOVERABLE =
        "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE";

对于需要对读写访问具有单独权限的内容提供程序(content providers)的权限,请在@RequiresPermission.Read@RequiresPermission.Write注解中包含每个权限要求:

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

间接权限(Indirect Permissions)

当权限取决于提供给方法参数的特定值时,只对该参数使用@RequiresPermission注解,而不用列出特定的权限。 例如,startActivity(Intent) 方法对传递给方法的intent参数就使用了间接权限:

public abstract void startActivity(@RequiresPermission Intent intent, @Nullable Bundle) {...}

当你使用间接权限时,构建工具会执行数据流分析,以检查传递给方法的参数是否有@RequiresPermission注解。 然后他们从方法本身的参数强制执行任何现有注解。 在startActivity(Intent)示例中,当没有适当权限的意图传递给方法时,Intent类中的注解导致对startActivity(Intent)的无效使用的结果警告,如图1所示。

构建工具从Intent类中相应意图操作名称上的注解在startActivity(Intent)上生成警告:

@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
@RequiresPermission(Manifest.permission.CALL_PHONE)
public static final String ACTION_CALL = "android.intent.action.CALL";

有时,您可以在注解方法的参数时,用@RequiresPermission替换@RequiresPermission.Read@RequiresPermission.Write。 但是,对于间接权限,@RequiresPermission不应与读取或写入权限注解混合使用。

返回值注解(Return Value Annotations)


使用@CheckResult注解来验证方法的结果或返回值实际上是否被使用。在容易混淆的方法结果上添加@CheckResult注解用以区分,而不是对每个非void方法都进行注解。在容易混淆的方法结果上添加@CheckResult注解用以区分,而不是对每个非void方法都进行注解。 例如,Java开发�新手经常错误地认为<String>.trim()是从原始字符串中删除所有空格。对方法使用包含<String>.trim()@CheckResult注解,�这样调用者就不用对方法的返回值�进行任何操作了。

以下示例中,@CheckResult注解了[checkPermissions()](https://developer.android.com/reference/android/content/pm/PackageManager.html#checkPermission(java.lang.String, java.lang.String))方法,以确保方法的返回值实际被引用。 它还建议开发人员将[enforcePermission()](https://developer.android.com/reference/android/content/ContextWrapper.html#enforcePermission(java.lang.String, int, int, java.lang.String))方法作为一种替代方案:

@CheckResult(suggest="#enforcePermission(String,int,int,String)")
public abstract int checkPermission(@NonNull String permission, int pid, int uid);

调用父类方法注解(CallSuper Annotations)


使用@CallSuper注解来验证该方法是否调用了被覆盖的父类�方法。以下示例中,onCreate()方法使用了该注解,以确保任何覆盖了这个方法的实现都必须调用super.onCreate)

@CallSuper
protected void onCreate(Bundle savedInstanceState) {
}

类型定义注解(Typedef Annotations)


你可以使用@IntDef@StringDef注解,来创建一个整数或者字符串集合的枚举类型。Typedef注解确保特定参数,返回值或字段只能使用特定的一组常量。它们还可以使代码拥有自动补全功能。

Typedef注解使用@interface声明新的枚举注解类型。@IntDef@StringDef以及@Retention注解一起标注新的注解它,并且它们三个是定义一个枚举类型所必需的。@Retention(RetentionPolicy.SOURCE)注解告诉编译器不要将被标记了的枚举类型数据存储在.class文件中。

以下示例说明了创建这种注解的步骤,以确保作为方法参数传递的值是一组已经定义好的常量集合中的一个:

import android.support.annotation.IntDef;
...
public abstract class ActionBar {
...
// Define the list of accepted constants and declare the NavigationMode annotation
@Retention(RetentionPolicy.SOURCE)
@IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
public @interface NavigationMode {}

// Declare the constants
public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;

// Decorate the target methods with the annotation
@NavigationMode
public abstract int getNavigationMode();

// Attach the annotation
public abstract void setNavigationMode(@NavigationMode int mode);

如果mode参数的值不是已经定义的(NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, or NAVIGATION_MODE_TABS)其中的一个,编译时就会受到警告。

你还可以将 @IntDef@IntRange 一起使用,用来限定一个整数值既是给定的一组常量中的一个,同时也是一个给定范围内的值。

将常量和标志位一起使用(Enable combining constants with flags)

如果用户想将有效的常量与标志位(例如|, &, ^等)组合使用,你可以结合flag属性定义这个注解,以检查参数或返回值是否是合法的样式。以下示例用一系列DISPLAY_常数创建了DisplayOptions注解:

import android.support.annotation.IntDef;
...

@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 {}

...

当你使用注解标志构建代码时,如果修饰的参数或返回值不是有效的样式时,则会生成警告。

代码访问权限注解(Code Accessibility Annotations)


使用@VisibleForTesting@Keep注解来表示方法,类或字段的可访问性。

@VisibleForTesting注解表示,代码测试时候,被注解了的这段代码比其所声明的,有更大的可见性。(比如声明为private,测试时就变成了public)。

@Keep注解确保被标注的元素在编译代码中压缩代码资源的时候不会被删除。这个标签的典型应用就是添加在要被反射调用(reflection)的类或者方法上面,确保编译器不会把这些方法或者类当做是无用的资源而被优化掉(删除)。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容