使用注解改进代码检查

使用代码检查工具(例如 lint)可以帮助您查找问题并改进代码,但检查工具只能推断这么多。例如,Android 资源 ID 使用 int 来标识字符串、图形、颜色和其他资源类型,因此检查工具无法判断您在应该指定颜色的地方指定了字符串资源。这种情况意味着,即使您使用代码检查,您的应用也可能渲染不正确或完全无法运行。

注释允许您为代码检查工具(例如 lint)提供提示,以帮助检测这些更细微的代码问题。注释作为元数据标签添加,您可以将其附加到变量、参数和返回值,以检查方法返回值、传递的参数、局部变量和字段。当与代码检查工具一起使用时,注释可以帮助您检测诸如空指针异常和资源类型冲突等问题。

Android 通过 Jetpack 注释库 支持各种注释。您可以通过 androidx.annotation 包访问该库。

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

将注释添加到您的项目

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

添加 Jetpack 注释库依赖项

Jetpack 注释库发布在 Google 的 Maven 存储库 上。要将 Jetpack 注释库添加到您的项目,请在您的 build.gradlebuild.gradle.kts 文件的 dependencies 块中包含以下行

Kotlin

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

Groovy

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

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

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

有关 Jetpack 存储库中包含的注释的完整列表,请参阅 Jetpack 注释库参考 或使用自动完成功能显示 import androidx.annotation. 语句的可用选项。

运行代码检查

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

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

尽管注释冲突会生成警告,但这些警告不会阻止您的应用编译。

空值注释

空值注释在 Java 代码中很有用,可以强制执行值是否可以为 null。它们在 Kotlin 代码中不太有用,因为 Kotlin 具有在编译时强制执行的内置空值规则。

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

例如,如果包含 null 值的局部变量作为参数传递给附加了 @NonNull 注释的参数的方法,则构建代码会生成警告,指示存在非空冲突。此外,尝试引用由 @Nullable 标记的方法的结果而不首先检查结果是否为 null 会生成空值警告。仅当方法的每次使用都必须显式检查 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 中运行空值分析,请选择分析 > 推断空值。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) {
    //...
}

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

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)方法对传递给该方法的意图使用间接权限

Kotlin

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

Java

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

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

图 1. startActivity(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替换@RequiresPermission.Read@RequiresPermission.Write。但是,对于间接权限,@RequiresPermission不应与读取或写入权限注释一起使用。

返回值注释

使用@CheckResult注释来验证方法的结果或返回值是否确实被使用。与其使用@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) {
}

类型定义注释

类型定义注释检查特定的参数、返回值或字段是否引用一组特定的常量。它们还使代码完成能够自动提供允许的常量。

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

类型定义注释使用@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属性的注释来检查参数或返回值是否引用了有效的模式。

以下示例创建了DisplayOptions注释,其中包含一个有效的DISPLAY_常量列表

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注释的类和方法始终出现在应用的 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 库已经可以使用此选项,这些库有大量的实现代码不打算供外部使用,但必须是 public 才能在各种互补的 Jetpack 库之间共享。

测试

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

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