【轉自】http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0427/2797.html
http://www.flysnow.org/2015/08/13/android-tech-docs-support-annotations.html
英文鏈接:http://anupcowkur.com/posts/a-look-at-android-support-annotations/
譯文鏈接 深入淺出Android Support Annotations
導讀:如果你之前遇到過在方法參數前面有@NonNull的情況卻不知道它是干什么的,這篇文章將解答你的疑問。
原文如下:
在Android Support Library19.1版本中,Android工具小組引入了幾個很酷的注解類型,供開發者在工程中使用。Support Library自身也使用這些注解,這是一個好兆頭。就讓我們好好研究下。 通過gradle可以很容易的把這些注解添加到我們的工程中:
compile 'com.android.support:support-annotations:20.0.0'
有三種類型的注解可供我們使用:
-
Nullness注解;
-
資源類型注解;
-
IntDef和StringDef注解;
我們將通過代碼例子來講解每一種類型的作用以及在工程中如何使用它們。
Nullness注解
使用@NonNull注解修飾的參數不能為null。在下面的代碼例子中,我們有一個取值為null的name變量,它被作為參數傳遞給sayHello函數,而該函數要求這個參數是非null的String類型:
public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); String name = null; sayHello(name); } void sayHello(@NonNull String s) { Toast.makeText(this, "Hello " + s, Toast.LENGTH_LONG).show(); } }
由於代碼中參數String s使用@NonNull注解修飾,因此IDE將會以警告的形式提醒我們這個地方有問題:

如果我們給name賦值,例如String name = “Our Lord Duarte”,那么警告將消失。使用@Nullable注解修飾的函數參數或者返回值可以為null。假設User類有一個名為name的變量,使用 User.getName()訪問,那么我們可以編寫如下代碼:
public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); User user = new User("Our Lord Duarte"); Toast.makeText(this, "Hello " + getName(user), Toast.LENGTH_LONG).show(); } @Nullable String getName(@NonNull User user) { return user.getName(); } }
因為getName函數的返回值使用@Nullable修飾,所以調用:
Toast.makeText(this, "Hello " + getName(user), Toast.LENGTH_LONG).show();
沒有檢查getName的返回值是否為空,將可能導致crash。
資源類型注解
是否曾經傳遞了錯誤的資源整型值給函數,還能夠愉快的得到本來想要的整型值嗎?資源類型注解可以幫助我們准確實現這一點。在下面的代碼中,我們的sayHello函數預期接受一個字符串類型的id,並使用@StringRes注解修飾:
public class MainActivity extends ActionBarActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); sayHello(R.style.AppTheme); } void sayHello(@StringRes int id) { Toast.makeText(this, "Hello " + getString(id), Toast.LENGTH_LONG).show(); } }
而我們傳遞了一個樣式資源id給它,這時IDE將提示警告如下:

類似的,我們把警告的地方使用一個字符串資源id代替警告就消失了:
sayHello(R.string.name);
IntDef和StringDef注解
我們要介紹的最后一種類型的注解是基於Intellij的“魔術常量”檢查機制(http://blog.jetbrains.com/idea/2012/02/new-magic-constant-inspection/)(我們不需要詳細了解這個機制具體是如何實現的,想了解的話可以點擊鏈接)。
很多時候,我們使用整型常量代替枚舉類型(性能考慮),例如我們有一個IceCreamFlavourManager類,它具有三種模式的操 作:VANILLA,CHOCOLATE和STRAWBERRY。我們可以定義一個名為@Flavour的新注解,並使用@IntDef指定它可以接受的值類型。
public class IceCreamFlavourManager { private int flavour; public static final int VANILLA = 0; public static final int CHOCOLATE = 1; public static final int STRAWBERRY = 2; @IntDef({VANILLA, CHOCOLATE, STRAWBERRY}) public @interface Flavour { } @Flavour public int getFlavour() { return flavour; } public void setFlavour(@Flavour int flavour) { this.flavour = flavour; } }
這時如果我們使用錯誤的整型值調用IceCreamFlavourManager.setFlavour時,IDE將報錯如下:

IDE甚至會提示我們可以使用的有效的取值:

我們也可以指定整型值作為標志位,也就是說這些整型值可以使用’|’或者’&’進行與或等操作。如果我們把@Flavour定義為如下標志位:
@IntDef(flag = true, value = {VANILLA, CHOCOLATE, STRAWBERRY}) public @interface Flavour { }
那么可以如下調用:
iceCreamFlavourManager.setFlavour(IceCreamFlavourManager.VANILLA & IceCreamFlavourManager.CHOCOLATE);
@StringDef用法和@IntDef基本差不多,只不過是針對String類型而已。
關於將來計划增加哪些新的注解類型或者這些注解的依賴以及和Intellij自身的注解如何交互等等問題,可以查看網址:http://tools.android.com/tech-docs/support-annotations。
我們可以使用@CheckResult注解來讓使用者知道該函數的返回值是需要使用的,沒有使用函數的返回值則Android Studio會給出警告提示:

@KEEP
最近我發現一個新的support annotation注解@Keep。根據support annotations文檔說明,這個注解還沒有被關聯到Gradle插件中,被這個注解修飾的函數或者類,在代碼混淆進行壓縮時會被保持住。你會知道當嘗試把某個特定的函數或者類從優化操作中排除掉是多么痛苦的事情。使用這個注解將會告訴Proguard不要對指定的函數或者類進行優化操作:
public class Example { @Keep public void doSomething() { // hopefully this method does something } ... }
我自己寫了一個例子:
package com.kale.lib.utils; import android.content.Context; import android.os.Looper; import android.support.annotation.IntDef; import android.support.annotation.NonNull; import android.widget.Toast; /** * @author Jack Tony * @date 2015/4/29 */ public class EasyToast { @IntDef({Toast.LENGTH_SHORT, Toast.LENGTH_LONG}) public @interface Length { } public static void makeText(@NonNull Context context, int msg) { makeText(context,String.valueOf(msg)); } public static void makeText(@NonNull Context context, String msg) { if (context != null) { Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); } } public static void makeText(@NonNull Context context, int msg, @Length int length) { makeText(context,String.valueOf(msg),length); } public static void makeText(@NonNull Context context, String msg, @Length int length) { if (length == Toast.LENGTH_SHORT || length == Toast.LENGTH_LONG) { if (context != null) { Toast.makeText(context, msg, length).show(); } } } public static void makeTextInThread(@NonNull final Context context, int msg) { makeTextInThread(context, String.valueOf(msg)); } public static void makeTextInThread(@NonNull Context context, String msg) { makeTextInThread(context, msg, Toast.LENGTH_SHORT); } public static void makeTextInThread(@NonNull Context context, int msg, @Length int length) { makeTextInThread(context,String.valueOf(msg),length); } public static void makeTextInThread(@NonNull final Context context, final String msg, @Length final int length) { new Thread() { @Override public void run() { Looper.prepare();//先移除 Toast.makeText(context, msg, length).show(); Looper.loop();// 進入loop中的循環,查看消息隊列 } }.start(); } }
資源類型注解
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
你可以把這個注解標注到類、方法或者字段上,以便你在測試的時候可以使用他們。
參考自:http://www.jianshu.com/p/1d0faca34a6e?utm_source=www.race604.com
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' }
