1. 程式人生 > >Android註解支援(Support Annotations)詳解

Android註解支援(Support Annotations)詳解

註解支援(Support Annotations)

Android support library從19.1版本開始引入了一個新的註解庫,它包含很多有用的元註解,你能用它們修飾你的程式碼,幫助你發現bug。Support library自己本身也用到了這些註解,所以作為support library的使用者,Android Studio已經基於這些註解校驗了你的程式碼並且標註其中潛在的問題。Support library 22.2版本又新增了13個新的註解以供使用。

使用註解庫

註解預設是沒有包含的;他們被包裝成一個獨立的庫。(support library現在由一些更小的庫組成:v4-support, appcompat, gridlayout, mediarouter等等)

(如果你正在使用appcompat庫,那麼你已經可以使用這些註解了,因為appcomat它自己也依賴它。)

新增使用註解最簡單的方式就是開啟Project Structure對話方塊。首先在左邊選中module,然後在右邊選中Dependencies標籤頁,點選面板底部的+按鈕,選擇Library Dependency,假設你已經把Android Support Repository安裝到你的SDK中了,那麼註解庫將會出現在列表中,你只需點選選中它即可(這裡是列表中的第一個):
新增依賴

點選OK完成Project Structure的編輯。這會修改你的build.gradle檔案,當然你也可以手動編輯它:

dependencies {
    compile 'com.android.support:support-annotations:22.2.0'
}

對於Android application和Android library這兩個型別的module(你應用了com.android.application或者com.android.library外掛的)來說,你需要做的已經都做好了。如果你想只在Java module使用這些註解,那麼你就明確的包含SDK倉庫了,因為support libraries不能從jcenter獲得(Android Gradle外掛會自動的包含這些依賴,但是Java外掛卻沒有。)

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

執行註解

當你用Android Studio和IntelliJ的時候,如果給標註了這些註解的方法傳遞錯誤型別的引數,那麼IDE就會實時標記出來。

從Gradle外掛1.3.0-beta1版本開始,並且安裝了Android M Preview平臺工具的情況下,通過命令列呼叫gradle的lint任務就可以執行這些檢查。如果你想把標記問題作為持續整合的一部分,那麼這種方式是非常有用的。說明:這並不包含nullness註解。本文中所介紹的其他註解都可以通過lint執行檢查。

Nullness Annotations

@Nullable註解能被用來標註給定的引數或者返回值可以為null。
類似的,@NonNull註解能被用來標註給定的引數或者返回值不能為null。

如果一個本地變數的值為null(比如因為過早的程式碼檢查它是否為null),而你又把它作為引數傳遞給了一個方法,並且該方法的引數又被@NonNull標註,那麼IDE會提醒你,你有一個潛在的崩潰問題。

v4 support library中的FragmentActivity的示例程式碼:

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
...

/**
 * Add support for inflating the &lt;fragment> tag.
 */
@Nullable
@Override
public View onCreateView(String name, @NonNull Context context, @NonNull AttributeSet attrs) {
...

(如果你執行Analyze > Infer Nullity,或者你在鍵入時把@NonNull替換成了@NotNull,那麼IDE可能會提供附加的IntelliJ註解。參考底部的“IntelliJ Annotations”段落了解更多)

注意@NonNull和@Nullable並不是對立的:還有第三種可能:未指定。當你沒有指定@NonNull或者@Nullable的時候,工具就不能確定,所以這個API也就不起作用。

最初,我們在findViewById方法上標註@Nullable,從技術上說,這是正確的:findViewById可以返回null。但是如果你知道你在做什麼的時候(如果你傳遞給他一個存在的id)他是不會返回null的。當我們使用@Nullable註解它的時候,就意味著原始碼編輯器中會有大量的程式碼出現高亮警告。如果你已經意識到每次使用該方法都應該明確的進行null檢查,那麼就只能用@Nullable標註返回值。有個經驗規則:看現有的“好的程式碼”(比如審查產品程式碼),看看這些API是怎麼被使用的。如果該程式碼為null檢查結果,你應該為方法註解@Nullable。

資源型別註解

Android的資源值通常都是使用整型傳遞。這意味著獲取一個drawable使用的引數,也能很容易的傳遞給一個獲取string的方法;因為他們都是int型別,編譯器很難區分。

資源型別註解可以在這種情況下提供型別檢查。比如一個被@StringRes住進誒的int型別引數,如果傳遞一個不是R.string型別的引用將會被IDE標註:
資源型別註解
以ActionBar為例:

import android.support.annotation.StringRes;
...
public abstract void setTitle(@StringRes int resId);

有很多不同資源型別的註解:如下的每一個Android資源型別:
@StringRes, @DrawableRes, @ColorRes, @InterpolatorRes,等等。一般情況下,如果有一個foo型別的資源,那麼它的相應的資源型別註解就是FooRes.

除此之外,還有一個名為@AnyRes特殊的資源型別註解。它被用來標註一個未知的特殊型別的資源,但是它必須是一個資源型別。比如在框架中,它被用在Resources#getResourceName(@AnyRes int resId)上,使用的時候,你可以這樣getResources().getResourceName(R.drawable.icon)用,也可以getResources().getResourceName(R.string.app_name)這樣用,但是卻不能這樣getResources().getResourceName(42)用。

請注意,如果你的API支援多個資源型別,你可以使用多個註解來標註你的引數。

IntDef/StringDef: 型別定義註解

整型除了可以作為資源的引用之外,也可以用作“列舉”型別使用。

@IntDef和”typedef”作用非常類似,你可以建立另外一個註解,然後用@IntDef指定一個你期望的整型常量值列表,最後你就可以用這個定義好的註解修飾你的API了。

appcompat庫裡的一個例子:

import android.support.annotation.IntDef;
...
public abstract class ActionBar {
...
@IntDef({NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS})
@Retention(RetentionPolicy.SOURCE)
public @interface NavigationMode {}

public static final int NAVIGATION_MODE_STANDARD = 0;
public static final int NAVIGATION_MODE_LIST = 1;
public static final int NAVIGATION_MODE_TABS = 2;

@NavigationMode
public abstract int getNavigationMode();

public abstract void setNavigationMode(@NavigationMode int mode);

上面非註解的部分是現有的API。我們建立了一個新的註解(NavigationMode)並且用@IntDef標註它,通過@IntDef我們為返回值或者引數指定了可用的常量值。我們還添加了@Retention(RetentionPolicy.SOURCE)告訴編譯器這個新定義的註解不需要被記錄在生成的.class檔案中(譯者注:原始碼級別的,生成class檔案的時候這個註解就被編譯器自動去掉了)。

使用這個註解後,如果你傳遞的引數或者返回值不在指定的常量值中的話,IDE將會標記出這種情況。
型別定義註解

你也可以指定一個整型是一個標記性質的型別;這樣客戶端程式碼就通過|,&等操作符同時傳遞多個常量了:

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

最後,還有一個字串版本的註解,就是@StringDef,它和@IntDef的作用基本上是一樣,所不同的是它是針對字串的。該註解一般不常用,但是有的時候非常有用,比如在限定向Activity#getSystemService方法傳遞的引數範圍的時候。

要了解關於型別註解的更多詳細資訊,請參考
https://developer.android.com/tools/debugging/annotations.html#enum-annotations

執行緒註解: @UiThread, @WorkerThread, …

(Support library 22.2及其之後版本支援.)

如果你的方法只能在指定的執行緒型別中被呼叫,那麼你就可以使用以下4個註解來標註它:

  • @UiThread
  • @MainThread
  • @WorkerThread
  • @BinderThread

如果一個類中的所有方法都有相同的執行緒需求,那麼你可以註解類本身。比如android.view.View,就被用@UiThread標註。

關於執行緒註解使用的一個很好的例子就是AsyncTask:

@WorkerThread
protected abstract Result doInBackground(Params... params);

@MainThread
protected void onProgressUpdate(Progress... values) {
}

如果你在重寫的doInBackground方法裡嘗試呼叫onProgressUpdate方法或者View的任何方法,IDE工具就會馬上把它標記為一個錯誤:
執行緒註解

@UiThread還是@MainThread?

在程序裡只有一個主執行緒。這個就是@MainThread。同時這個執行緒也是一個@UiThread。比如activity的主要視窗就執行在這個執行緒上。然而它也有能力為應用建立其他執行緒。這很少見,一般具備這樣功能的都是系統程序。通常是把和生命週期有關的用@MainThread標註,和View層級結構相關的用@UiThread標註。但是由於@MainThread本質上是一個@UiThread,而大部分情況下@UiThread又是一個@MainThread,所以工具(lint ,Android Studio,等等)可以把他們互換,所以你能在一個可以呼叫@MainThread方法的地方也能呼叫@UiThread方法,反之亦然。

RGB顏色整型

當你的API期望一個顏色資源的時候,可以用@ColorRes標註,但是當你有一個相反的使用場景時,這種用法就不可用了,因為你並不是期望一個顏色資源id,而是一個真實的RGB或者ARGB的顏色值。

在這種情況下,你可以使用@ColorInt註解,表示你期望的是一個代表顏色的整數值:

public void setTextColor(@ColorInt int color)

有了這個,當你傳遞一個顏色id而不是顏色值的時候,lint就會標記出這段不正確的程式碼:
顏色值註解

值約束: @Size, @IntRange, @FloatRange

如果你的引數是一個float或者double型別,並且一定要在某個範圍內,你可以使用@FloatRange註解:

public void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {

如果有人使用該API的時候傳遞一個0-255的值,比如嘗試呼叫setAlpha(128),那麼工具就會捕獲這一問題:

值約束註解

(你也可以指定是否包括起始值。)

同樣的,如果你的引數是一個int或者long型別,你可以使用@IntRange註解約束其值在一個特定的範圍內:

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

把這些註解應用到引數上是非常有用的,因為使用者很有可能會提供錯誤範圍的引數,比如上面的setAlpha例子,有的API是採用0-255的方式,而有的是採用0-1的float值的方式。

最後,對於資料、集合以及字串,你可以用@Size註解引數來限定集合的大小(當引數是字串的時候,可以限定字串的長度)。

舉幾個例子

  • 集合不能為空: @Size(min=1)
  • 字串最大隻能有23個字元: @Size(max=23)
  • 陣列只能有2個元素: @Size(2)
  • 陣列的大小必須是2的倍數 (例如圖形API中獲取位置的x/y座標陣列: @Size(multiple=2)

值約束註解

許可權註解: @RequiresPermission

如果你的方法的呼叫需要呼叫者有特定的許可權,你可以使用@RequiresPermission註解:

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

如果你至少需要許可權集合中的一個,你可以使用anyOf屬性:

@RequiresPermission(anyOf = {
    Manifest.permission.ACCESS_COARSE_LOCATION,
    Manifest.permission.ACCESS_FINE_LOCATION})
public abstract Location getLastKnownLocation(String provider);

如果你同時需要多個許可權,你可以用allOf屬性:

@RequiresPermission(allOf = {
    Manifest.permission.READ_HISTORY_BOOKMARKS,
    Manifest.permission.WRITE_HISTORY_BOOKMARKS})
public static final void updateVisitedHistory(ContentResolver cr, String url, boolean real) {

對於intents的許可權,可以直接在定義的intent常量字串欄位上標註許可權需求(他們通常都已經被@SdkConstant註解標註過了):

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

對於content providers的許可權,你可能需要單獨的標註讀和寫的許可權訪問,所以可以用@Read或者@Write標註每一個許可權需求:

@RequiresPermission.Read(@RequiresPermission(READ_HISTORY_BOOKMARKS))
@RequiresPermission.Write(@RequiresPermission(WRITE_HISTORY_BOOKMARKS))
public static final Uri BOOKMARKS_URI = Uri.parse("content://browser/bookmarks");

許可權註解

方法重寫: @CallSuper

如果你的API允許使用者重寫你的方法,但是呢,你又需要你自己的方法(父方法)在重寫的時候也被呼叫,這時候你可以使用@CallSuper標註:

@CallSuper
protected void onCreate(@Nullable Bundle savedInstanceState) {

用了這個後,當重寫的方法沒有呼叫父方法時,工具就會給予標記提示:

方法重寫

(Android Studio 1.3 Preview 1的lint檢查有個關於這個註解的bug,這個bug就是即使是對的重寫也會報錯,這個bug已經在Preview 2版本修改,可以通過canary channel更新到Preview 2版本。)

返回值: @CheckResult

如果你的方法返回一個值,你期望呼叫者用這個值做些事情,那麼你可以使用@CheckResult註解標註這個方法。

你並不需要微每個非空方法都進行標註。它主要的目的是幫助哪些容易被混淆,難以被理解的API的使用者。

比如,可能很多開發者都對String.trim()一知半解,認為呼叫了這個方法,就可以讓字串改變以去掉空白字元。如果這個方法被@CheckResult標註,工具就會對那些沒有使用trim()返回結果的呼叫者發出警告。

Android中,Context#checkPermission這個方法已經被@CheckResult標註了:

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

這是非常重要的,因為有些使用context.checkPermission的開發者認為他們已經執行了一個許可權 —-但其實這個方法僅僅只做了檢查並且反饋一個是否成功的值而已。如果開發者使用了這個方法,但是又不用其返回值,那麼這個開發者真正想呼叫的可能是這個Context#enforcePermission方法,而不是checkPermission。

返回值

@VisibleForTesting

你可以把這個註解標註到類、方法或者欄位上,以便你在測試的時候可以使用他們。

@Keep

我們還在註解庫裡添加了@Keep註解,但是Gradle外掛還支援(儘管已經在進行中)。被這個註解標註的類和方法在混淆的時候將不會被混淆。

在你自己的庫中使用註解

如果你在你自己的庫中使用了這些註解,並且是通過Gradle構建生成aar包,那麼在構建的時候Android Gradle外掛會提取註解資訊放在AAR檔案中供引用你的庫的客戶端使用。在AAR檔案中你可以看到一個名為annotations.zip的檔案,這個檔案記錄的就是註解資訊,使用的是IntelliJ的擴充套件註解XML格式。這是必須的,因為.class檔案不能包含足夠的要處理以上@IntDef註解的資訊;注意我們只需記錄該常量的一個引用,而不是它的值。當且僅當你的工程依賴註解庫的時候,Android Gradle外掛會把提取註解的任務作為構建的一部分執行它。(說明:只有源保留註解被放置在.aar檔案中;class級別的會被放在classes.jar裡。)

IntelliJ註解

IntelliJ,Android Studio就是基於它開發的,IntelliJ有一套它自己的註解;IntDef分析其實重用的是MagicConstant分析的程式碼,IntelliJ null分析其實用的是一組配置好的null註解。如果你執行Analyze > Infer Nullity…,它會試圖找出所有的null約束並新增他們。這個檢查有時會插入IntelliJ註解。你可以通過搜尋,替換為Android註解庫的註解,或者你也可以直接用IntelliJ註解。在build.gradle裡或者通過Project Structure對話方塊的Dependencies面板都可以新增如下依賴:

dependencies {
    compile 'com.intellij:annotations:12.0'
}