利用注解改善 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 依赖。

  1. 打开 SDK Manager:点击工具栏的 SDK Manager 图标 {% img https://developer.android.google.cn/studio/images/buttons/toolbar-sdk-manager.png %} 或依次进入菜单 Tools -> Android -> SDK Manager
  2. 点击 SDK Tools 选项
  3. 展开 Support Repository 并且勾选 Android Support Repository
  4. 点击 OK
  5. 继续随着提示来安装勾选的包
  6. 添加 support-annotations 依赖:在 build.grale 文件的 dependencies 块中加入以下语句
1
2
3
dependencies {
    compile 'com.android.support:support-annotations:24.2.0'
}

你下载的库版本可能更高,一定要确认语句中的版本号和步骤 3 中的一致。

  1. 在工具栏的同步通知中,点击 Sync Now

如果你在库模块中使用这些注解,它们会作为 AAR 工件(AAR artifact)的一部分以 XML 格式包含在 annotations.zip 文件中。添加 support-annotations 依赖并不会为你的库的下游使用者引入依赖。 如果你的项目模块不使用 Android 插件的 Gradle 模块 (com.android.application or com.android.library) ,而使用 Java 插件,要使用这些注解必须显式地添加 SDK 库依赖,因为 JCenter 仓库中并不包含 Android 地支持库。

1
2
3
4
5
6
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 不会强制执行空值注解检查(只有 Android Studio 会)。想要获取更多 Lint 检查的信息,可以访问 Improve Your Code with Lint

使用 @Nullable@NonNull 可以检查变量、参数和返回值是否是空值。@Nullable 注解表明变量、参数或这返回值可以为空,而 @NonNull 恰恰相反。 举例来说,如果一个方法的参数被 @NonNull 标记,而你要传入一个可能为空的本地变量,那么在构建的时候就会产生警告信息,提醒你出现了非空冲突。反过来说,如果一个方法的返回值被 @Nullable 标记,尝试访问它却不检查它是否是空值时就会出现空值警告。只有当一个方法的放回值必须进行显式的空值检查时,才用 @Nullable 标记它。 下面的代码使用 @NonNull 注解检查参数 contextattrs 不是空值,同时检查方法 onCreateView() 不返回空值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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) {
        ...
    }
...

Android Studio 支持进行可空性分析来自动推断并在代码中插入空值注解。可空性分析会扫描代码中的所有方法层次结构中的约定来检测:

  • 对会返回空值的方法的调用
  • 不应该返回空值的方法
  • 可为空的变量(比如字段、本地变量和参数)
  • 不能为空的变量 然后分析程序会自动在需要的位置插入合适的注解。 要使用 Android Studio 的可空性分析功能,需要使用菜单项 Analyze > Infer Nullity。Android Studio 会在检测到的位置插入 Android 注解 。而在进行可空性分析之后,最好再亲自校验加入的注解。

注意:当加入空值注解的时候,自动完成也许会建议使用 Intellij 的 @Nullable@NonNull,而不是 Android 的空值注解,并且可能自动导入相应的库。然而,Android Studio 的 Lint 检查工具只会检测 Android 空值注解。再校验注解的时候,要确认使用的时 Android 空值注解,这样进行代码检查时 Lint 检查工具才能正常工作。

对资源类型的验证非常有益,因为 Android 项目中对资源的引用(比如图片字符串)总是以整型传递。如果参数需要传入特定类型的资源(比如图片),传递给它的实际是整型数值,实际上可能指向不同类型的资源(比如字符串)。 举例来说,加入 @StringRes 注解来检查资源参数类型为字符串,代码如下:

1
2
3
public abstract void setTitle(@StringRes int resId) {
    ...
}

如果传入的资源类型不是字符串,在进行代码检查时就会产生警告信息。 其他的资源类型注解,比如 @DrawableRes@DimenRes@ColorRes@InterpolatorRes,也可以用同样的方式使用和检查。如果你的参数需要支持多种资源类型,也可以在一个参数之前加上多个注解。@AnyRes 注解代表注解的参数可被传入任何类型的资源。 虽然可以使用 @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 之间:

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

@FloatRange 注解检查类型为浮点数或双精度浮点数的参数在指定的实数范围内。下面的例子保证了参数 alpha 的取值范围在 0.0 到 1.0 之间:

1
2
3
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) 验证数组刚好有 3 个元素。下面的例子保证数组 location 至少有 1 个元素:

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

使用 @RequiresPermission 注解来验证方法中对权限的请求。要单个权限的有效性,请使用 anyOf 属性。要检查一组权限,请使用 allOf 属性。以下示例对 setWallpaper() 方法的注解,确保方法的调用者具有 permission.SET_WALLPAPERS 权限:

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

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

1
2
3
4
5
6
@RequiresPermission(allOf = {
    Manifest.permission.READ_EXTERNAL_STORAGE,
    Manifest.permission.WRITE_EXTERNAL_STORAGE})
public static final void copyFile(String dest, String source) {
    ...
}

对于 intent 的权限检查,可将权限要求放置在定义 intent 操作(action)名称的字符串字段上:

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

对于需要单独的读写权限的内容提供程序的权限注解,请在 @RequiresPermission.Read@RequiresPermission.Write 注解中包含各自的权限要求:

1
2
3
@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 使用了间接权限:

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

当使用间接权限时,构建工具会进行数据流分析,以检查传递到方法中的参数是否具有任何 @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) 上生成警告:

1
2
3
@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 注解来澄清可能引起混淆的方法,而不是标记每个非 void 方法。例如,新手 Java 开发人员经常错误地认为 <String>.trim() 会从原始字符串中删除空格。@CheckResult 注解会标记出不对 <String>.trim() 的返回值执行任何操作的地方。 以下示例给 checkPermissions() 方法加注解,以确保方法的返回值被引用。它还建议开发人员使用 enforcePermission() 方法作为替代:

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

请使用 @CallSuper 注解来验证重写的方法调用了该方法的父类实现。以下示例对 onCreate() 方法的注解,确保任何重写的方法都要调用 super.onCreate()

1
2
3
@CallSuper
protected void onCreate(Bundle savedInstanceState) {
}

使用 @IntDef@StringDef 可以创建整数和字符串集的枚举注解,以验证对它们的代码引用。TypeDef 注解确保参数、返回值或字段引用一组特定的常量。它们还令代码补完能够自动提供合适的常量。 TypeDef 注解使用 @interface 声明新的枚举注解类型。@IntDef@StringDef 放在 @Retention 注解之后标识出新注解,同时它们也是定义枚举类型所必需的注解。@Retention(RetentionPolicy.SOURCE) 告诉编译器不要将枚举注解存储在 .class 文件中。 以下示例说明了如何创建注解以确保传入方法参数的值只引用定义好的常量:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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)之一,则会产生警告。

如果用户能组合定义的常量与标志(例如|、&、^ 等),你可以在注解中使用 flag 属性,以检查参数或返回值是否引用了有效的模式。以下示例定义 DISPLAY_ 常数列表创建 DisplayOptions 注释:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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 确保在构建时的压缩行为不会删除注解标记的元素。它通常被添加到通过反射访问的方法和类,以防止编译器认为代码未使用从而“优化”这些代码。