0 使用手册
http://jakewharton.github.io/butterknife/
1 使用方法简介
class ExampleActivity extends Activity {
@InjectView(R.id.title) TextView title1;
@InjectView(R.id.title) TextView title2;
@InjectViews({ R.id.first_name, R.id.middle_name,R.id.last_name })
List nameViews;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.inject(this);
// TODO Use "injected" views...
}
}
@OnClick({ R.id.door1, R.id.door2, R.id.door3 })
public void pickDoor(DoorView door) {
if (door.hasPrizeBehind()) {
Toast.makeText(this, "You win!", LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Try again", LENGTH_SHORT).show();
}
}
@OnTextChanged(value=R.id.submit,callback=OnTextChanged.Callback.TEXT_CHANGED)
public void sayHi() {
button.setText("Hello!");
}
@OnTextChanged(value=R.id.submit,callback=OnTextChanged.Callback.TEXT_CHANGED)
public void sayHello() {
button.setText("Hello!");
}
2 基本原理
在编译之前,利用Java的预处理功能,扫描每一个有注解的类,找出类中注解的字段生成ViewBinding,每一个方法生成ListenerBinding,形成一个ViewInjector,再利用Java的Filer生成一个有注入代码的类文件,最后在执行阶段,当调用ButterKnife.inject()方法时调用生成的类文件完成注入功能。
3 数据结构分析
@ListenerClass:代表一个事件监听器,1部分中每注解一个方法都是使用了一个事件监听器,如@OnClick最后会生成一个OnClickListener对象,该注解表明了每一个方法注解所对应的事件监听器。callbacks:如果事件监听器中有多个响应方法,则通过这个字段区分,如果只有一个方法,则在method字段中注明。
@ListenerMethod:代表事件监听器中的一个方法,例如OnClickListener对象中的OnClick方法
@OnClick : 方法注解,表示需要生成对应类型的事件监听器。
@InjectView:字段注解,表示需要为该字段生成相应的注入代码
@InjectViews:数组或列表字段注解,表示需要为该字段生成响应的注入代码。
Finder:查找注解id,根据注解的id,找到响应的View。
ViewBinding:代表一个注解字段,例如1部分中的 title1,title2字段。
CollectionBinding:代表一个数组或列表注解字段,例如1部分中的nameViews字段。
ListenerBinding:代表一个注解方法,例如1部分中的sayHi() , sayHello()。
ViewInjection:代表一个注解id,将同一个id的所有注解字段,注解方法整合在一起,形成ViewInjection。
ViewInjector:代表一个注解类,例如1部分中的ExampleActivity ,每一个有注解的类都生成一个ViewInjector,在编译之前使用该ViewInjector,为每一个类生成XXXX$$ViewInjector,生成响应的注入代码
4 代码分析
ButterKnife.inject(Object target, Object source, Finder finder)
//加载编译前生成的XXXX$$ViewInjector类,调用该类的inject方法,该inject方法利用finder根据注解中的id查找到响应的View,为该view设置事件监听器,再为target注入该view。
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());
Injector<Object> injector = findInjectorForClass(targetClass);
if (injector != null) {
injector.inject(finder, target, source);
}
} catch (Exception e) {
throw new RuntimeException("Unable to inject views for " + targetClass.getName(), e);
}
}
ButterKnifeProcessor.process
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
//先生成ViewInjector
Map<TypeElement, ViewInjector> targetClassMap = findAndParseTargets(env);
//使用ViewInjector动态生成XXXX$$ViewInjector.java代码
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;
}
ButterKnife.findAndParseTargets
private Map<TypeElement, ViewInjector> findAndParseTargets(RoundEnvironment env) {
//处理@InjectView;
for (Element element : env.getElementsAnnotatedWith(InjectView.class)) {
try {
parseInjectView(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
StringWriter stackTrace = new StringWriter();
e.printStackTrace(new PrintWriter(stackTrace));
error(element, "Unable to generate view injector for @InjectView.\n\n%s", stackTrace);
}
}
//处理@InjectViews;
// Process each @InjectViews element.
for (Element element : env.getElementsAnnotatedWith(InjectViews.class)) {
try {
parseInjectViews(element, targetClassMap, erasedTargetNames);
} catch (Exception e) {
StringWriter stackTrace = new StringWriter();
e.printStackTrace(new PrintWriter(stackTrace));
error(element, "Unable to generate view injector for @InjectViews.\n\n%s", stackTrace);
}
}
//处理Listener注解;
// Process each annotation that corresponds to a listener.
for (Class<? extends Annotation> listener : LISTENERS) {
findAndParseListener(env, listener, targetClassMap, erasedTargetNames);
}
//设置父类Injector
for (Map.Entry<TypeElement, ViewInjector> entry : targetClassMap.entrySet()) {
String parentClassFqcn = findParentFqcn(entry.getKey(), erasedTargetNames);
if (parentClassFqcn != null) {
entry.getValue().setParentInjector(parentClassFqcn + SUFFIX);
}
}
return targetClassMap;
}
ButterKnife.parseInjectView
private void parseInjectView(Element element, Map<TypeElement, ViewInjector> targetClassMap,
Set<String> erasedTargetNames) {
boolean hasError = false;
//获取字段所在的类
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Verify that the target type extends from View.
TypeMirror elementType = element.asType();
if (elementType instanceof TypeVariable) {
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
//判断是否是View的子类,或者接口
if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
error(element, "@InjectView fields must extend from View or be an interface. (%s.%s)",
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
//判断字段和所在的类不能是Private的
// Verify common generated code restrictions.
hasError |= isInaccessibleViaGeneratedCode(InjectView.class, "fields", element);
//判断不是java和android内部类
hasError |= isBindingInWrongPackage(InjectView.class, element);
// Check for the other field annotation.
if (element.getAnnotation(InjectViews.class) != null) {
error(element, "Only one of @InjectView and @InjectViews is allowed. (%s.%s)",
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
if (hasError) {
return;
}
// Assemble information on the injection point.
int id = element.getAnnotation(InjectView.class).value();
//判断该id仅出现了一次
ViewInjector injector = targetClassMap.get(enclosingElement);
if (injector != null) {
ViewInjection viewInjection = injector.getViewInjection(id);
if (viewInjection != null) {
Iterator<ViewBinding> iterator = viewInjection.getViewBindings().iterator();
if (iterator.hasNext()) {
ViewBinding existingBinding = iterator.next();
error(element,
"Attempt to use @InjectView for an already injected ID %d on '%s'. (%s.%s)", id,
existingBinding.getName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
return;
}
}
}
//生成一个viewInjector和viewBinding
String name = element.getSimpleName().toString();
String type = elementType.toString();
boolean required = element.getAnnotation(Optional.class) == null;
ViewInjector viewInjector = getOrCreateTargetClass(targetClassMap, enclosingElement);
ViewBinding binding = new ViewBinding(name, type, required);
viewInjector.addView(id, binding);
// Add the type-erased version to the valid injection targets set.
erasedTargetNames.add(enclosingElement.toString());
}
ButterKnife.parseInjectViews
private void parseInjectViews(Element element, Map<TypeElement, ViewInjector> targetClassMap,
Set<String> erasedTargetNames) {
boolean hasError = false;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Verify that the type is a List or an array.
//判断注解的是List或者数组
TypeMirror elementType = element.asType();
String erasedType = doubleErasure(elementType);
TypeMirror viewType = null;
CollectionBinding.Kind kind = null;
if (elementType.getKind() == TypeKind.ARRAY) {
ArrayType arrayType = (ArrayType) elementType;
viewType = arrayType.getComponentType();
kind = CollectionBinding.Kind.ARRAY;
} else if (LIST_TYPE.equals(erasedType)) {
DeclaredType declaredType = (DeclaredType) elementType;
List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
if (typeArguments.size() != 1) {
error(element, "@InjectViews List must have a generic component. (%s.%s)",
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
} else {
viewType = typeArguments.get(0);
}
kind = CollectionBinding.Kind.LIST;
} else {
error(element, "@InjectViews must be a List or array. (%s.%s)",
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
//???????
if (viewType instanceof TypeVariable) {
TypeVariable typeVariable = (TypeVariable) viewType;
viewType = typeVariable.getUpperBound();
}
//判断是否是View的之类,或者接口
// Verify that the target type extends from View.
if (viewType != null && !isSubtypeOfType(viewType, VIEW_TYPE) && !isInterface(viewType)) {
error(element, "@InjectViews type must extend from View or be an interface. (%s.%s)",
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
//判断字段和所在的类不能是Private的
//判断不是java和android内部类
// Verify common generated code restrictions.
hasError |= isInaccessibleViaGeneratedCode(InjectViews.class, "fields", element);
hasError |= isBindingInWrongPackage(InjectViews.class, element);
if (hasError) {
return;
}
// Assemble information on the injection point.
String name = element.getSimpleName().toString();
int[] ids = element.getAnnotation(InjectViews.class).value();
if (ids.length == 0) {
error(element, "@InjectViews must specify at least one ID. (%s.%s)",
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
Integer duplicateId = findDuplicate(ids);
if (duplicateId != null) {
error(element, "@InjectViews annotation contains duplicate ID %d. (%s.%s)", duplicateId,
enclosingElement.getQualifiedName(), element.getSimpleName());
}
assert viewType != null; // Always false as hasError would have been true.
String type = viewType.toString();
boolean required = element.getAnnotation(Optional.class) == null;
//生成一个viewInjector和CollectionBinding
ViewInjector viewInjector = getOrCreateTargetClass(targetClassMap, enclosingElement);
CollectionBinding binding = new CollectionBinding(name, type, kind, required);
viewInjector.addCollection(ids, binding);
erasedTargetNames.add(enclosingElement.toString());
}
Butterknife.parseListenerAnnotation
private void parseListenerAnnotation(Class<? extends Annotation> annotationClass, Element element,
Map<TypeElement, ViewInjector> targetClassMap, Set<String> erasedTargetNames)
throws Exception {
// This should be guarded by the annotation's @Target but it's worth a check for safe casting.
if (!(element instanceof ExecutableElement) || element.getKind() != METHOD) {
throw new IllegalStateException(
String.format("@%s annotation must be on a method.", annotationClass.getSimpleName()));
}
ExecutableElement executableElement = (ExecutableElement) element;
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Assemble information on the injection point.
Annotation annotation = element.getAnnotation(annotationClass);
Method annotationValue = annotationClass.getDeclaredMethod("value");
if (annotationValue.getReturnType() != int[].class) {
throw new IllegalStateException(
String.format("@%s annotation value() type not int[].", annotationClass));
}
//获取该注解上的所有View的id
int[] ids = (int[]) annotationValue.invoke(annotation);
String name = executableElement.getSimpleName().toString();
boolean required = element.getAnnotation(Optional.class) == null;
// Verify that the method and its containing class are accessible via generated code.
boolean hasError = isInaccessibleViaGeneratedCode(annotationClass, "methods", element);
hasError |= isBindingInWrongPackage(annotationClass, element);
Integer duplicateId = findDuplicate(ids);
if (duplicateId != null) {
error(element, "@%s annotation for method contains duplicate ID %d. (%s.%s)",
annotationClass.getSimpleName(), duplicateId, enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}
ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class);
if (listener == null) {
throw new IllegalStateException(
String.format("No @%s defined on @%s.", ListenerClass.class.getSimpleName(),
annotationClass.getSimpleName()));
}
for (int id : ids) {
if (id == View.NO_ID) {
if (ids.length == 1) {
if (!required) {
error(element, "ID free injection must not be annotated with @Optional. (%s.%s)",
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
// Verify target type is valid for a binding without an id.
String targetType = listener.targetType();
if (!isSubtypeOfType(enclosingElement.asType(), targetType)
&& !isInterface(enclosingElement.asType())) {
error(element, "@%s annotation without an ID may only be used with an object of type "
+ "\"%s\" or an interface. (%s.%s)",
annotationClass.getSimpleName(), targetType,
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
} else {
error(element, "@%s annotation contains invalid ID %d. (%s.%s)",
annotationClass.getSimpleName(), id, enclosingElement.getQualifiedName(),
element.getSimpleName());
hasError = true;
}
}
}
ListenerMethod method;
ListenerMethod[] methods = listener.method();
if (methods.length > 1) {
throw new IllegalStateException(String.format("Multiple listener methods specified on @%s.",
annotationClass.getSimpleName()));
} else if (methods.length == 1) {
//ListenerClass中如果填充的是method,则保证只有一个
if (listener.callbacks() != ListenerClass.NONE.class) {
throw new IllegalStateException(
String.format("Both method() and callback() defined on @%s.",
annotationClass.getSimpleName()));
}
method = methods[0];
} else {
//ListenerClass中如果没有填充method,则取callback
Method annotationCallback = annotationClass.getDeclaredMethod("callback");
Enum<?> callback = (Enum<?>) annotationCallback.invoke(annotation);
Field callbackField = callback.getDeclaringClass().getField(callback.name());
method = callbackField.getAnnotation(ListenerMethod.class);
if (method == null) {
throw new IllegalStateException(
String.format("No @%s defined on @%s's %s.%s.", ListenerMethod.class.getSimpleName(),
annotationClass.getSimpleName(), callback.getDeclaringClass().getSimpleName(),
callback.name()));
}
}
//527-548行确保注解的方法参数与返回值,跟注解中ListenerClass保存的方法参数和返回值一致
// Verify that the method has equal to or less than the number of parameters as the listener.
List<? extends VariableElement> methodParameters = executableElement.getParameters();
if (methodParameters.size() > method.parameters().length) {
error(element, "@%s methods can have at most %s parameter(s). (%s.%s)",
annotationClass.getSimpleName(), method.parameters().length,
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
// Verify method return type matches the listener.
TypeMirror returnType = executableElement.getReturnType();
if (returnType instanceof TypeVariable) {
TypeVariable typeVariable = (TypeVariable) returnType;
returnType = typeVariable.getUpperBound();
}
if (!returnType.toString().equals(method.returnType())) {
error(element, "@%s methods must have a '%s' return type. (%s.%s)",
annotationClass.getSimpleName(), method.returnType(),
enclosingElement.getQualifiedName(), element.getSimpleName());
hasError = true;
}
if (hasError) {
return;
}
Parameter[] parameters = Parameter.NONE;
if (!methodParameters.isEmpty()) {
parameters = new Parameter[methodParameters.size()];
BitSet methodParameterUsed = new BitSet(methodParameters.size());
String[] parameterTypes = method.parameters();
for (int i = 0; i < methodParameters.size(); i++) {
VariableElement methodParameter = methodParameters.get(i);
TypeMirror methodParameterType = methodParameter.asType();
if (methodParameterType instanceof TypeVariable) {
TypeVariable typeVariable = (TypeVariable) methodParameterType;
methodParameterType = typeVariable.getUpperBound();
}
for (int j = 0; j < parameterTypes.length; j++) {
if (methodParameterUsed.get(j)) {
continue;
}
if (isSubtypeOfType(methodParameterType, parameterTypes[j])
|| isInterface(methodParameterType)) {
parameters[i] = new Parameter(j, methodParameterType.toString());
methodParameterUsed.set(j);
break;
}
}
if (parameters[i] == null) {
StringBuilder builder = new StringBuilder();
builder.append("Unable to match @")
.append(annotationClass.getSimpleName())
.append(" method arguments. (")
.append(enclosingElement.getQualifiedName())
.append('.')
.append(element.getSimpleName())
.append(')');
for (int j = 0; j < parameters.length; j++) {
Parameter parameter = parameters[j];
builder.append("\n\n Parameter #")
.append(j + 1)
.append(": ")
.append(methodParameters.get(j).asType().toString())
.append("\n ");
if (parameter == null) {
builder.append("did not match any listener parameters");
} else {
builder.append("matched listener parameter #")
.append(parameter.getListenerPosition() + 1)
.append(": ")
.append(parameter.getType());
}
}
builder.append("\n\nMethods may have up to ")
.append(method.parameters().length)
.append(" parameter(s):\n");
for (String parameterType : method.parameters()) {
builder.append("\n ").append(parameterType);
}
builder.append(
"\n\nThese may be listed in any order but will be searched for from top to bottom.");
error(executableElement, builder.toString());
return;
}
}
}
ListenerBinding binding = new ListenerBinding(name, Arrays.asList(parameters), required);
ViewInjector viewInjector = getOrCreateTargetClass(targetClassMap, enclosingElement);
for (int id : ids) {
if (!viewInjector.addListener(id, listener, method, binding)) {
error(element, "Multiple listener methods with return value specified for ID %d. (%s.%s)",
id, enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
}
// Add the type-erased version to the valid injection targets set.
erasedTargetNames.add(enclosingElement.toString());
}
ViewInjector.emitInject
生成inject方法
private void emitInject(StringBuilder builder) {
builder.append(" @Override ")
.append("public void inject(final Finder finder, final T target, Object source) {\n");
// Emit a call to the superclass injector, if any.
if (parentInjector != null) {
builder.append(" super.inject(finder, target, source);\n\n");
}
// Local variable in which all views will be temporarily stored.
builder.append(" View view;\n");
//生成字段注入代码和事件监听器注入代码
for (ViewInjection injection : viewIdMap.values()) {
emitViewInjection(builder, injection);
}
//生成数组注入代码
for (Map.Entry<CollectionBinding, int[]> entry : collectionBindings.entrySet()) {
emitCollectionBinding(builder, entry.getKey(), entry.getValue());
}
builder.append(" }\n");
}
ViewInjector.emitViewBindings
生成的代码类似于
view = finder.findRequiredView(source, 2131361935, "field 'title1;'");
target.title1 = (android.widget.TextView) view;
private void emitViewBindings(StringBuilder builder, ViewInjection injection) {
Collection<ViewBinding> viewBindings = injection.getViewBindings();
if (viewBindings.isEmpty()) {
return;
}
for (ViewBinding viewBinding : viewBindings) {
builder.append(" target.")
.append(viewBinding.getName())
.append(" = ");
if (viewBinding.requiresCast()) {
builder.append("finder.castView(view")
.append(", ")
.append(injection.getId())
.append(", \"");
emitHumanDescription(builder, viewBindings);
builder.append("\");\n");
} else {
builder.append("view;\n");
}
}
}
ButterKnife.emitListenerBindings
生成事件监听器的注入代码,生成的代码类似于
view = finder.findRequiredView(source, 2131361935, "field 'title1' and method 'sayHi'");
target.title1 = (android.widget.TextView) view;
((android.widget.TextView) view).addTextChangedListener(
new android.text.TextWatcher() {
@Override public void onTextChanged(
java.lang.CharSequence p0,
int p1,
int p2,
int p3
) {
target.sayHi();
}
@Override public void beforeTextChanged(
java.lang.CharSequence p0,
int p1,
int p2,
int p3
) {
}
@Override public void afterTextChanged(
android.text.Editable p0
) {
}
});