代码检查工具(如 lint)可以帮助您找到问题并改进代码,但检查工具只能推断出这么多信息。例如,Android 资源 ID 使用 int
来标识字符串、图形、颜色及其他资源类型,因此如果您在本应指定颜色的地方指定了字符串资源,检查工具就无法发现这一问题。这种情况意味着,即使您使用了代码检查工具,您的应用也可能无法正确呈现或根本无法运行。
您可以使用注解向 lint 之类的代码检查工具提供提示,以帮助它们检测这些更细微的代码问题。注解将作为附加到变量、参数和返回值的元数据标记进行添加,用于检查方法返回值、传递的参数、局部变量和字段。与代码检查工具配合使用时,注解可以帮助您发现一些问题,如 null 指针异常和资源类型冲突。
Android 通过 Jetpack 注解库支持各种注解。
您可以通过 androidx.annotation
软件包获取该库。
注意:如果模块依赖于某个注解处理器,若要添加相应依赖项,对于 Kotlin,您必须使用 kapt
或 ksp
依赖项配置;对于 Java,则必须使用 annotationProcessor
依赖项配置。
为您的项目添加注解
如需在您的项目中启用注解,请为库或应用添加 androidx.annotation:annotation
依赖项。这样一来,当您运行代码检查或 lint
任务时,您添加的所有注解都会接受检查。
添加 Jetpack 注解库依赖项
Jetpack 注解库是在 Google 的 Maven 制品库中发布的。如需为您的项目添加 Jetpack 注解库,请在 build.gradle
或 build.gradle.kts
文件的 dependencies
代码块中添加以下代码行:
Kotlin
dependencies { implementation("androidx.annotation:annotation:1.8.2") }
Groovy
dependencies { implementation 'androidx.annotation:annotation:1.8.2' }
如果您在自己的库模块中使用注解,注解将作为 Android ARchive (AAR) 工件的一部分以 XML 格式添加到 annotations.zip
文件中。添加 androidx.annotation
依赖项不会为相应库的任何下游用户引入依赖项。
注意:如果您使用的是其他 Jetpack 库,可能无需添加 androidx.annotation
依赖项。因为许多其他 Jetpack 库依赖于注解库,您或许已经有权访问相关注解。
如需获取 Jetpack 仓库中包含的注解的完整列表,请参阅 Jetpack 注解库参考文档,或者使用自动补全功能来显示 import androidx.annotation.
语句的可用选项。
运行代码检查
若要从 Android Studio 启动代码检查(包括验证注解和自动 lint 检查),请从菜单中依次选择 Analyze > Inspect Code。Android Studio 将显示冲突消息,在您的代码与注解冲突的地方标记潜在问题并建议可能的解决方法。
您还可以使用命令行运行 lint
任务,以便强制执行注解。尽管这对标记持续集成服务器出现的问题可能有用,但 lint
任务并不会强制执行 null 性注解(说明见下方部分),只有 Android Studio 会强制执行此类注解。如需详细了解如何启用和运行 lint 检查,请参阅通过进行 lint 检查来改进代码。
尽管注解冲突会导致生成警告,但这些警告不会阻止应用编译。
null 性注解
在 Java 代码中 null 性注解非常实用,可以强制执行相应值是否可为 null。这类注解在 Kotlin 代码中用处不大,因为 Kotlin 内置了在编译时强制执行的可为 null 性规则。添加 @Nullable
和 @NonNull
注解,以检查给定变量、参数或返回值的 null 性。@Nullable
注解用于指明可为 null 的变量、参数或返回值。@NonNull
则用于指明不可为 null 的变量、参数或返回值。
例如,如果将包含 null 值的局部变量作为参数传递给方法,并且将 @NonNull
注解附加到该参数,则构建代码时会生成一条警告,指明存在非 null 冲突。另外,如果不事先检查被标记为 @Nullable
的方法返回的结果是否为 null,就尝试引用该结果,会导致生成一条 null 性警告。只有在每次使用某方法都必须明确检查返回值是否为 null 的情况下,才对该方法的返回值使用 @Nullable
。
以下示例演示了可为 null 性的实际运用。Kotlin 示例代码没有利用 @NonNull
注解,因为当指定了不可为 null 的类型时,该注解会自动添加到生成的字节码。Java 示例对 context
和 attrs
参数利用 @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) { ... } ...
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 对资源(如可绘制对象和字符串资源)的引用以整数形式传递。
如果代码需要一个参数来引用特定类型的资源(例如 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
指定某个参数应为颜色资源,但系统不会将颜色整数(采用 RRGGBB
或 AARRGGBB
格式)识别为颜色资源。您可以改用 @ColorInt
注解来指明某个参数必须为颜色整数。构建工具会标记执行以下操作的错误代码:将颜色资源 ID(例如 android.R.color.black
)而非颜色整数传递给添加了此类注解的方法。
线程注解
线程注解可以检查某个方法是否从特定类型的线程调用。支持以下线程注解:
构建工具会将 @MainThread
和 @UiThread
注解视为可互换,因此您可以从 @MainThread
方法调用 @UiThread
方法,反之亦然。不过,如果系统应用有多个视图在不同的线程上,那么界面线程可能会与主线程不同。因此,您应使用 @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";
如果您需要对 content provider 拥有单独的读取和写入访问权限,则需要将每个权限要求封装在 @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 所示。
构建工具会根据 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
为每个非 void 方法添加注解,而应添加注解来阐明可能令人不解的方法的结果。
例如,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_STANDARD
、NAVIGATION_MODE_LIST
或 NAVIGATION_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
注解可以确保以下情况:如果在构建时缩减代码大小,将不会移除带有该注解的类或方法。该注解通常添加到通过反射访问的方法和类,以防止编译器将代码视为未使用。
注意:使用 @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 库之间共享,而此选项已经可供此类 Jetpack 库使用。
测试
使用 @RestrictTo(RestrictTo.Scope.TESTS)
注解形式可以防止其他开发者访问您的测试 API。
只有测试代码可以访问添加了此类注解的 API。这样可以防止其他开发者将您仅打算作测试之用的 API 用于开发。