使用注解改进代码检查

使用 lint 等代码检查工具可以帮助您发现问题并改进代码,但检查工具的推断能力毕竟有限。例如,Android 资源 ID 使用 int 来标识字符串、图形、颜色和其他资源类型,因此当您在应该指定颜色资源的地方误指定了字符串资源时,检查工具无法识别。这意味着即便您使用了代码检查,您的应用也可能无法正确渲染,甚至根本无法运行。

通过注解,您可以向 lint 等代码检查工具提供提示,从而帮助检测这些更为细微的代码问题。注解作为元数据标签添加,您可以将其附加到变量、参数和返回值上,以检查方法返回值、传入的参数、局部变量和字段。当与代码检查工具结合使用时,注解可以帮助您检测空指针异常和资源类型冲突等问题。

Android 通过 Jetpack Annotations 库支持多种注解。您可以通过 androidx.annotation 软件包访问该库。

注意:如果模块依赖于某个注解处理器,则必须使用 Kotlin 的 kaptksp 依赖配置,或者 Java 的 annotationProcessor 依赖配置来添加该依赖项。

将注解添加到您的项目中

要在项目中启用注解,请将 androidx.annotation:annotation 依赖项添加到您的库或应用中。当您运行代码检查或 lint 任务时,系统会检查您添加的任何注解。

添加 Jetpack Annotations 库依赖项

Jetpack Annotations 库发布在 Google 的 Maven 仓库中。要将 Jetpack Annotations 库添加到您的项目中,请在 build.gradlebuild.gradle.kts 文件的 dependencies 代码块中包含以下行:

Kotlin

dependencies {
    implementation("androidx.annotation:annotation:1.10.0")
}

Groovy

dependencies {
    implementation 'androidx.annotation:annotation:1.10.0'
}
然后,在出现的工具栏或同步通知中,点击 Sync Now(立即同步)。

如果您在自己的库模块中使用注解,这些注解将作为 Android 归档 (AAR) 工件的一部分以 XML 格式包含在 annotations.zip 文件中。添加 androidx.annotation 依赖项不会为库的下游用户引入任何依赖。

注意:如果您使用的是其他 Jetpack 库,则可能不需要添加 androidx.annotation 依赖项。因为许多其他 Jetpack 库都依赖于 Annotations 库,所以您可能已经可以使用这些注解了。

如需查看 Jetpack 仓库中包含的注解完整列表,请参阅 Jetpack Annotations 库参考文档,或使用自动补全功能显示 import androidx.annotation. 语句可用的选项。

运行代码检查

要从 Android Studio 启动代码检查(包括验证注解和自动 lint 检查),请从菜单中选择 Analyze(分析)> Inspect Code(检查代码)。Android Studio 会显示冲突消息,标记代码与注解冲突的潜在问题,并建议可能的解决方法。

您还可以通过使用命令行运行 lint 任务来强制执行注解。虽然这对于标记持续集成服务器上的问题很有用,但 lint 任务不会强制执行空值注解(在下一节中介绍);只有 Android Studio 会执行此操作。有关启用和运行 lint 检查的详细信息,请参阅使用 lint 检查改进代码

虽然注解冲突会生成警告,但这些警告不会阻止您的应用编译。

空值注解

空值注解在 Java 代码中非常有用,可以强制规定值是否可以为空。它们在 Kotlin 代码中的用处较小,因为 Kotlin 具有内置的、在编译时强制执行的空安全性规则。

添加 @Nullable@NonNull 注解以检查给定变量、参数或返回值的空值状态。@Nullable 注解表示变量、参数或返回值可以为空。@NonNull 表示变量、参数或返回值不能为空。

例如,如果包含 null 值的局部变量作为参数传递给附加了 @NonNull 注解的方法,则构建代码时会生成一个警告,指出存在非空冲突。此外,尝试引用标记为 @Nullable 的方法的结果,但未先检查结果是否为 null,也会生成空值警告。仅当必须对方法的所有使用位置进行显式空值检查时,才对方法的返回值使用 @Nullable

以下示例演示了空安全性。Kotlin 示例代码未利用 @NonNull 注解,因为当指定非空类型时,它会自动添加到生成的字节码中。Java 示例在 contextattrs 参数上利用了 @NonNull 注解,以检查传入的参数值是否不为 null。它还检查了 onCreateView() 方法本身是否不返回 null。

Kotlin

...
    /** Annotation not used because of the safe-call operator(?)**/
    override fun onCreateView(
            name: String?,
            context: Context,
            attrs: AttributeSet
    ): View? {
        ...
    }
...

Java

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

空值分析

Android Studio 支持运行空值分析,以自动推断并将空值注解插入到您的代码中。空值分析会扫描代码中整个方法层次结构的契约,以检测:

  • 调用可能返回 null 的方法。
  • 不应该返回 null 的方法。
  • 可能为 null 的变量(如字段、局部变量和参数)。
  • 不能包含 null 值的变量(如字段、局部变量和参数)。

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

要在 Android Studio 中运行空值分析,请选择 Analyze(分析)> Infer Nullity(推断空值)。Android Studio 会在代码中检测到的位置插入 Android @Nullable@NonNull 注解。运行空值分析后,最好核实一下插入的注解。

注意:添加空值注解时,自动补全功能可能会建议使用 IntelliJ 的 @Nullable@NotNull 注解,而不是 Android 空值注解,并可能自动导入相应的库。但是,Android Studio lint 检查器仅查找 Android 空值注解。在核实注解时,请确保您的项目使用的是 Android 空值注解,以便 lint 检查器在代码检查期间能正确向您发出通知。

资源注解

验证资源类型非常有用,因为 Android 对资源(如 drawablestring 资源)的引用都是作为整数传递的。

对于期望参数引用特定类型资源(如 String)的代码,可能会传入预期的 int 引用类型,但实际上引用了不同类型的资源,例如 R.string 资源。

例如,添加 @StringRes 注解以检查资源参数是否包含 R.string 引用,如下所示:

Kotlin

abstract fun setTitle(@StringRes resId: Int)

Java

public abstract void setTitle(@StringRes int resId)

在代码检查期间,如果参数中未传入 R.string 引用,该注解将生成警告。

对于其他资源类型(如 @DrawableRes@DimenRes@ColorRes@InterpolatorRes),可以使用相同的注解格式添加,并可在代码检查期间运行。

如果您的参数支持多种资源类型,则可以在给定参数上放置多个资源类型注解。使用 @AnyRes 表示带注解的参数可以是任何类型的 R 资源。

虽然可以使用 @ColorRes 指定参数应为颜色资源,但颜色整数(RRGGBBAARRGGBB 格式)不会被识别为颜色资源。请改用 @ColorInt 注解来指明参数必须是颜色整数。构建工具会标记错误的代码,即向带注解的方法传递颜色资源 ID(例如 android.R.color.black)而非颜色整数的代码。

线程注解

线程注解用于检查方法是否从特定类型的线程调用。支持以下线程注解:

构建工具将 @MainThread@UiThread 注解视为可互换的,因此您可以从 @MainThread 方法调用 @UiThread 方法,反之亦然。但是,在具有多个线程上运行多个视图的系统应用的情况下,UI 线程可能与主线程不同。因此,您应该使用 @UiThread 注解与应用视图层次结构关联的方法,并仅使用 @MainThread 注解与应用生命周期关联的方法。

如果类中的所有方法具有相同的线程要求,您可以将单个线程注解添加到该类,以验证类中的所有方法是否都是从相同类型的线程调用的。

线程注解的一个常见用途是验证使用 @WorkerThread 注解的方法或类是否仅从适当的后台线程调用。

值约束注解

使用 @IntRange@FloatRange@Size 注解来验证传入参数的值。@IntRange@FloatRange 最常用于用户容易出错的参数。

@IntRange 注解用于验证整数或长整数参数值是否在指定范围内。以下示例表明 alpha 参数必须包含 0 到 255 之间的整数值:

Kotlin

fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) { ... }

Java

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

@FloatRange 注解用于检查浮点数或双精度浮点数参数值是否在指定的浮点值范围内。以下示例表明 alpha 参数必须包含 0.0 到 1.0 之间的浮点值:

Kotlin

fun setAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float) {...}

Java

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) 验证数组是否包含恰好三个值。

以下示例表明 location 数组必须至少包含一个元素:

Kotlin

fun getLocation(button: View, @Size(min=1) location: IntArray) {
    button.getLocationOnScreen(location)
}

Java

void getLocation(View button, @Size(min=1) int[] location) {
    button.getLocationOnScreen(location);
}

权限注解

使用 @RequiresPermission 注解来验证方法调用者的权限。要检查有效权限列表中的单个权限,请使用 anyOf 属性。要检查一组权限,请使用 allOf 属性。以下示例对 setWallpaper() 方法进行注解,表明该方法的调用者必须拥有 permission.SET_WALLPAPERS 权限:

Kotlin

@RequiresPermission(Manifest.permission.SET_WALLPAPER)
@Throws(IOException::class)
abstract fun setWallpaper(bitmap: Bitmap)

Java

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

以下示例要求 copyImageFile() 方法的调用者既拥有对外部存储的读取访问权限,又拥有对所复制图像中位置元数据的读取访问权限:

Kotlin

@RequiresPermission(allOf = [
    Manifest.permission.READ_EXTERNAL_STORAGE,
    Manifest.permission.ACCESS_MEDIA_LOCATION
])
fun copyImageFile(dest: String, source: String) {
    ...
}

Java

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

对于 Intent 的权限,请将权限要求放在定义 Intent 操作名称的字符串字段上:

Kotlin

@RequiresPermission(android.Manifest.permission.BLUETOOTH)
const val ACTION_REQUEST_DISCOVERABLE = "android.bluetooth.adapter.action.REQUEST_DISCOVERABLE"

Java

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

对于需要分别读取和写入权限的内容提供程序,请将每个权限要求包装在 @RequiresPermission.Read@RequiresPermission.Write 注解中:

Kotlin

@RequiresPermission.Read(RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(RequiresPermission(WRITE_HISTORY_BOOKMARKS))
val BOOKMARKS_URI = Uri.parse("content://browser/bookmarks")

Java

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

间接权限

当权限取决于提供给方法参数的特定值时,请在参数本身上使用 @RequiresPermission,而无需列出特定权限。例如,startActivity(Intent) 方法对传递给该方法的 Intent 使用了间接权限:

Kotlin

abstract fun startActivity(@RequiresPermission intent: Intent, bundle: Bundle?)

Java

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

当您使用间接权限时,构建工具会执行数据流分析,以检查传入方法的参数是否具有任何 @RequiresPermission 注解。然后,它们会将参数上现有的任何注解强制执行到方法本身上。在 startActivity(Intent) 示例中,Intent 类中的注解会导致在向该方法传递缺少适当权限的 Intent 时,对不当使用 startActivity(Intent) 的行为产生警告,如图 1 所示。

图 1.startActivity(Intent) 方法上因间接权限注解而生成的警告。

构建工具会根据 Intent 类中相应 Intent 操作名称上的注解,在 startActivity(Intent) 上生成警告。

Kotlin

@RequiresPermission(Manifest.permission.CALL_PHONE)
const val ACTION_CALL = "android.intent.action.CALL"

Java

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

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

返回值注解

使用 @CheckResult 注解来验证是否确实使用了方法的执行结果或返回值。不要给每个非 void 方法都加上 @CheckResult,而应将其添加到需要明确容易混淆的方法结果的场景中。

例如,Java 开发新手常误认为 <String>.trim() 会从原始字符串中移除空格。用 @CheckResult 注解该方法,可以标记那些调用者未对方法返回值做任何处理的 <String>.trim() 使用情况。

以下示例对 checkPermissions() 方法进行了注解,以检查该方法的返回值是否确实被引用。它还将 enforcePermission() 方法命名为建议开发人员替换的方法。

Kotlin

@CheckResult(suggest = "#enforcePermission(String,int,int,String)")
abstract fun checkPermission(permission: String, pid: Int, uid: Int): Int

Java

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

CallSuper 注解

使用 @CallSuper 注解来验证重写方法是否调用了该方法的父类实现。

以下示例对 onCreate() 方法进行注解,以确保任何重写的方法实现都调用 super.onCreate()

Kotlin

@CallSuper
override fun onCreate(savedInstanceState: Bundle?) {
}

Java

@CallSuper
protected void onCreate(Bundle savedInstanceState) {
}

Typedef 注解

Typedef 注解用于检查特定参数、返回值或字段是否引用了特定的一组常量。它们还支持代码自动补全功能,以自动提供允许的常量。

使用 @IntDef@StringDef 注解创建整数和字符串集的枚举注解,以验证其他类型的代码引用。

Typedef 注解使用 @interface 来声明新的枚举注解类型。@IntDef@StringDef 注解连同 @Retention 一起用于注解新注解,对于定义枚举类型是必需的。@Retention(RetentionPolicy.SOURCE) 注解告诉编译器不要将枚举注解数据存储在 .class 文件中。

以下示例展示了创建注解的步骤,该注解用于检查作为方法参数传递的值是否引用了定义的常量之一:

Kotlin

import androidx.annotation.IntDef
//...
// Define the list of accepted constants and declare the NavigationMode annotation.
@Retention(AnnotationRetention.SOURCE)
@IntDef(NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS)
annotation class NavigationMode

// Declare the constants.
const val NAVIGATION_MODE_STANDARD = 0
const val NAVIGATION_MODE_LIST = 1
const val NAVIGATION_MODE_TABS = 2

abstract class ActionBar {

    // Decorate the target methods with the annotation.
    // Attach the annotation.
    @get:NavigationMode
    @setparam:NavigationMode
    abstract var navigationMode: Int

}

Java

import androidx.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_STANDARDNAVIGATION_MODE_LISTNAVIGATION_MODE_TABS)之一,则会生成警告。

您可以结合使用 @IntDef@IntRange,以指明一个整数既可以是一组给定的常量,也可以是某个范围内的值。

启用带有标志的常量组合

如果用户可以将允许的常量与标志(例如 |&^ 等)结合使用,您可以定义一个带有 flag 属性的注解,以检查参数或返回值是否引用了有效的模式。

以下示例创建了带有有效 DISPLAY_ 常量列表的 DisplayOptions 注解:

Kotlin

import androidx.annotation.IntDef
...

@IntDef(flag = true, value = [
    DISPLAY_USE_LOGO,
    DISPLAY_SHOW_HOME,
    DISPLAY_HOME_AS_UP,
    DISPLAY_SHOW_TITLE,
    DISPLAY_SHOW_CUSTOM
])
@Retention(AnnotationRetention.SOURCE)
annotation class DisplayOptions
...

Java

import androidx.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 {}

...

当您构建带有注解标志的代码时,如果修饰的参数或返回值未引用有效的模式,则会生成警告。

Keep 注解

@Keep 注解确保带注解的类或方法在构建时代码缩减(minification)过程中不会被移除。此注解通常添加到通过反射访问的方法和类中,以防止编译器将这些代码视为未使用。

注意:使用 @Keep 注解的类和方法始终会出现在您的应用 APK 中,即使您从未在应用的逻辑中引用过这些类和方法。

为了保持应用体积较小,请考虑是否真的有必要保留每个 @Keep 注解。如果您使用反射来访问带注解的类或方法,请在 ProGuard 规则中使用 -if 条件,并指定进行反射调用的类。

有关如何缩减代码以及指定不应移除哪些代码的详细信息,请参阅缩减、混淆和优化您的应用

代码可见性注解

使用以下注解来指明特定代码部分(如方法、类、字段或包)的可见性。

使代码对测试可见

@VisibleForTesting 注解指明带注解的方法的可见性高于正常所需的可见性,从而使其可被测试。该注解具有一个可选的 otherwise 参数,允许您指定如果不是为了可测试性,该方法的可见性应该是什么。Lint 使用 otherwise 参数来强制执行预期的可见性。

在以下示例中,myMethod() 通常是 private 的,但为了测试,它被设为 package-private。通过 VisibleForTesting.PRIVATE 指定,如果此方法从 private 访问权限允许的上下文之外(例如从不同的编译单元)被调用,lint 会显示一条消息。

Kotlin

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
fun myMethod() {
    ...
}

Java

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
void myMethod() { ... }

您还可以指定 @VisibleForTesting(otherwise = VisibleForTesting.NONE) 以指明该方法仅为测试而存在。此形式与使用 @RestrictTo(TESTS) 相同。它们都执行相同的 lint 检查。

限制 API

@RestrictTo 注解指明对带注解的 API(包、类或方法)的访问受到以下限制:

子类

使用注解形式 @RestrictTo(RestrictTo.Scope.SUBCLASSES) 可将 API 访问限制为仅限子类。

只有继承了带注解类的类才能访问此 API。Java 的 protected 修饰符限制性不够,因为它允许同一包内的不相关类访问。此外,有时您希望保持方法为 public 以备将来灵活使用(因为您无法将之前 protected 且被重写的方法改为 public),但又想提供一个提示,即该类仅旨在在类内部或由子类使用。

使用注解形式 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX) 可将 API 访问限制为仅限您的库。

只有您的库代码才能访问带注解的 API。这不仅让您可以按想要的任何包层次结构组织代码,还可以让您在一组相关的库之间共享代码。此选项已可供 Jetpack 库使用,因为这些库有大量不打算供外部使用的实现代码,但为了在各种互补的 Jetpack 库之间共享,这些代码必须是 public 的。

测试

使用注解形式 @RestrictTo(RestrictTo.Scope.TESTS) 可防止其他开发人员访问您的测试 API。

只有测试代码才能访问带注解的 API。这可以防止其他开发人员使用仅供您测试使用的开发 API。