Android 中的註解深入探究

技術小黑屋發表於2016-08-20

本文系GDG Android Meetup分享內容總結文章

註解是我們經常接觸的技術,Java有註解,Android也有註解,本文將試圖介紹Android中的註解,以及ButterKnife和Otto這些基於註解的庫的一些工作原理.

歸納而言,Android中的註解大概有以下好處

  • 提高我們的開發效率
  • 更早的發現程式的問題或者錯誤
  • 更好的增加程式碼的描述能力
  • 更加利於我們的一些規範約束
  • 提供解決問題的更優解

準備工作

預設情況下,Android中的註解包並沒有包括在framework中,它獨立成一個單獨的包,通常我們需要引入這個包.

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

但是如果我們已經引入了appcompat則沒有必要再次引用support-annotations,因為appcompat預設包含了對其引用.

替代列舉

在最早的時候,當我們想要做一些值得限定實現列舉的效果,通常是

  • 定義幾個常量用於限定
  • 從上面的常量選取值進行使用

一個比較描述上面問題的示例程式碼如下

public static final int COLOR_RED = 0;
public static final int COLOR_GREEN = 1;
public static final int COLOR_YELLOW = 2;

public void setColor(int color) {
    //some code here
}
//呼叫
setColor(COLOR_RED)

然而上面的還是有不盡完美的地方

  • setColor(COLOR_RED)setColor(0)效果一樣,而後者可讀性很差,但卻可以正常執行
  • setColor方法可以接受列舉之外的值,比如setColor(3),這種情況下程式可能出問題

一個相對較優的解決方法就是使用Java中的Enum.使用列舉實現的效果如下

// ColorEnum.java
public enum ColorEmun {
    RED,
    GREEN,
    YELLOW
}

public void setColorEnum(ColorEmun colorEnum) {
    //some code here
}

setColorEnum(ColorEmun.GREEN);

然而Enum也並非最佳,Enum因為其相比方案一的常量來說,佔用記憶體相對大很多而受到曾經被Google列為不建議使用,為此Google特意引入了一些相關的註解來替代列舉.

Android中新引入的替代列舉的註解有IntDefStringDef,這裡以IntDef做例子說明一下.

public class Colors {
    @IntDef({RED, GREEN, YELLOW})
    @Retention(RetentionPolicy.SOURCE)
    public @interface LightColors{}

    public static final int RED = 0;
    public static final int GREEN = 1;
    public static final int YELLOW = 2;
}
  • 宣告必要的int常量
  • 宣告一個註解為LightColors
  • 使用@IntDef修飾LightColors,引數設定為待列舉的集合
  • 使用@Retention(RetentionPolicy.SOURCE)指定註解僅存在與原始碼中,不加入到class檔案中

Null相關的註解

和Null相關的註解有兩個

@Nullable 註解的元素可以是Null
@NonNull 註解的元素不能是Null

上面的兩個可以修飾如下的元素

  • 成員屬性
  • 方法引數
  • 方法的返回值
@Nullable
private String obtainReferrerFromIntent(@NonNull Intent intent) {
    return intent.getStringExtra("apps_referrer");
}

NonNull檢測生效的條件

  • 顯式傳入null
  • 在呼叫方法之前已經判斷了引數為null時
setReferrer(null);//提示警告

//不提示警告
String referrer = getIntent().getStringExtra("apps_referrer");
setReferrer(referrer);

//提示警告
String referrer = getIntent().getStringExtra("apps_referrer");
if (referrer == null) {
    setReferrer(referrer);
}

private void setReferrer(@NonNull String referrer) {
    //some code here
}

區間範圍註解

Android中的IntRange和FloatRange是兩個用來限定區間範圍的註解,

float currentProgress;

public void setCurrentProgress(@FloatRange(from=0.0f, to=1.0f) float progress) {
    currentProgress = progress;
}

如果我們傳入非法的值,如下所示

setCurrentProgress(11);

就會得到這樣的錯誤

Value must be >=0.0 and <= 1.0(was 11)

長度以及陣列大小限制

限制字串的長度

private void setKey(@Size(6) String key) {
}

限定陣列集合的大小

private void setData(@Size(max = 1) String[] data) {
}
setData(new String[]{"b", "a"});//error occurs

限定特殊的陣列長度,比如3的倍數

private void setItemData(@Size(multiple = 3) String[] data) {
}

許可權相關

在Android中,有很多場景都需要使用許可權,無論是Marshmallow之前還是之後的動態許可權管理.都需要在manifest中進行宣告,如果忘記了,則會導致程式崩潰. 好在有一個註解能輔助我們避免這個問題.使用RequiresPermission註解即可.

@RequiresPermission(Manifest.permission.SET_WALLPAPER)
    public void changeWallpaper(Bitmap bitmap) throws IOException {
}

資源註解

在Android中幾乎所有的資源都可以有對應的資源id.比如獲取定義的字串,我們可以通過下面的方法

public String getStringById(int stringResId) {
    return getResources().getString(stringResId);
}

使用這個方法,我們可以很容易的獲取到定義的字串,但是這樣的寫法也存在著風險.

getStringById(R.mipmap.ic_launcher)

如果我們在不知情或者疏忽情況下,傳入這樣的值,就會出現問題. 但是如果我們使用資源相關的註解修飾了引數,就能很大程度上避免錯誤的情況.

public String getStringById(@StringRes  int stringResId) {
    return getResources().getString(stringResId);
}

在Android中資源註解如下所示

  • AnimRes
  • AnimatorRes
  • AnyRes
  • ArrayRes
  • AttrRes
  • BoolRes
  • ColorRes
  • DimenRes
  • DrawableRes
  • FractionRes
  • IdRes
  • IntegerRes
  • InterpolatorRes
  • LayoutRes
  • MenuRes
  • PluralsRes
  • RawRes
  • StringRes
  • StyleRes
  • StyleableRes
  • TransitionRes
  • XmlRes

Color值限定

上面部分提到了ColorRes,用來限定顏色資源id,這裡我們將使用ColorInt,一個用來限定Color值的註解. 在較早的TextView的setTextColor是這樣實現的.

public void setTextColor(int color) {
    mTextColor = ColorStateList.valueOf(color);
    updateTextColors();
}

然而上面的方法在呼叫時常常會出現這種情況

myTextView.setTextColor(R.color.colorAccent);

如上,如果傳遞過去的引數為color的資源id就會出現顏色取錯誤的問題,這個問題在過去還是比較嚴重的.好在ColorInt出現了,改變了這一問題.

public void setTextColor(@ColorInt int color) {
    mTextColor = ColorStateList.valueOf(color);
    updateTextColors();
}

當我們再次傳入Color資源值時,就會得到錯誤的提示.

CheckResult

這是一個關於返回結果的註解,用來註解方法,如果一個方法得到了結果,卻沒有使用這個結果,就會有錯誤出現,一旦出現這種錯誤,就說明你沒有正確使用該方法。

@CheckResult
public String trim(String s) {
    return s.trim();
}

執行緒相關

Android中提供了四個與執行緒相關的註解

  • @UiThread,通常可以等同於主執行緒,標註方法需要在UIThread執行,比如View類就使用這個註解
  • @MainThread 主執行緒,經常啟動後建立的第一個執行緒
  • @WorkerThread 工作者執行緒,一般為一些後臺的執行緒,比如AsyncTask裡面的doInBackground就是這樣的.
  • @BinderThread 註解方法必須要在BinderThread執行緒中執行,一般使用較少.

一些示例

new AsyncTask<Void, Void, Void>() {
        //doInBackground is already annotated with @WorkerThread
        @Override
        protected Void doInBackground(Void... params) {
            return null;
            updateViews();//error
        }
    };

@UiThread
public void updateViews() {
    Log.i(LOGTAG, "updateViews ThreadInfo=" + Thread.currentThread());
}

注意,這種情況下不會出現錯誤提示

new Thread(){
    @Override
    public void run() {
        super.run();
        updateViews();
    }
}.start();

雖然updateViews會在一個新的工作者執行緒中執行,但是在compile時沒有錯誤提示.

因為它的判斷依據是,如果updateView的執行緒註解(這裡為@UiThread)和run(沒有執行緒註解)不一致才會錯誤提示.如果run方法沒有執行緒註解,則不提示.

CallSuper

重寫的方法必須要呼叫super方法

使用這個註解,我們可以強制方法在重寫時必須呼叫父類的方法 比如Application的onCreate,onConfigurationChanged等.

Keep

在Android編譯生成APK的環節,我們通常需要設定minifyEnabled為true實現下面的兩個效果

  • 混淆程式碼
  • 刪除沒有用的程式碼

但是出於某一些目的,我們需要不混淆某部分程式碼或者不刪除某處程式碼,除了配置複雜的Proguard檔案之外,我們還可以使用@Keep註解 .

@Keep
public static int getBitmapWidth(Bitmap bitmap) {
    return bitmap.getWidth();
}

ButterKnife

ButterKnife是一個用來繫結View,資源和回撥的提高效率的工具.作者為Jake Wharton. ButterKnife的好處

  • 使用BindView替代繁瑣的findViewById和型別轉換
  • 使用OnClick註解方法來替換顯式宣告的匿名內部類
  • 使用BindString,BindBool,BindDrawable等註解實現資源獲取

一個摘自Github的示例

class ExampleActivity extends Activity {
  @BindView(R.id.user) EditText username;
  @BindView(R.id.pass) EditText password;

  @BindString(R.string.login_error) String loginErrorMessage;

  @OnClick(R.id.submit) void submit() {
    // TODO call server...
  }

  @Override public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);
    // TODO Use fields...
  }
}

ButterKnife工作原理

以BindView註解使用為例,示例程式碼為

public class MainActivity extends AppCompatActivity {
    @BindView(R.id.myTextView)
    TextView myTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
    }
}

1.程式在compile時,會根據註解自動生成兩個類,這裡為MainActivity_ViewBinder.classMainActivity_ViewBinding.class
2.當我們呼叫ButterKnife.bind(this);時,會查詢當前類對應的ViewBinder類,並呼叫bind方法,這裡會呼叫到MainActiivty_ViewBinder.bind方法.
3.MainActiivty_ViewBinder.bind方法實際上是呼叫了findViewById然後在進行型別轉換,賦值給MainActivity的myTextView屬性

ButterKnife的bind方法

public static Unbinder bind(@NonNull Activity target) {
    return getViewBinder(target).bind(Finder.ACTIVITY, target, target);
}

ButterKnife的getViewBinderfindViewBinderForClass

@NonNull @CheckResult @UiThread
  static ViewBinder<Object> getViewBinder(@NonNull Object target) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
    return findViewBinderForClass(targetClass);
  }

  @NonNull @CheckResult @UiThread
  private static ViewBinder<Object> findViewBinderForClass(Class<?> cls) {
   //如果記憶體集合BINDERS中包含,則不再查詢
    ViewBinder<Object> viewBinder = BINDERS.get(cls);
    if (viewBinder != null) {
      if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
      return viewBinder;
    }
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return NOP_VIEW_BINDER;
    }
    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
      //使用反射建立例項
      Class<?> viewBindingClass = Class.forName(clsName + "_ViewBinder");
      //noinspection unchecked
      viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
      if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
    } catch (ClassNotFoundException e) {
        //如果沒有找到,對父類進行查詢
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      viewBinder = findViewBinderForClass(cls.getSuperclass());
    } catch (InstantiationException e) {
      throw new RuntimeException("Unable to create view binder for " + clsName, e);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Unable to create view binder for " + clsName, e);
    }
    //加入記憶體集合,便於後續的查詢
    BINDERS.put(cls, viewBinder);
    return viewBinder;
  }

MainActivity_ViewBinder的反編譯原始碼

➜  androidannotationsample javap -c MainActivity_ViewBinder
Warning: Binary file MainActivity_ViewBinder contains com.example.admin.androidannotationsample.MainActivity_ViewBinder
Compiled from "MainActivity_ViewBinder.java"
public final class com.example.admin.androidannotationsample.MainActivity_ViewBinder implements butterknife.internal.ViewBinder<com.example.admin.androidannotationsample.MainActivity> {
  public com.example.admin.androidannotationsample.MainActivity_ViewBinder();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public butterknife.Unbinder bind(butterknife.internal.Finder, com.example.admin.androidannotationsample.MainActivity, java.lang.Object);
    Code:
       0: new           #2                  // class com/example/admin/androidannotationsample/MainActivity_ViewBinding
       3: dup
       4: aload_2
       5: aload_1
       6: aload_3                           // 建立ViewBinding例項
       7: invokespecial #3                  // Method com/example/admin/androidannotationsample/MainActivity_ViewBinding."<init>":(Lcom/example/admin/androidannotationsample/MainActivity;Lbutterknife/internal/Finder;Ljava/lang/Object;)V
      10: areturn

  public butterknife.Unbinder bind(butterknife.internal.Finder, java.lang.Object, java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: aload_2
       3: checkcast     #4                  // class com/example/admin/androidannotationsample/MainActivity
       6: aload_3                           //呼叫上面的過載方法
       7: invokevirtual #5                  // Method bind:(Lbutterknife/internal/Finder;Lcom/example/admin/androidannotationsample/MainActivity;Ljava/lang/Object;)Lbutterknife/Unbinder;
      10: areturn
}

MainActivity_ViewBinding的反編譯原始碼

➜  androidannotationsample javap -c MainActivity_ViewBinding
Warning: Binary file MainActivity_ViewBinding contains com.example.admin.androidannotationsample.MainActivity_ViewBinding
Compiled from "MainActivity_ViewBinding.java"
public class com.example.admin.androidannotationsample.MainActivity_ViewBinding<T extends com.example.admin.androidannotationsample.MainActivity> implements butterknife.Unbinder {
  protected T target;

  public com.example.admin.androidannotationsample.MainActivity_ViewBinding(T, butterknife.internal.Finder, java.lang.Object);
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: aload_1
       6: putfield      #2                  // Field target:Lcom/example/admin/androidannotationsample/MainActivity;
       9: aload_1
      10: aload_2
      11: aload_3                           //呼叫Finder.findRequireViewAsType找到View,並進行型別轉換,並複製給MainActivity中對一個的變數
      12: ldc           #4                  // int 2131427412
      14: ldc           #5                  // String field 'myTextView'
      16: ldc           #6                  // class android/widget/TextView
                                            // 內部實際呼叫了findViewById
      18: invokevirtual #7                  // Method butterknife/internal/Finder.findRequiredViewAsType:(Ljava/lang/Object;ILjava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
      21: checkcast     #6                  // class android/widget/TextView
      24: putfield      #8                  // Field com/example/admin/androidannotationsample/MainActivity.myTextView:Landroid/widget/TextView;
      27: return

  public void unbind();
    Code:
       0: aload_0
       1: getfield      #2                  // Field target:Lcom/example/admin/androidannotationsample/MainActivity;
       4: astore_1
       5: aload_1
       6: ifnonnull     19
       9: new           #9                  // class java/lang/IllegalStateException
      12: dup
      13: ldc           #10                 // String Bindings already cleared.
      15: invokespecial #11                 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
      18: athrow
      19: aload_1
      20: aconst_null                       // 解除繫結,設定對應的變數為null
      21: putfield      #8                  // Field com/example/admin/androidannotationsample/MainActivity.myTextView:Landroid/widget/TextView;
      24: aload_0
      25: aconst_null
      26: putfield      #2                  // Field target:Lcom/example/admin/androidannotationsample/MainActivity;
      29: return
}

Finder的原始碼

package butterknife.internal;

import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.support.annotation.IdRes;
import android.view.View;

@SuppressWarnings("UnusedDeclaration") // Used by generated code.
public enum Finder {
  VIEW {
    @Override public View findOptionalView(Object source, @IdRes int id) {
      return ((View) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return ((View) source).getContext();
    }

    @Override protected String getResourceEntryName(Object source, @IdRes int id) {
      final View view = (View) source;
      // In edit mode, getResourceEntryName() is unsupported due to use of BridgeResources
      if (view.isInEditMode()) {
        return "<unavailable while editing>";
      }
      return super.getResourceEntryName(source, id);
    }
  },
  ACTIVITY {
    @Override public View findOptionalView(Object source, @IdRes int id) {
      return ((Activity) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return (Activity) source;
    }
  },
  DIALOG {
    @Override public View findOptionalView(Object source, @IdRes int id) {
      return ((Dialog) source).findViewById(id);
    }

    @Override public Context getContext(Object source) {
      return ((Dialog) source).getContext();
    }
  };

  //查詢對應的Finder,如上面的ACTIVITY, DIALOG, VIEW
  public abstract View findOptionalView(Object source, @IdRes int id);

  public final <T> T findOptionalViewAsType(Object source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findOptionalView(source, id);
    return castView(view, id, who, cls);
  }

  public final View findRequiredView(Object source, @IdRes int id, String who) {
    View view = findOptionalView(source, id);
    if (view != null) {
      return view;
    }
    String name = getResourceEntryName(source, id);
    throw new IllegalStateException("Required view '"
        + name
        + "' with ID "
        + id
        + " for "
        + who
        + " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
        + " (methods) annotation.");
  }

  //來自ViewBinding的呼叫
  public final <T> T findRequiredViewAsType(Object source, @IdRes int id, String who,
      Class<T> cls) {
    View view = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
  }

  public final <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
    try {
      return cls.cast(view);
    } catch (ClassCastException e) {
      String name = getResourceEntryName(view, id);
      throw new IllegalStateException("View '"
          + name
          + "' with ID "
          + id
          + " for "
          + who
          + " was of the wrong type. See cause for more info.", e);
    }
  }

  @SuppressWarnings("unchecked") // That's the point.
  public final <T> T castParam(Object value, String from, int fromPos, String to, int toPos) {
    try {
      return (T) value;
    } catch (ClassCastException e) {
      throw new IllegalStateException("Parameter #"
          + (fromPos + 1)
          + " of method '"
          + from
          + "' was of the wrong type for parameter #"
          + (toPos + 1)
          + " of method '"
          + to
          + "'. See cause for more info.", e);
    }
  }

  protected String getResourceEntryName(Object source, @IdRes int id) {
    return getContext(source).getResources().getResourceEntryName(id);
  }

  public abstract Context getContext(Object source);
}

Otto

Otto Bus 是一個專為Android改裝的Event Bus,在很多專案中都有應用.由Square開源共享.

public class EventBusTest {
    private static final String LOGTAG = "EventBusTest";
    Bus mBus  = new Bus();

    public void test() {
        mBus.register(this);
    }

    class NetworkChangedEvent {

    }

    @Produce
    public NetworkChangedEvent sendNetworkChangedEvent() {
        return new NetworkChangedEvent();
    }

    @Subscribe
    public void onNetworkChanged(NetworkChangedEvent event) {
        Log.i(LOGTAG, "onNetworkChanged event=" + event);
    }
}

Otto 的工作原理

  • 使用@Produce和@Subscribe標記方法
  • 當呼叫bus.register方法,去檢索註冊物件的標記方法,並cache對映關係
  • 當post事件時,將事件與handler方法對應加入事件佇列
  • 抽取事件佇列,然後呼叫handler處理

如下為對Otto如何利用註解的分析

register的原始碼

public void register(Object object) {
    if (object == null) {
      throw new NullPointerException("Object to register must not be null.");
    }
    enforcer.enforce(this);
    //查詢object中的Subscriber
    Map<Class<?>, Set<EventHandler>> foundHandlersMap = handlerFinder.findAllSubscribers(object);
    for (Class<?> type : foundHandlersMap.keySet()) {
      Set<EventHandler> handlers = handlersByType.get(type);
      if (handlers == null) {
        //concurrent put if absent
        Set<EventHandler> handlersCreation = new CopyOnWriteArraySet<EventHandler>();
        handlers = handlersByType.putIfAbsent(type, handlersCreation);
        if (handlers == null) {
            handlers = handlersCreation;
        }
      }
      final Set<EventHandler> foundHandlers = foundHandlersMap.get(type);
      if (!handlers.addAll(foundHandlers)) {
        throw new IllegalArgumentException("Object already registered.");
      }
    }

    for (Map.Entry<Class<?>, Set<EventHandler>> entry : foundHandlersMap.entrySet()) {
      Class<?> type = entry.getKey();
      EventProducer producer = producersByType.get(type);
      if (producer != null && producer.isValid()) {
        Set<EventHandler> foundHandlers = entry.getValue();
        for (EventHandler foundHandler : foundHandlers) {
          if (!producer.isValid()) {
            break;
          }
          if (foundHandler.isValid()) {
            dispatchProducerResultToHandler(foundHandler, producer);
          }
        }
      }
    }
  }

HandlerFinder原始碼

interface HandlerFinder {

  Map<Class<?>, EventProducer> findAllProducers(Object listener);

  Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener);

  //Otto註解查詢器
  HandlerFinder ANNOTATED = new HandlerFinder() {
    @Override
    public Map<Class<?>, EventProducer> findAllProducers(Object listener) {
      return AnnotatedHandlerFinder.findAllProducers(listener);
    }

    @Override
    public Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener) {
      return AnnotatedHandlerFinder.findAllSubscribers(listener);
    }
  };

具體查詢實現

/** This implementation finds all methods marked with a {@link Subscribe} annotation. */
  static Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener) {
    Class<?> listenerClass = listener.getClass();
    Map<Class<?>, Set<EventHandler>> handlersInMethod = new HashMap<Class<?>, Set<EventHandler>>();

    Map<Class<?>, Set<Method>> methods = SUBSCRIBERS_CACHE.get(listenerClass);
    if (null == methods) {
      methods = new HashMap<Class<?>, Set<Method>>();
      loadAnnotatedSubscriberMethods(listenerClass, methods);
    }
    if (!methods.isEmpty()) {
      for (Map.Entry<Class<?>, Set<Method>> e : methods.entrySet()) {
        Set<EventHandler> handlers = new HashSet<EventHandler>();
        for (Method m : e.getValue()) {
          handlers.add(new EventHandler(listener, m));
        }
        handlersInMethod.put(e.getKey(), handlers);
      }
    }

    return handlersInMethod;
  }

以上就是關於Android中註解的一些總結,文章部分內容參考自 Support Annotations ,希望能幫助大家對註解有基礎的認識,並運用到實際的日常開發之中。

相關文章