使用註解提升程式碼檢查效率

使用 Lint 等程式碼檢查工具可協助您找出問題並改善程式碼,但檢查工具不能推論出所有問題。舉例來說,Android 資源 ID 會使用 int 來判定字串、圖形、顏色和其他資源類型,因此檢查工具並無法判斷您是否在應該指定顏色的地方,指定了字串資源。在這種情況下,即使您使用程式碼檢查功能,應用程式仍可能算繪錯誤或完全無法運作。

註解讓使用者可以對 Lint 等檢查工具進行微調,藉此偵測出這種較細微的程式碼問題。系統會將這些註解新增為中繼資料標記,您可以附加到變數、參數和回傳值,以檢查方法回傳值、傳遞的參數、本機變數和各項欄位。與程式碼檢查工具搭配使用時,註解可協助您偵測問題,例如空值指標例外狀況和資源類型衝突。

Android 透過註解支援資料庫支援各種註解。您可以透過 android.support.annotation 套件存取這個資料庫。

注意:假使模組在註解處理工具中有依附元件,必須使用「annotationProcessor」依附元件設定才能新增該元件。 詳情請參閱使用註解處理工具依附元件設定

新增專案註解

如要在專案中啟用註解,請將 support-annotations 依附元件新增至程式庫或應用程式。在您執行程式碼檢查或 lint 工作時,系統會檢查您新增的所有註解。

新增支援註解程式庫依附元件

「支援註解」程式庫已發布在 Google 的 Maven 存放區中。如要將支援註解程式庫新增至專案,請在 build.gradle 檔案的 dependencies 區塊中加入下列程式碼:

Groovy

dependencies {
    implementation 'com.android.support:support-annotations:28.0.0'
}

Kotlin

dependencies {
    implementation("com.android.support:support-annotations:28.0.0")
}
然後在隨即出現的工具列或同步處理通知中,按一下「Sync Now」(立即同步處理)

假使您在自己的程式庫模組中使用註解,則註解會在 annotations.zip 檔案中以 XML 格式包含在 Android ARchive (AAR) 構件內。加入 support-annotations 依附元件,並不會使程式庫下游使用者的依附元件增加。

注意:假使您使用的是 appcompat 程式庫,則不必新增 support-annotations 依附元件。由於 appcompat 程式庫已經與註解程式庫存在依附關係,因此您可以存取註解。

如需支援存放區內註解的完整清單,請參閱支援註解程式庫參考資料,或使用自動完成功能查看可用的 import android.support.annotation. 陳述式選項。

執行程式碼檢查

如要透過 Android Studio 啟動程式碼檢查作業,包括驗證註解和自動 Lint 檢查,請從選單列依序選取「Analyze」(分析) >「Inspect Code」(檢查程式碼)。如果程式碼與註解發生衝突,Android Studio 會顯示衝突訊息,以標明潛在問題,並建議可行的解決方式。

您也可以使用指令列執行 lint 工作,強制執行註解。雖然這有助找出持續整合伺服器的問題,但 lint 工作不會強制執行空值註解,只有 Android Studio 才會執行。如要進一步瞭解如何啟用及執行 Lint 檢查,請參閱使用 Lint 改善程式碼

請注意,雖然註解衝突會產生警示,但這些警示並不會妨礙應用程式編譯。

空值註解

新增 @Nullable@NonNull 註解,檢查特定變數、參數或傳回值的空值。@Nullable 註解代表可以是空值的變數、參數或傳回值,而 @NonNull 則代表不能是空值的變數、參數或傳回值。

舉例來說,如果您將包含空值的本機變數當做參數傳至方法,且該參數帶有 @NonNull 註解,則建構程式碼時,系統會產生警示,標示出非空值衝突。另一方面,在未檢查結果是否為空值的情況下,嘗試對標有 @Nullable 的方法參照結果,則會使系統產生空值警示。只有在每次使用該方法都要明確執行空值檢查時,才應將 @Nullable 用於方法的回傳值。

下列範例會將 @NonNull 註解附加至 contextattrs 參數,以確認傳遞的參數值並非空值,也會檢查 onCreateView() 方法本身不會傳回空值。請注意,使用 Kotlin 時,不需要使用 @NonNull 註解,因為當系統指定不可為空值的類型時,註解即會自動新增至產生的位元碼:

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

分析是否可為空值

Android Studio 能執行是否可為空值的分析,以在程式碼中自動推論並插入空值註解。這項分析會掃描程式碼中所有方法階層的契約,以偵測下列項目:

  • 可以傳回空值的呼叫方法
  • 不應傳回空值的方法
  • 可以是空值的變數,例如欄位、本機變數和參數
  • 不能是空值的變數,例如欄位、本機變數和參數

分析工具隨後會在偵測到上述項目的位置,自動插入適當的空值註解。

如要在 Android Studio 中執行空值分析,請選取「Analyze」(分析) >「Infer Nullity」。Android Studio 會將 Android @Nullable@NonNull 註解插入在程式碼中偵測到上述項目的位置。執行空值分析後,建議您驗證插入的註解。

注意:新增空值註解時,自動完成功能可能會建議 IntelliJ @Nullable@NotNull 註解,而非 Android 空值註解,並可能會自動匯入相應的程式庫。然而,Android Studio Lint 檢查工具僅會尋找 Android 空值的註解。驗證註解時,請確認專案使用的是 Android 空值註解,這樣 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 註解來表明參數必須是顏色整數。如果不正確的程式碼將 android.R.color.black 這類的顏色資源 ID 傳遞至附有註解的方法,而不是傳遞顏色整數,建構工具就會標記該程式碼。

執行緒註解

執行緒註解會檢查方法是不是從特定類型的執行緒呼叫。系統支援的執行緒註解如下:

注意:建構工具會將 @MainThread@UiThread 註解視為可互換,因此您可以透過 @MainThread 方法呼叫 @UiThread 方法,反之亦然。不過,如果是在各執行緒檢視畫面不同的系統應用程式當中,UI 執行緒和主要執行緒則可能不同。因此,對於和應用程式檢視區塊階層關聯的方法,您應使用 @UiThread 來做為註解,至於 @MainThread 註解則僅應用於和應用程式生命週期關聯的方法。

假使類別中所有方法都具有相同的執行緒要求,您可以在類別中新增單一執行緒註解,以驗證類別中的所有方法都是透過相同執行緒來呼叫。

執行緒註解的常見用途是驗證 AsyncTask 類別的方法覆寫作業,因為此類別會執行背景作業,然後僅將結果發布於 UI 執行緒。

值限制註解

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

@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.Read 和/或 @RequiresPermission.Write 換成 @RequiresPermission。然而,假使是間接權限,@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 註解的類別和方法一律會出現在應用程式 APK 中,即使您從未在應用程式的邏輯中參照這些類別和方法。

為了讓應用程式大小保持較小,請考慮是否有必要保留應用程式中的各個 @Keep 註解。假使您是透過反映來存取加註的類別或方法,請在 ProGuard 規則中使用 -if 條件,指定反映此回應的類別調用。

如要進一步瞭解如何壓縮程式碼,以及指定不應移除的程式碼,請參閱縮減程式碼和資源

程式碼瀏覽權限註解

使用下列註解來表明程式碼的特定部分 (例如方法、類別、欄位或套件) 的瀏覽權限。

公開測試

@VisibleForTesting 註解表明註解方法比使該方法可測試的通常所需更明顯。此註解包含選用的 otherwise 引數,可讓您指定是否要讓方法顯示,而不開放測試。Lint 使用 otherwise 引數來強制執行預期的瀏覽權限。

在下列範例中,myMethod() 通常為 private,但這是不公開的測試用套件。透過下列 VisibleForTesting.PRIVATE 標示,假使從 private 存取權允許的內容之外 (例如其他編譯單位) 調用此方法,ML 會顯示訊息。

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 進行您打算僅用於測試目的的開發。