こんにちは。
みなさん、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のアノテーションは下記で定義されています。
@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
public static void inject(Activity target) {
inject(target, target, Finder.ACTIVITY);
}
引数が四つの下記が呼ばれます
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を見てみましょう。
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);
}
って書き出されるよってなっています。
##雑感
アノテーション自作してみようかなぁ。と思いました。
特にライブラリとの相性は良さそう。