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執行檢查。
@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 <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和”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
(Support library 22.2及其之後版本支持.)
如果你的方法只能在指定的線程類型中被調用,那麼你就可以使用以下4個注解來標注它:
如果一個類中的所有方法都有相同的線程需求,那麼你可以注解類本身。比如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方法,反之亦然。
當你的API期望一個顏色資源的時候,可以用@ColorRes標注,但是當你有一個相反的使用場景時,這種用法就不可用了,因為你並不是期望一個顏色資源id,而是一個真實的RGB或者ARGB的顏色值。
在這種情況下,你可以使用@ColorInt注解,表示你期望的是一個代表顏色的整數值:
public void setTextColor(@ColorInt int color)
有了這個,當你傳遞一個顏色id而不是顏色值的時候,lint就會標記出這段不正確的代碼:
如果你的參數是一個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注解參數來限定集合的大小(當參數是字符串的時候,可以限定字符串的長度)。
舉幾個例子
如果你的方法的調用需要調用者有特定的權限,你可以使用@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");
如果你的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注解標注這個方法。
你並不需要微每個非空方法都進行標注。它主要的目的是幫助哪些容易被混淆,難以被理解的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。
你可以把這個注解標注到類、方法或者字段上,以便你在測試的時候可以使用他們。
我們還在注解庫裡添加了@Keep注解,但是Gradle插件還支持(盡管已經在進行中)。被這個注解標注的類和方法在混淆的時候將不會被混淆。
如果你在你自己的庫中使用了這些注解,並且是通過Gradle構建生成aar包,那麼在構建的時候Android Gradle插件會提取注解信息放在AAR文件中供引用你的庫的客戶端使用。在AAR文件中你可以看到一個名為annotations.zip的文件,這個文件記錄的就是注解信息,使用的是IntelliJ的擴展注解XML格式。這是必須的,因為.class文件不能包含足夠的要處理以上@IntDef注解的信息;注意我們只需記錄該常量的一個引用,而不是它的值。當且僅當你的工程依賴注解庫的時候,Android Gradle插件會把提取注解的任務作為構建的一部分執行它。(說明:只有源保留注解被放置在.aar文件中;class級別的會被放在classes.jar裡。)
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'
}
更多Android相關信息見Android 專題頁面 http://www.linuxidc.com/topicnews.aspx?tid=11