概述
想要浏览ButterKnife的注解处理器实现原理,前提需要掌握Java编程元素接口的相关知识,所以下面的代码遨游默认老铁们都是掌握了这一部分知识。
注解处理器源码解析
话不多说,开始遨游。
支持的注解类型\集合
private Set<Class<? extends Annotation>> getSupportedAnnotations() { Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>(); annotations.add(BindAnim.class); annotations.add(BindArray.class); annotations.add(BindBitmap.class); annotations.add(BindBool.class); annotations.add(BindColor.class); annotations.add(BindDimen.class); annotations.add(BindDrawable.class); annotations.add(BindFloat.class); annotations.add(BindFont.class); annotations.add(BindInt.class); annotations.add(BindString.class); annotations.add(BindView.class); annotations.add(BindViews.class); annotations.addAll(LISTENERS); return annotations; }
LISTENERS,监听回调类型的注解集合
private static final List<Class<? extends Annotation>> LISTENERS = Arrays.asList(// OnCheckedChanged.class, // OnClick.class, // OnEditorAction.class, // OnFocusChange.class, // OnItemClick.class, // OnItemLongClick.class, // OnItemSelected.class, // OnLongClick.class, // OnPageChange.class, // OnTextChanged.class, // OnTouch.class // );
process(…)方法,解析被支持的注解标记的元素,然后生成Java文件
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) { Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env); for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) { TypeElement typeElement = entry.getKey(); BindingSet binding = entry.getValue(); JavaFile javaFile = binding.brewJava(sdk, debuggable); try { javaFile.writeTo(filer); } catch (IOException e) { error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage()); } } return false; }
其中重要的一个方法
findAndParseTargets(env)
,顾名思义,查找并解析注解标记的元素private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) { Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>(); //paichu Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); scanForRClasses(env); // Process each @BindAnim element. for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) { if (!SuperficialValidation.validateElement(element)) continue; try { parseResourceAnimation(element, builderMap, erasedTargetNames); } catch (Exception e) { logParsingError(element, BindAnim.class, e); } } .... \\省略代码,代码套路基本一致,略微存在一些差异 }
我们先以被@BindAnim注解标记的元素解析为例,看看ButterKnife的代码是怎么样的一个思路,上面的代码可知,首先是获取到被@BindAnim标记的元素的集合,然后遍历集合中所有的元素,其主要解析的方法是
parseResourceAnimation(...)
,代码如下:private void parseResourceAnimation(Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) { boolean hasError = false; TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Verify that the target type is Animation. if (!ANIMATION_TYPE.equals(element.asType().toString())) { error(element, "@%s field type must be 'Animation'. (%s.%s)", BindAnim.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName()); hasError = true; } // Verify common generated code restrictions. hasError |= isInaccessibleViaGeneratedCode(BindAnim.class, "fields", element); hasError |= isBindingInWrongPackage(BindAnim.class, element); if (hasError) { return; } // Assemble information on the field. String name = element.getSimpleName().toString(); int id = element.getAnnotation(BindAnim.class).value(); QualifiedId qualifiedId = elementToQualifiedId(element, id); BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement); builder.addResource(new FieldAnimationBinding(getId(qualifiedId), name)); erasedTargetNames.add(enclosingElement); }
总览上面的代码,可以清晰的了解其思路,分为2个步骤:校验和信息组装收集,首先来看校验:
- 首先校验被标记的元素的类型是否是Animation。
- 再校验被标记的元素的修饰符是否是private和static,校验封装此元素的最里层元素enclosingElement的类别Kind是否为CLASS类别,这一个步骤的校验主要是为了便于后面生成的Java文件中给元素赋值的时候,保证其元素是可访问的。(这一步校验我把它称为
适用校验
)
如果上述校验通过,则开始组装收集信息,为生成Java文件做准备,我们分析其组装信息的代码,主要收集了被标记元素的simpleName、@BindAnim注解的value值(id),然后将封装此元素的最里层元素enclosingElement作为key值,一个BuilderSet.Builder对象最为value,存储进入Map集合。
到这里解析就基本告一段落了,但是还有一个BuilderSet.Builder还没弄清楚,我们在来看看是什么东西。
final class BindingSet { //... private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>(); private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings = ImmutableList.builder(); private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder(); private BindingSet(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal, boolean isView, boolean isActivity, boolean isDialog, ImmutableList<ViewBinding> viewBindings, ImmutableList<FieldCollectionViewBinding> collectionBindings, ImmutableList<ResourceBinding> resourceBindings, BindingSet parentBinding) { this.isFinal = isFinal; this.targetTypeName = targetTypeName; this.bindingClassName = bindingClassName; this.isView = isView; this.isActivity = isActivity; this.isDialog = isDialog; this.viewBindings = viewBindings; this.collectionBindings = collectionBindings; this.resourceBindings = resourceBindings; this.parentBinding = parentBinding; } static final class Builder { private final TypeName targetTypeName; private final ClassName bindingClassName; private final boolean isFinal; private final boolean isView; private final boolean isActivity; private final boolean isDialog; ... private final Map<Id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>(); private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings = ImmutableList.builder(); private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder(); private Builder(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal, boolean isView, boolean isActivity, boolean isDialog) { this.targetTypeName = targetTypeName; this.bindingClassName = bindingClassName; this.isFinal = isFinal; this.isView = isView; this.isActivity = isActivity; this.isDialog = isDialog; } //... } }
整理了一下代码,然后我们可以清晰的看到,这是一个建造者模式的类,其中的成员变量用来保存需要组装的信息,因为这里省略了很多代码,但是其不仅仅是保存组装信息,还有涉及到javapoet的一些东西,由于篇幅以及保证思路清晰,在此不一一列举,留待下一篇再分析。
最后,是一个封装了Animation id和需要绑定的变量名称name的FieldAnimationBinding对象,然后封装到Map集合中对应的enclosingElement为key的Builder对象中去。findAndParseListener
除了解析一般的像是被@BindView、@BindAnim等这类比较简单普通的注解标记的元素外,ButterKnife还有像@Onclick这类比较稍微复杂的注解,这一类注解主要是用来实现监听回调用途,我们再来看看代码是如何写的,思路又是什么样的呢?
private void findAndParseListener(RoundEnvironment env, Class<? extends Annotation> annotationClass, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> erasedTargetNames) { for (Element element : env.getElementsAnnotatedWith(annotationClass)) { if (!SuperficialValidation.validateElement(element)) continue; try { parseListenerAnnotation(annotationClass, element, builderMap, erasedTargetNames); } catch (Exception e) { StringWriter stackTrace = new StringWriter(); e.printStackTrace(new PrintWriter(stackTrace)); error(element, "Unable to generate view binder for @%s.\n\n%s", annotationClass.getSimpleName(), stackTrace.toString()); } } }
首先一开始和之前分析过的一样,利用env对象获取被注解标记的Elements集合,然后遍历集合,解析每一个被标记的元素。和之前的不一样在于,其主要解析方法是parseListenerAnnotation(…),话不多说,来看代码。
private void parseListenerAnnotation(Class<? extends Annotation> annotationClass, Element element, Map<TypeElement, BindingSet.Builder> builderMap, Set<TypeElement> 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 method. 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)); } int[] ids = (int[]) annotationValue.invoke(annotation); String name = executableElement.getSimpleName().toString(); boolean required = isListenerRequired(executableElement); // 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())); } //...省略代码,对id数组进行正确性校验 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) { if (listener.callbacks() != ListenerClass.NONE.class) { throw new IllegalStateException( String.format("Both method() and callback() defined on @%s.", annotationClass.getSimpleName())); } method = methods[0]; } else { 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())); } } // 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]) && isSubtypeOfType(methodParameterType, VIEW_TYPE)) || isTypeEqual(methodParameterType, parameterTypes[j]) || isInterface(methodParameterType)) { parameters[i] = new Parameter(j, TypeName.get(methodParameterType)); 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; } } } MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required); BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement); for (int id : ids) { QualifiedId qualifiedId = elementToQualifiedId(element, id); if (!builder.addMethod(getId(qualifiedId), 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 binding targets set. erasedTargetNames.add(enclosingElement); }
代码较多,我们一步一步来看,过程中可能会偏离主线,然后再回到主线,所以老铁们要紧跟思路。
首先校验集合中的每一个Element,判断元素是否是一个方法元素(ExecutableElement)。
如果是满足上面的判断,则获取方法元素Element上指定注解类型Annotation对象,进而获得注解上定义的一些信息,这里我们以@Onclick注解类型详细来了解其编码思路。
我们找到@Onclick的类定义:@Target(METHOD) @Retention(CLASS) @ListenerClass( targetType = "android.view.View", setter = "setOnClickListener", type = "butterknife.internal.DebouncingOnClickListener", method = @ListenerMethod( name = "doClick", parameters = "android.view.View" ) ) public @interface OnClick { /** View IDs to which the method will be bound. */ @IdRes int[] value() default { View.NO_ID }; }
代码很清晰,@Onclick注解是一个编译时注解,适用于Method,有一个方法value(),返回类型为int[],默认值为只包含一个数值-1的数组,然后还被@ListenerClass注解标记,那么,接下来再看看@ListenerClass注解是如何定义的。
@Retention(RUNTIME) @Target(ANNOTATION_TYPE) public @interface ListenerClass { String targetType(); /** Name of the setter method on the {@linkplain #targetType() target type} for the listener. */ String setter(); /** * Name of the method on the {@linkplain #targetType() target type} to remove the listener. If * empty {@link #setter()} will be used by default. */ String remover() default ""; /** Fully-qualified class name of the listener type. */ String type(); /** Enum which declares the listener callback methods. Mutually exclusive to {@link #method()}. */ Class<? extends Enum<?>> callbacks() default NONE.class; /** * Method data for single-method listener callbacks. Mutually exclusive with {@link #callbacks()} * and an error to specify more than one value. */ ListenerMethod[] method() default { }; /** Default value for {@link #callbacks()}. */ enum NONE { } }
可以很清晰的看到,@ListenerClass是一个元注解,也是一个运行时注解,其有几个方法,这里我们先不介绍,后面在详细分析。
回到parseListenerAnnotation,我们上面分析到拿到指定注解类型的对象。
那么接下来是反射对象,得到注解的value()方的返回值类型,判断是否是int[] 类型,如果符合,则继续反射调用invoke得到方法的具体返回值,注意,这个返回值就是R类中的id值。
然后还需要对被标记的元素对象进行适用校验
,这就不在赘述,之前已经分析过。得到了id数组,对数组中的id值做重叠校验。
再接下来,是对指定注解上的@ListenerClass注解进行解析,这里分析的是@Onclick注解上的@ListenerClass。为了思路清晰,我们这里在贴一下部分代码:
ListenerClass listener = annotationClass.getAnnotation(ListenerClass.class); 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) { if (listener.callbacks() != ListenerClass.NONE.class) { throw new IllegalStateException( String.format("Both method() and callback() defined on @%s.", annotationClass.getSimpleName())); } method = methods[0]; } else { 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())); } }
首先,获取@ListenerClass注解对象中method方法的返回值,method方法的返回值类型是@ListenerMethod数组。先来看看@ListenerMethod注解的定义:
@Retention(RUNTIME) @Target(FIELD) public @interface ListenerMethod { /** Name of the listener method for which this annotation applies. */ String name(); /** List of method parameters. If the type is not a primitive it must be fully-qualified. */ String[] parameters() default { }; /** Primitive or fully-qualified return type of the listener method. May also be {@code void}. */ String returnType() default "void"; /** If {@link #returnType()} is not {@code void} this value is returned when no binding exists. */ String defaultReturn() default "null"; }
@ListenerMethod注解是一个运行时注解,其适用于变量Field,包含方法name(回调方法名称),parameters(回调方法参数类型),returnType(回调方法返回值类型),比如这里@Onclick注解的回调方法是OnClickListener接口中的doOnclick方法。
再回到@ListenenrClass的method方法,我们得到的是一个@ListenerMethod[ ] 数组,然后对这个数组进行校验。首先,这个数组必须满足length<=1,当length==1的时候,就是我们这里分析的例如@Onclick类型的注解,我们只需要拿到数组中唯一的一个值,也就是一个@ListernerMethod对象。而如果length<1,则是需要另行判断注解中的callback方法的返回值,callback的返回值是一个Enum枚举类型对象,callback方法适用于具有多个回调方法的接口,譬如OnTextChangListener接口,其对应的注解类型是@OnTextChanged注解,而@OnTextChanged注解中定义了一个Callback类型的枚举,其有3个实例,分别被@ListenerMethod标记,其中callback方法返回值默认为其中的Callback.TEXT_CHANGED实例。和length==1的时候一样,我们同样也需要拿到一个封装了回调方法信息的@ListenerMethod对象,具体就是这段代码:
Annotation annotation = element.getAnnotation(annotationClass); Method annotationCallback = annotationClass.getDeclaredMethod("callback"); //反射Annotation注解对象callback方法,拿到返回值,是一个枚举常量 Enum<?> callback = (Enum<?>) annotationCallback.invoke(annotation); //获取枚举常量的枚举类型的Class对象 //获取指定枚举名称name的实例(枚举成员变量)的反射-Field Field callbackField = callback.getDeclaringClass().getField(callback.name()); //获取枚举常量上的@ListenerMethod注解信息对象 ListenerMethod method = callbackField.getAnnotation(ListenerMethod.class);
到这里,拿到了id,拿到了封装了回调方法的@ListenerMethod对象,接下来,还需要校验被注解标记的方法元素executableElement的方法参数、返回值类型是否和@ListenerMethod中定义的一致。如下代码:
// 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; }
然后还需要对executableElement的参数,按照@ListenerMethod中parameters方法定义的顺序,另行封装为一个Parameter[]数组,而Parameter封装了接口回调方法参数定义的顺序position和方法元素的参数类型TypeName,我们看代码:
List<? extends VariableElement> methodParameters = executableElement.getParameters(); //... 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; //泛型类型提升(如<K extends A>,则返回A类型) methodParameterType = typeVariable.getUpperBound(); } for (int j = 0; j < parameterTypes.length; j++) { if (methodParameterUsed.get(j)) { continue; } if ((isSubtypeOfType(methodParameterType, parameterTypes[j]) && isSubtypeOfType(methodParameterType, VIEW_TYPE)) || isTypeEqual(methodParameterType, parameterTypes[j]) || isInterface(methodParameterType)) { //关键代码,校验类型,然后Parameter封装 parameters[i] = new Parameter(j, TypeName.get(methodParameterType)); methodParameterUsed.set(j); break; } } //... }
最后,还需要将方法元素和回调方法的相关信息做最后一次封装,看代码。
MethodViewBinding binding = new MethodViewBinding(name, Arrays.asList(parameters), required); BindingSet.Builder builder = getOrCreateBindingBuilder(builderMap, enclosingElement); for (int id : ids) { QualifiedId qualifiedId = elementToQualifiedId(element, id); if (!builder.addMethod(getId(qualifiedId), listener, method, binding)) { error(element, "Multiple listener methods with return value specified for ID %d. (%s.%s)", id, enclosingElement.getQualifiedName(), element.getSimpleName()); return; } }
看这代码,是不是觉得和之前的FieldAnimationBinding有点类似呢?命名方式都是XxxBinding,因为每一种不同注解绑定的元素以及元素的类型都不一样,需要封装的信息自然也就不一样,所以篇幅原因就不一一解析了,我们只需要大概知道是为了之后的javapoet生成java文件做准备就可以了,有兴趣的可以自行遨游一波。
到这里我们的遨游就先差不多了,下一阶段,我们开始在ButterKnife中遨游:javapoet库如何利用注解解析器封装的信息生成出我们需要的Java文件以在编译时期就可以绑定指定的Target,我们下一篇再见。