Androidの注釈を深く分析する。

24979 ワード

要するに、Androidの注釈は以下のような利点があります。
      1、私たちの開発効率を向上させる。
      2、早くプログラムの問題やエラーを発見する
      3、より良いのはコードの説明能力を増加します。
      4、私達のいくつかの規範的な制約にもっと有利です。
      5、問題解決のためのより良い解を提供する。
準備工作
デフォルトでは、Androidの注釈パケットはフレームワークに含まれていません。単独のパッケージになっています。このバッグを導入する必要があります。

dependencies {
  compile 'com.android.support:support-annotations:22.2.0'
}
しかし、appcompmpmputを導入したら、再度support-annotationsを参照する必要はありません。appcomppatはデフォルトで引用が含まれています。
代替列挙
最初に、いくつかの限定的な列挙の効果を達成するためにしたいと思いますが、通常は
      1、いくつかの定数を定義して限定する
      2、上記の定数から値を選んで使用します。
上記の問題を説明するための比較コードの例は以下の通りです。

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)
しかし、上には完璧ではないところがあります。
      1、set Color(COLOR_)RED)はset Color(0)の効果と同じで、後者は可読性が悪いが、正常に動作することができる。
      2、set Color方法は、列挙以外の値を受け入れることができます。例えば、set Color(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に新しく導入された代替列挙の注釈はIntDefとStringDefがあります。ここでは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;
}
      1、必要なint定数を宣言する
      2、声明の一つの注釈はLightColorsである。
      3、@IntDefを使ってLight Colorsを修飾し、パラメータは列挙待ちの集合に設定する。
      4、@Retension(Retension Policy.SOURCE)で指定された注釈を使って、ソースコードの中にしか存在しないので、classファイルには加入しません。
Null関連のコメント
Nullに関する注釈は二つあります。
       @Nullableの注釈の要素はNullとすることができます。
       @NonNull注の要素はNullではありません。
上の二つは次のような元素を修飾できます。
       1、メンバーの属性
       2、方法パラメータ
       3、方法の戻り値

@Nullable
private String obtainReferrerFromIntent(@NonNull Intent intent) {
  return intent.getStringExtra("apps_referrer");
}
NonNull検出有効条件
      1、顕式着信null
      2、メソッドを呼び出す前にパラメータが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は、区間範囲を限定する2つの注釈であり、

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では、多くの場面で使用権限が必要です。Mashmallowの前にも後にも動的権限管理が必要です。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
      アニメイトReses
      AnyRes
      アラシー
      リセット
      BoolRes
      ColorRes
      DimenRes
      Drawable Reses
      FractionRes
      IdRes
      インテグレーツ
      Interpolators
      LayoutRes
      MenuRes
      PluralsRes
      RawRes
      StringRes
      Style Res
      Style Reses
      Transitions Res
      Xml Reses
Color値限定
上の部分はColorResに言及していますが、色資源idを限定するためにここでColorIntを使います。Color値を限定する注釈です。以前のTextViewのsetText Colorはこのように実現されます。

public void setTextColor(int color) {
  mTextColor = ColorStateList.valueOf(color);
  updateTextColors();
}
しかし、上記の方法は呼び出し時によく発生します。

myTextView.setTextColor(R.color.colorAccent);
以上のように、過去のパラメータがカラーのリソースIDであると、色取りのエラーが発生します。この問題は過去にも深刻です。幸いColorIntが発生し、この問題を変えました。

public void setTextColor(@ColorInt int color) {
  mTextColor = ColorStateList.valueOf(color);
  updateTextColors();
}
Colorリソース値が再び入ってきたら、エラーのヒントが得られます。
CheckResoult
これは戻り結果についての注釈です。一つの方法が結果を得たら、この結果を使っていないと、エラーが発生します。このようなエラーが発生したら、正しい使い方をしていないということです。

@CheckResult
public String trim(String s) {
  return s.trim();
}
スレッド関連
Androidではスレッドに関する4つの注釈が提供されている。
      @UiThreadは通常メインスレッドと同じです。表示方法はUID Threadで実行する必要があります。例えば、View類はこの注釈を使います。
      @MainThreadメインスレッドは、常に起動後に作成される最初のスレッドです。
      @Worket Thread作業者スレッドは、一般的にいくつかのバックグラウンドのスレッドです。例えば、AyncTaskの中の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();
udateViewsは新しい作業者スレッドで実行されますが、compleではエラーメッセージがありません。
その判断根拠は、udateViewのスレッド書き込み(ここでは@UiThread)とrun(スレッドコメントがない)が一致しないとエラーメッセージになります。
CallSuper
書き換えの方法はsuperメソッドを呼び出す必要があります。
この注釈を使って、私達は強制的に方法を書き換えることができます。例えば、AppplicationのonCreate、onConfigrationChangedなどの父の方法を呼び出さなければなりません。
Keep
AndroidコンパイルでAPKを生成する環節において、minifyEnbaledを設定してtrueのために次の二つの効果を実現する必要があります。
      1、混淆コード
      2、不要なコードを削除する
しかし、ある目的のために、一部のコードを混同しないか、またはどこかのコードを削除しない必要があります。複雑なプログラードファイルを配置する以外に、@Keep注解を使ってもいいです。

@Keep
public static int getBitmapWidth(Bitmap bitmap) {
  return bitmap.getWidth();
}
ButterKnife
ButterKnifeはViewを結びつけるためのもので、資源とフィードバックの効率化を図るためのツールです。著者はJake Wharn.ButterrKnifeのメリットです。
       1、BindViewを使用して、煩雑なfindViewByIdとタイプ転換を代替する。
       2、OnClick注釈方法を使用して明示的な声明の匿名内部クラスを置換する。
      3、BindStering、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.プログラムはcompleの場合、注釈によって自動的に2つのクラスが生成されます。ここはMainActivity_ViewBinder.class MainActivity_ViewBinding.classです。
2.ButterKnife.bind(this); を呼び出すと、現在のクラスの対応するViewBinderクラスを検索し、bind方法を呼び出します。ここでMainActiivty_ViewBinder.bind 方法を呼び出します。
3.MainActiivty_ViewBinder.bind方法は実際にfindViewByIdを呼び出してタイプ変換を行い、MainActivitymyTextViewの属性に値を付与する。
ButterKnifeのbind方法

public static Unbinder bind(@NonNull Activity target) {
  return getViewBinder(target).bind(Finder.ACTIVITY, target, target);
}
ButterKnifeのget View BinderとfindView BinderForClass

@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_View Binderの逆コンパイルソース

➜ 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_View Bindingの逆コンパイルソース

➜ 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の作業原理
      1、@Produceと@Subscribeの表記方法を使う
      2、bus.registerメソッドを呼び出し、登録対象のマーク方法を検索し、cacheマッピング関係
      3、postイベントの場合は、イベントをhandlerメソッドに対応してイベントキューに追加します。
      4、イベントキューを抽出し、ハンドルを呼び出して処理する。
Ottoに対してどうやって注釈を利用するかの分析です。
レジスターのソースコード

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);
     }
    }
   }
  }
 }
Handler Finderソース

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から参考にして、注釈に対する基礎的な認識を助けて、実際の日常開発に応用してください。質問があれば、皆さんのコメントを歓迎します。