LoginSignup
47
45

More than 5 years have passed since last update.

【github code reading】みんな大好きButterKnifeを読んで学ぶJavaのアノテーション

Last updated at Posted at 2014-10-14

こんにちは。
みなさん、ButterKnifeというライブラリをご存知でしょうか?

今回はButterKnifeでのアノテーションについて調査しましたのでこの場を借りて書き連ねたいと思います。

ButterKnifeとは

ButterKnifeを使うとこれが

class ExampleActivity extends Activity {
  TextView title;
  TextView subtitle;
  TextView footer;

  @Override 
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    title = (TextView) findViewById(R.id.title);
    subtitle = (TextView) findViewById(R.id.subtitle);
    footer = (TextView) findViewById(R.id.footer);
    :
  }
:
}

こうなります。

class ExampleActivity extends Activity {
  @InjectView(R.id.title)
  TextView title;

  @InjectView(R.id.subtitle)
  TextView subtitle;

  @InjectView(R.id.footer)
  TextView footer;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.inject(this);
    // TODO Use "injected" views...
  }
}

すっきりしますね!

コードリーディング

どうして上記のような事が可能になるのか見ていきます。

アノテーションの定義箇所

まずはアノテーションの定義から。
injectViewのアノテーションは下記で定義されています。

InjectView.java
@Retention(CLASS) @Target(FIELD)
public @interface InjectView {
  /** View ID to which the field will be bound. */
  int value();
}

@Retantion はアノテーションの有効範囲を指定できます。CLASSの他に、RUNTIME、SOURCEがあります。
@Target は対象が誰かを設定しています。一番はじめの例にあるようにこのアノテーションはFieldに作用するのでFIELDを指定します。

実際に処理しているのは?

最初の例であげた

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.inject(this);
    // TODO Use "injected" views...
  }

ButterKnife.inject(this)の中身は下記です。 ButterKnife.java

ButterKnife.java
  public static void inject(Activity target) {
    inject(target, target, Finder.ACTIVITY);
  }

引数が四つの下記が呼ばれます

ButterKnife.java
  static void inject(Object target, Object source, Finder finder) {
    Class<?> targetClass = target.getClass();
    try {
      if (debug) Log.d(TAG, "Looking up view injector for " + targetClass.getName());
      Method inject = findInjectorForClass(targetClass); //①
      if (inject != null) {
        inject.invoke(null, finder, target, source);//②
      }
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      Throwable t = e;
      if (t instanceof InvocationTargetException) {
        t = t.getCause();
      }
      throw new RuntimeException("Unable to inject views for " + target, t);
    }
  }

対象のActivityのクラス名を引数として、injectなるメソッドを取得し(①)それを実行(②)しているようです。
おそらくこのメソッドがfindViewById()に化けるのだろうと想像がつきます。
findInjectForClassを見てみましょう。

ButterKnife.java

  private static Method findInjectorForClass(Class<?> cls) throws NoSuchMethodException {
    Method inject = INJECTORS.get(cls);
    if (inject != null) {
      if (debug) Log.d(TAG, "HIT: Cached in injector map.");
      return inject;
    }
    String clsName = cls.getName();
    if (clsName.startsWith(ANDROID_PREFIX) || clsName.startsWith(JAVA_PREFIX)) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      return NO_OP;
    }
    try {
      Class<?> injector = Class.forName(clsName + ButterKnifeProcessor.SUFFIX);
      inject = injector.getMethod("inject", Finder.class, cls, Object.class); // ③
      if (debug) Log.d(TAG, "HIT: Class loaded injection class.");
    } catch (ClassNotFoundException e) {
      if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
      inject = findInjectorForClass(cls.getSuperclass());
    }
    INJECTORS.put(cls, inject);
    return inject;
  }

ButterKnifeProcessor.SUFFIXは"\$\$ViewInjector"という文字列です。
つまるところ何をしているかというと、対象のAcitivity名 + $$ViewInjector という名前のクラスの inject というメソッドを取得しています(③)。

たとえば ExampleAcitivityというAcitivityがあったとき、ExcampleActivity$$ViewInjectorクラスのinjectメソッドを呼び出しているのです。

なるほど、なるほど、、、、、ん?
そう、対象のAcitivity名 + $$ViewInjector (とinjectメソッド)はどこで定義されているのでしょうか?

アノテーションでソースを生成

答えをいうと、「対象のAcitivity名 + $$ViewInjector (とinjectメソッド)」はButterKnifeProcessorのprocessメソッドで(ファイルレベルで)作成されます。

ButterKnifeProcessorはAbstractProcessorは継承し、processメソッドをOverrideしています。このAbstractProcessorのprocessメソッドでannotationを処理しているのです。

  @Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, ViewInjector> targetClassMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, ViewInjector> entry : targetClassMap.entrySet()) {
      TypeElement typeElement = entry.getKey();
      ViewInjector viewInjector = entry.getValue();

      try {
        JavaFileObject jfo = filer.createSourceFile(viewInjector.getFqcn(), typeElement);
        Writer writer = jfo.openWriter();
        writer.write(viewInjector.brewJava());
        writer.flush();
        writer.close();
      } catch (IOException e) {
        error(typeElement, "Unable to write injector for type %s: %s", typeElement, e.getMessage());
      }
    }

    return true;
  }

targetClassMapで各annotationの情報を落とし込み、viewInjector.brewJava()でクラスやメソッドを書き出しています。

どういう風に書き出すのか見てみると、たとえば

public class MyActivity extends Activity {

    @InjectView(R.id.test)
    TextView mTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_my);
        ButterKnife.inject(this);
        mTextView.setText("butter knife");
    }
}

とあった場合、MyActivity$$ViewInjector.javaというファイルができて
下記が書き出されます。

// Generated code from Butter Knife. Do not modify!
package com.butterknfietest;

import android.view.View;
import butterknife.ButterKnife.Finder;

public class MyActivity$$ViewInjector {
  public static void inject(Finder finder, final com.butterknfietest.MyActivity target, Object source) {
    View view;
    view = finder.findRequiredView(source, 2131230720, "field 'mTextView'");
    target.mTextView = (android.widget.TextView) view;
  }

  public static void reset(com.butterknfietest.MyActivity target) {
    target.mTextView = null;
  }
}

findRequiredViewでは下記にあるようにActivityやDilogによって処理は変わりますが、結局 findViewByIdメソッドに落とされます。

  public enum Finder {
    VIEW {
      @Override public View findOptionalView(Object source, int id) {
        return ((View) source).findViewById(id);
      }
    },
    ACTIVITY {
      @Override public View findOptionalView(Object source, int id) {
        return ((Activity) source).findViewById(id);
      }
    },
    DIALOG {
      @Override public View findOptionalView(Object source, int id) {
        return ((Dialog) source).findViewById(id);
      }
    };

    public static <T extends View> T[] arrayOf(T... views) {
      return views;
    }

    public static <T extends View> List<T> listOf(T... views) {
      return new ImmutableViewList<T>(views);
    }

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

ちなみに公式ではそこらへんの説明を端折ってinjectメソッドは

public void inject(ExampleActivity activity) {
  activity.subtitle = (android.widget.TextView) activity.findViewById(2130968578);
  activity.footer = (android.widget.TextView) activity.findViewById(2130968579);
  activity.title = (android.widget.TextView) activity.findViewById(2130968577);
}

って書き出されるよってなっています。

雑感

アノテーション自作してみようかなぁ。と思いました。
特にライブラリとの相性は良さそう。

47
45
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
45