Save the date! Android Dev Summit is coming to Mountain View, CA on November 7-8, 2018.

使用注解改进代码检查

使用代码检查工具(例如 Lint)可以帮助您找到问题并改进代码,不过,检查工具只能推断这么多信息。例如,Android 资源 ID 使用 int 标识字符串、图形、颜色和其他资源类型,因此,检查工具无法告诉您何时指定字符串资源以及应在什么地方指定颜色。在这种情况下,您的应用可能无法正确渲染或根本无法运行,即使使用代码检查也是如此。

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

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

向您的项目添加注解

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

添加支持注解库依赖项

支持注解库是 Android 支持库的一部分。要向您的项目添加注解,您必须下载支持存储库并向 build.gradle 文件中添加 support-annotations 依赖项。

  1. 打开 SDK 管理器,方法是点击工具栏中的 SDK Manager 或者选择 Tools > Android > SDK Manager
  2. 点击 SDK Tools 标签。
  3. 展开 Support Repository 并选中 Android Support Repository 复选框。
  4. 点击 OK
  5. 继续安装向导的说明操作,安装软件包。
  6. 将以下代码行添加到 build.gradle 文件的 dependencies 块中,向您的项目添加 support-annotations 依赖项:
     dependencies { compile 'com.android.support:support-annotations:24.2.0' } 
    您下载的库版本可能较高,因此,确保您在此指定的值与第 3 步中的版本匹配。
  7. 在显示的工具栏或同步通知中,点击 Sync Now

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

如果想要在未使用适用于 Gradle 的 Android 插件但使用 Gradle Java 插件的 Gradle 模块中使用注解(com.android.applicationcom.android.library),您必须明确添加 SDK 存储库,因为无法从 JCenter Java 存储库获得 Android 支持库:

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

:如果您使用 appcompat 库,则无需添加 support-annotations 依赖项。因为 appcompat 库已经依赖注解库,您可以访问相关注解。

要查看支持存储库中包含的完整注解列表,请查看支持注解库参考,或者使用自动填充功能显示 import android.support.annotation. 语句的可用选项。

运行代码检查

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

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

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

Nullness 注解

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

例如,如果一个包含 null 值的局部变量作为已附加 @NonNull 注解的参数传递到某个方法,则构建代码将生成一个指示非 null 冲突的警告。另一方面,对于通过 @Nullable 标记的方法的结果,如果不先检查其是否为 null,那么在尝试引用它时将生成 nullness 警告。只有在每次使用方法时都应明确检查是否为 null 的情况下,才应对方法返回值使用 @Nullable

下面的示例会将 @NonNull 注解附加到 contextattrs 参数,以检查传递的参数值是否不为 null。它还会检查 onCreateView() 方法本身是否不会返回 null:

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 分析

Android Studio 支持通过运行 nullability 分析,在您的代码中自动推断和插入 nullness 注解。Nullability 分析会在您代码的整个方法层次结构中扫描协定类,以检测:

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

然后,此分析将自动在已检测到的位置插入相应的 null 注解。

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

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

资源注解

验证资源类型可能非常有用,因为 Android 对资源(例如可绘制对象字符串资源)的引用以整型形式传递。需要参数来引用特定类型资源(例如可绘制对象)的代码可以作为预计的引用类型 int 传入,不过实际将引用不同类型的资源,例如 R.string 资源。

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

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 仅标注与应用生命周期关联的方法。

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

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

值约束注解

使用 @IntRange@FloatRange@Size 注解可以验证传递的参数的值。在应用到用户可能弄错其范围的参数时,@IntRange@FloatRange 都非常有用。

@IntRange 注解可以验证整型或长整型参数值是否位于指定范围内。下面的示例可以确保 alpha 参数包含 0 至 255 范围内的整数值:

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

@FloatRange 注解可以检查浮点或双整型参数值是否位于指定的浮点值范围内。下面的示例可以确保 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) 可以验证某个数组是否刚好包含三个值。下面的示例可以确保 location 数组至少包含一个元素:

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

权限注解

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

对于 intent 权限,请将权限要求添加到定义 intent 操作名称的字符串字段上:

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

对于您需要单独读写权限的内容提供程序的权限,请在 @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");

间接权限

如果权限依赖于提供给方法参数的特定值,请对参数本身使用 @RequiresPermission,而不用列出具体权限。例如, startActivity(Intent) 方法会对传递到方法的 intent 使用间接权限:

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

在您使用间接权限时,构建工具将执行数据流分析以检查传递到方法的参数是否具有任何 @RequiresPermission 注解。随后,它们会对方法本身强制参数的任何现有注解。在 startActivity(Intent) 示例中,当一个不具有相应权限的 intent 传递到方法时,Intent 类中的注解会针对 startActivity(Intent) 的无效使用生成警告,如图 1 中所示。

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

构建工具会在 startActivity(Intent) 上从 Intent 类中相应 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 不应与读取或写入权限注解搭配使用。

返回值注解

使用 @CheckResult 注解可以验证实际使用的是方法的结果还是返回值。添加注释来阐明可能令人困惑的方法的结果,而不是使用 @CheckResult 标注每个非空方法。例如,新 Java 开发者经常误认为 <String>.trim() 会移除原始字符串中的空格。使用 @CheckResult 标注方法会在调用方未对方法返回值作任何处理的地方标记 <String>.trim() 的使用。

下面的示例会标注 checkPermissions() 方法,以确保实际引用方法的返回值。它还会将 enforcePermission() 方法指定为要向开发者建议的替换方法:

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

CallSuper 注解

使用 @CallSuper 注解可以验证替换方法是否会调用方法的超类实现。下面的示例会标注 onCreate() 方法,以确保任何替换方法实现都会调用 super.onCreate()

@CallSuper
protected void onCreate(Bundle savedInstanceState) {
}

Typedef 注解

使用 @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_STANDARDNAVIGATION_MODE_LISTNAVIGATION_MODE_TABS),则会生成警告。

您还可以组合 @IntDef@IntRange,以指示整型可以是给定的常量集或某个范围内的值。

允许将常量与标志相结合

如果用户可以将允许的常量与标志(例如,|&^,等等)相结合,则您可以通过 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 {}

...

在您使用注解标志构建代码时,如果经过修饰的参数或返回值不引用有效模式,则将生成警告。

代码可访问性注解

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

@VisibleForTesting 注解指示一个代码块的可见性是否高于让代码变得可测试所需要的水平。

@Keep 注解可以确保如果在构建时缩减代码,标注的元素不会移除。它一般会添加到通过反射访问的方法和类中,以阻止编译器将代码视为未使用。