利用注解改善 Android 代码检查
译自 Google Android Studio 用户指南 Improve Code Inspection with Annotations
使用 Lint 这样的代码检查工具可以帮助你发现问题和改进代码,但是代码检查工具能力有限,比如:Android 资源 ID 一律使用整型(int
)标识字符串、图片、颜色等,而在应该指定颜色资源的地方,即使使用了字符串资源代码检查工具也不会报警。这样,即使使用了代码检查工具,你的应用仍可能出现显示错误甚至会无法运行。
注解代码检测工具(比如 Lint)提供了指引,有助于发现更细小的问题。它们就像元数据标签,可以附加给变量、参数和返回值,以检查方法返回值、传入的参数、本地变量和字段是否符合要求。借助注解代码检查工具就可以发现空指针异常和资源类型冲突这样的问题。
Android 官方库 Annotations Support Library 提供了多种多样的注解定义。你可以通过包 访问它。
向你的项目添加注解
要让你的项目支持 Android 的注解,必须为你的库或者应用添加 support-annotations
依赖。每当你进行代码检查操作或 lint
任务注解就会被自动检测。
添加注解支持库依赖
注解支持库是支持库(Android Support Repository)的一部分。要为项目添加依赖,就必须下载支持库并且在你的 build.gradle
文件中添加 support-annotations
依赖。
- 打开
SDK Manager
:点击工具栏的SDK Manager
图标 {% img https://developer.android.google.cn/studio/images/buttons/toolbar-sdk-manager.png %} 或依次进入菜单Tools -> Android -> SDK Manager
- 点击
SDK Tools
选项 - 展开
Support Repository
并且勾选Android Support Repository
- 点击 OK
- 继续随着提示来安装勾选的包
- 添加
support-annotations
依赖:在build.grale
文件的dependencies
块中加入以下语句
|
|
你下载的库版本可能更高,一定要确认语句中的版本号和步骤 3 中的一致。
- 在工具栏的同步通知中,点击
Sync Now
如果你在库模块中使用这些注解,它们会作为 AAR 工件(AAR artifact)的一部分以 XML 格式包含在 annotations.zip
文件中。添加 support-annotations
依赖并不会为你的库的下游使用者引入依赖。
如果你的项目模块不使用 Android 插件的 Gradle 模块 (com.android.application
or com.android.library
) ,而使用 Java 插件,要使用这些注解必须显式地添加 SDK 库依赖,因为 JCenter 仓库中并不包含 Android 地支持库。
|
|
注意:如果你使用了
appcompat
库,就不再需要添加support-annotations
依赖了。因为appcompat
库依赖注解库,所以你可以直接使用这些注解。
想要了解支持库中的全部注解,可以浏览 注解支持库总览页面或语句 import android.support.annotation.
弹出的自动补全信息。
运行代码检查功能
Android Studio 的代码检查功能能够检查注解并自动进行 Lint 检查,使用它想要从菜单栏中选择 Analyze -> Inspect Code
。Android Studio 会在消息窗口展示冲突信息,提示潜在问题,同时给出可行的建议。
你也可以通过命令行运行 lint
命令进行强制的注解检查。虽然这对于持续集成服务器来说很有用,你也要注意命令行 lint
不会强制执行空值注解检查(只有 Android Studio 会)。想要获取更多 Lint 检查的信息,可以访问 Improve Your Code with Lint。
空值注解
使用 @Nullable
和 @NonNull
可以检查变量、参数和返回值是否是空值。@Nullable
注解表明变量、参数或这返回值可以为空,而 @NonNull
恰恰相反。
举例来说,如果一个方法的参数被 @NonNull
标记,而你要传入一个可能为空的本地变量,那么在构建的时候就会产生警告信息,提醒你出现了非空冲突。反过来说,如果一个方法的返回值被 @Nullable
标记,尝试访问它却不检查它是否是空值时就会出现空值警告。只有当一个方法的放回值必须进行显式的空值检查时,才用 @Nullable
标记它。
下面的代码使用 @NonNull
注解检查参数 context
和 attrs
不是空值,同时检查方法 onCreateView()
不返回空值。
|
|
可空性分析
Android Studio 支持进行可空性分析来自动推断并在代码中插入空值注解。可空性分析会扫描代码中的所有方法层次结构中的约定来检测:
- 对会返回空值的方法的调用
- 不应该返回空值的方法
- 可为空的变量(比如字段、本地变量和参数)
- 不能为空的变量
然后分析程序会自动在需要的位置插入合适的注解。
要使用 Android Studio 的可空性分析功能,需要使用菜单项
Analyze > Infer Nullity
。Android Studio 会在检测到的位置插入 Android 注解 。而在进行可空性分析之后,最好再亲自校验加入的注解。
注意:当加入空值注解的时候,自动完成也许会建议使用 Intellij 的
@Nullable
和@NonNull
,而不是 Android 的空值注解,并且可能自动导入相应的库。然而,Android Studio 的 Lint 检查工具只会检测 Android 空值注解。再校验注解的时候,要确认使用的时 Android 空值注解,这样进行代码检查时 Lint 检查工具才能正常工作。
资源注解
对资源类型的验证非常有益,因为 Android 项目中对资源的引用(比如图片和字符串)总是以整型传递。如果参数需要传入特定类型的资源(比如图片),传递给它的实际是整型数值,实际上可能指向不同类型的资源(比如字符串)。
举例来说,加入 @StringRes
注解来检查资源参数类型为字符串,代码如下:
|
|
如果传入的资源类型不是字符串,在进行代码检查时就会产生警告信息。
其他的资源类型注解,比如 @DrawableRes
、@DimenRes
、@ColorRes
和 @InterpolatorRes
,也可以用同样的方式使用和检查。如果你的参数需要支持多种资源类型,也可以在一个参数之前加上多个注解。@AnyRes
注解代表注解的参数可被传入任何类型的资源。
虽然可以使用 @ColorRes
来标记参数应为颜色类型资源,以 RRGGBB
或 AARRGGBB
表示颜色的整型数却不会被认定为颜色资源。应使用 @ColorInt
注解标记这样的颜色整型数。这样,如果传入颜色资源 ID(比如 android.R.color.black
)而不是颜色整型数,构建时就会提示错误。
线程注解
线程注解检查方法是否由特定种类的线程调用。库中支持以下种类的线程注解:
注意:
@MainThread
和@UiThread
可以互换,所以你可以在@MainThread
标记的方法调用@UiThread
方法,反之亦然。然而,UI 线程也可能和主线程不同,比如在不同线程上由多个视图的系统应用。因此,你应该使用@UiThread
注释与应用程序视图层次结构有关的方法,而且只用@MainThread
注释与应用程序生命周期有关的方法。
如果一个类的所有方法都使用同样的线程要求,可以只给类加上一个线程注解来表明它的所有成员都从同种线程调用。
线程注释常用来验证 AsyncTask
类中的方法重写,因为这个类既需要执行后台操作,还要仅在 UI 线程中显示结果。
值约束注解
使用 @IntRange
、@FloatRange
和 @Size
来验证参数的取值。加给参数的 @IntRange
和 @FloatRange
注解是最有用的,因为用户可能会得到正常范围之外的值。
@IntRange
注解验证整型或长整型参数在指定的范围之内。下面的例子保证了参数 alpha
的取值范围在 0 到 255 之间:
|
|
@FloatRange
注解检查类型为浮点数或双精度浮点数的参数在指定的实数范围内。下面的例子保证了参数 alpha
的取值范围在 0.0 到 1.0 之间:
|
|
@Size
注解检查集合或者数组的大小,以及字符串的长度。@Size
注解可以用来验证下面的属性:
- 最小尺寸(比如
@Size(min=2)
) - 最大尺寸(比如
@Size(max=2)
) - 精确尺寸(比如
@Size(2)
) - 尺寸必须是指定数字的倍数(比如
@Size(multiple=2)
)
举例来说,@Size(min=1)
检查集合是否非空,而 @Size(3)
验证数组刚好有 3 个元素。下面的例子保证数组 location
至少有 1 个元素:
|
|
权限注解
使用 @RequiresPermission
注解来验证方法中对权限的请求。要单个权限的有效性,请使用 anyOf
属性。要检查一组权限,请使用 allOf
属性。以下示例对 setWallpaper()
方法的注解,确保方法的调用者具有 permission.SET_WALLPAPERS
权限:
|
|
下面的示例要求 copyFile()
方法的调用者对外部存储具有读取和写入权限:
|
|
对于 intent 的权限检查,可将权限要求放置在定义 intent 操作(action)名称的字符串字段上:
|
|
对于需要单独的读写权限的内容提供程序的权限注解,请在 @RequiresPermission.Read
或 @RequiresPermission.Write
注解中包含各自的权限要求:
|
|
间接权限
当方法参数的权限取决于传入参数的值时,仅对参数使用 @RequiresPermission
注解,而不用列出具体的权限。例如,startActivity(Intent)
方法对传递给方法的 intent 使用了间接权限:
|
|
当使用间接权限时,构建工具会进行数据流分析,以检查传递到方法中的参数是否具有任何 @RequiresPermission
注解。然后强制检查方法的参数注解要求的权限。在 startActivity(Intent)
示例中,当没有适当权限的 intent 传入方法时,Intent 类中的注释导致对 startActivity(Intent)
方法的无效使用结果的警告,如图 1 所示。
{% img https://developer.android.google.cn/studio/images/write/indirect-permissions-warning_2-2_2x.png “图 1. 从 startActivity(Intent)
方法上的间接权限注解生成的警告。” %}
构建工具根据 Intent
类中相应 intent 操作名称上的注解在 startActivity(Intent)
上生成警告:
|
|
如有必要,可以在注释方法的参数时,把 @RequiresPermission
替换为 @RequiresPermission.Read
和(或) @RequiresPermission.Write
。但是,对于间接权限来说, @RequiresPermission
不应与读取或写入权限注解结合使用。
返回值注解
使用 @CheckResult
注解来验证方法的结果或返回值是否被实际使用。应使用 @CheckResult
注解来澄清可能引起混淆的方法,而不是标记每个非 void
方法。例如,新手 Java 开发人员经常错误地认为 <String>.trim()
会从原始字符串中删除空格。@CheckResult
注解会标记出不对 <String>.trim()
的返回值执行任何操作的地方。
以下示例给 checkPermissions()
方法加注解,以确保方法的返回值被引用。它还建议开发人员使用 enforcePermission()
方法作为替代:
|
|
父类实现调用注解
请使用 @CallSuper
注解来验证重写的方法调用了该方法的父类实现。以下示例对 onCreate()
方法的注解,确保任何重写的方法都要调用 super.onCreate()
:
|
|
TypeDef 注解
使用 @IntDef
和 @StringDef
可以创建整数和字符串集的枚举注解,以验证对它们的代码引用。TypeDef 注解确保参数、返回值或字段引用一组特定的常量。它们还令代码补完能够自动提供合适的常量。
TypeDef 注解使用 @interface
声明新的枚举注解类型。@IntDef
和 @StringDef
放在 @Retention
注解之后标识出新注解,同时它们也是定义枚举类型所必需的注解。@Retention(RetentionPolicy.SOURCE)
告诉编译器不要将枚举注解存储在 .class 文件中。
以下示例说明了如何创建注解以确保传入方法参数的值只引用定义好的常量:
|
|
构建此代码时,如果 mode
参数不引用定义的常量(NAVIGATION_MODE_STANDARD
、NAVIGATION_MODE_LIST
或 NAVIGATION_MODE_TABS
)之一,则会产生警告。
允许组合常量与标志
如果用户能组合定义的常量与标志(例如|、&、^ 等),你可以在注解中使用 flag
属性,以检查参数或返回值是否引用了有效的模式。以下示例定义 DISPLAY_
常数列表创建 DisplayOptions
注释:
|
|
当使用注解标志构建代码时,如果注解修饰的参数或返回值没有引用有效的模式,则会生成警告。
代码访问权注解
使用 @VisibleForTesting
和 @Keep
注解标明方法、类或字段的可访问权。
@VisibleForTesting
表示为了方便代码的测试注解的代码块比必要情况更易访问。
@Keep
确保在构建时的压缩行为不会删除注解标记的元素。它通常被添加到通过反射访问的方法和类,以防止编译器认为代码未使用从而“优化”这些代码。