The Android Developer Challenge is back! Submit your idea before December 2.

利用注解改进代码检查

使用代码检查工具(如 Lint)可以帮助您找到问题并改进代码,但检查工具只能推断出这么多信息。例如,Android 资源 ID 使用 int 来标识字符串、图形、颜色及其他资源类型,因此如果您在本应指定颜色的地方指定了字符串资源,检查工具就无法判断。这种情况意味着,即使您使用代码检查,您的应用也可能无法正确呈现或根本无法运行。

您可以使用注解向 Lint 之类的代码检查工具提供提示,帮助检测这些更细微的代码问题。它们将作为附加到变量、参数和返回值的元数据标记进行添加,用于检查方法返回值、传递的参数、局部变量和字段。如果与代码检查工具搭配使用,注解可以帮助您检测一些问题,如 null 指针异常和资源类型冲突。

Android 通过注解支持库支持各种注解。您可以通过 android.support.annotation 软件包获取该库。

注意:如果模块依赖于注解处理器,则必须使用 `annotationProcessor` 依赖项配置来添加该依赖项。要了解详情,请阅读使用注解处理器依赖项配置

向您的项目添加注解

要在您的项目中启用注解,请向您的库或应用添加 support-annotations 依赖项。随后,当您运行代码检查或 lint 任务时,您添加的任何注解都会经过检查。

添加支持注解库依赖项

支持注解库在 Google 的 Maven 代码库中发布。要将支持注解库添加到您的项目,请在 build.gradle 文件的 dependencies 代码块中添加以下代码行:

    dependencies {
        implementation 'com.android.support:support-annotations:28.0.0'
    }
    
然后,在工具栏或显示的同步通知中,点击 Sync Now

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

注意:如果您使用 appcompat 库,则不需要添加 support-annotations 依赖项。由于 appcompat 库已经依赖于注解库,因此您可以访问相关注解。

如需查看支持代码库中所含注解的完整列表,请查阅支持注解库参考,或使用自动填充功能显示 import android.support.annotation. 语句的可用选项。

运行代码检查

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

您还可以使用命令行运行 lint 任务来强制执行注解。尽管这对标记持续集成服务器遇到的问题可能有用,但请注意,lint 任务并不会强制执行 null 性注解(只有 Android Studio 才会)。如需详细了解如何启用和运行 Lint 检查,请参阅使用 Lint 改进您的代码

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

Null 性注解

添加 @Nullable@NonNull 注解来检查给定变量、参数或返回值的 null 性。@Nullable 注解指示可以为 null 的变量、参数或返回值,而 @NonNull 则指示不可为 null 的变量、参数或返回值。

例如,如果将包含 null 值的局部变量作为参数传递给方法且将 @NonNull 注解附加到该参数,则编译代码时会生成一条警告,指明存在非 null 冲突。另一方面,对于被标记为 @Nullable 的方法返回的结果,如果不事先检查其是否为 null,则在尝试引用它时会生成一条 null 性警告。只有在每次使用方法时都应明确检查是否为 null 的情况下,才应对方法的返回值使用 @Nullable

以下示例将 @NonNull 注解附加到 contextattrs 参数,以检查传递的参数值是否不为 null。此外,还会检查 onCreateView() 方法本身是否不会返回 null。请注意,对于 Kotlin,我们不需要使用 @NonNull 注解,因为当我们指定不可为 null 的类型时,该注解会自动添加到生成的字节码:

Kotlin

    import android.support.annotation.NonNull
    ...

        /** Add support for inflating the <fragment> tag. **/
        fun onCreateView(
                name: String?,
                context: Context,
                attrs: AttributeSet
        ): View? {
            ...
        }
    ...
    

Java

    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) {
          ...
          }
    ...
    

Null 性分析

Android Studio 支持运行 null 性分析,以在代码中自动推断和插入 null 性注解。null 性分析会在代码的整个方法层次结构中扫描协定元素,以检测:

  • 可以返回 null 的调用方法
  • 不应返回 null 的方法
  • 可以为 null 的变量,如字段、局部变量和参数
  • 不能具有 null 值的变量,如字段、局部变量和参数

然后,该分析会自动在检测到的位置插入适当的 null 注解。

要在 Android Studio 中运行 null 性分析,请依次选择 Analyze > Infer Nullity。Android Studio 会在代码中检测到的位置插入 Android @Nullable@NonNull 注解。运行 null 性分析后,最好验证一下这些注入的注解。

注意:添加 null 性注解时,自动填充功能可能会建议 IntelliJ @Nullable@NotNull 注解,而不是 Android null 注解,并且可能会自动导入相应的库。不过,Android Studio Lint 检查工具只会查找 Android null 注解。验证您的注解时,请确认您的项目使用 Android null 注解,以便 Lint 检查工具可以在代码检查期间正确通知您。

资源注解

验证资源类型可能非常有用,因为 Android 对资源(如可绘制对象字符串资源)的引用以整数形式传递。如果代码需要一个参数来引用特定类型的资源(例如可绘制对象),可以为该代码传递预期的引用类型 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 方法,反之亦然。不过,如果系统应用的多个视图在不同的线程上,那么界面线程可能会与主线程不同。因此,您应使用 @UiThread 标注与应用的视图层次结构关联的方法,使用 @MainThread 仅标注与应用的生命周期关联的方法。

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

线程注解的一个常见用途是验证 AsyncTask 类中的方法替换,因为此类会执行后台操作并且仅在界面线程上发布结果。

值约束注解

使用 @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;
    

此示例要求 copyFile() 方法的调用方具有对外部存储空间的读取和写入权限:

Kotlin

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

Java

    @RequiresPermission(allOf = {
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE})
    public static final void copyFile(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) 示例中,如果向 startActivity(Intent) 方法传递了不具备相应权限的 intent,Intent 类中的注解就会针对无效使用该方法生成警告,如图 1 所示。

图 1. 根据间接权限注解针对 startActivity(Intent) 方法生成的警告。

编译工具会根据 Intent 类中相应 intent 操作名称上的注解对 startActivity(Intent) 生成警告:

Kotlin

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

Java

    @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 不应与读取或写入权限注解结合使用。

返回值注解

使用 @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) {
    }
    

Typedef 注解

使用 @IntDef@StringDef 注解,您可以创建整数和字符串集的枚举注解来验证其他类型的代码引用。Typedef 注解可以确保某个特定的参数、返回值或字段引用一组特定的常量。这些注解还可以启用代码填充功能以自动提供允许的常量。

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

以下示例说明了创建注解的具体步骤,该注解用于确保作为方法参数传递的值引用某个已定义的常量:

Kotlin

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

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

支持使用标记将常量结合起来

如果用户可以使用标志(如 |&^,等等)将允许的常量组合起来,则您可以通过 flag 属性定义一个注解,以检查参数或返回值是否引用有效模式。以下示例使用一组有效的 DISPLAY_ 常量来创建 DisplayOptions 注解:

Kotlin

    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(AnnotationRetention.SOURCE)
    annotation class DisplayOptions
    ...
    

Java

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

    ...
    

编译带注解标志的代码时,如果经过修饰的参数或返回值未引用有效模式,则会生成一条警告。

Keep 注解

使用 @Keep 注解可以确保,如果在编译时缩减代码大小,将不会移除带有该注解的类或方法。此注解通常添加到通过反射访问的方法和类,以防止编译器将代码视为未使用。

注意:使用 @Keep 标注的类和方法会始终包含在应用的 APK 中,即使您从未在应用的逻辑中引用这些类和方法也是如此。

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

如需详细了解如何缩减代码大小并指定不应移除的代码,请参阅压缩代码和资源

代码公开范围注解

使用以下注解来表示代码的特定部分(如方法、类、字段或软件包)的公开范围。

使代码公开以进行测试

@VisibleForTesting 注解表明所标注的方法的公开范围大于通常使该方法可供测试所需的范围。此注解包含一个可选的 otherwise 参数,可让您指定在不需要使某个方法公开以进行测试时,该方法的公开范围应该是多大。Lint 使用 otherwise 参数来强制执行预期的公开范围。

在以下示例中,myMethod() 通常是 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.GROUP_ID) 注解形式可以限制只有库能够访问 API。

只有您的库代码可以访问标注的 API。这样,您不仅可以按所需的任何软件包层次结构组织您的代码,而且还可以在一组相关库之间共享代码。某些支持库包含大量的实现代码,这些代码不适合外部使用,但必须处于 public 状态,以便在各种互补支持库之间共享,而此选项已经可供此类支持库使用。

注意Android 支持库类和软件包现在带有 @RestrictTo(GROUP_ID) 注解,这意味着,如果您不小心使用了这些实现类,Lint 会警告您不建议这样做。

测试

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

只有测试代码可以访问标注的 API。这样可防止其他开发者使用您仅打算作测试之用的 API 进行开发。