Related articles:
A brief analysis of the implementation of ButterKnife (I) — building a development framework
Analysis of the implementation of ButterKnife (2) — BindResource
This is the beginning of the most commonly used binding View annotation, which is a bit more complicated than resource binding annotation, but the general process is similar.
@Bind
Define an annotation to inject a View resource:
/ / @retention (retentionPolicy.class) @target (elementtype.field) public @interface Bind {@idres int[] value(); }Copy the code
Here with
@IdResLimits the value range of the attribute to
R.id.xxxAnd the property value is an array, because this annotation can bind not only a single View but also multiple views at the same time.
Let’s look at the annotation handler:
@AutoService(Processor.class) public class ButterKnifeProcessor extends AbstractProcessor { @Override public boolean Process (Set Annotations, RoundEnvironment roundEnv) {// // Bind for (Element Element: roundEnv.getElementsAnnotatedWith(Bind.class)) { if (VerifyHelper.verifyView(element, messager)) { ParseHelper.parseViewBind(element, targetClassMap, erasedTargetNames, elementUtils, typeUtils, messager); }} //... return true; } @Override public Set getSupportedAnnotationTypes() { Set annotations = new LinkedHashSet<>(); annotations.add(BindString.class.getCanonicalName()); annotations.add(BindColor.class.getCanonicalName()); annotations.add(Bind.class.getCanonicalName()); annotations.add(OnClick.class.getCanonicalName()); return annotations; }}Copy the code
Still want to see annotation detection and parsing, for this annotation detection did not do special processing, so I will not paste the code, mainly look at the parsing process.
public final class ParseHelper {
private static final String BINDING_CLASS_SUFFIX = "$ViewBinder";
private static final String LIST_TYPE = List.class.getCanonicalName();
private static final String ITERABLE_TYPE = "java.lang.Iterable";
static final String VIEW_TYPE = "android.view.View";
/**
* 解析 View 资源
*
* @param element 使用注解的元素
* @param targetClassMap 映射表
* @param elementUtils 元素工具类
*/
public static void parseViewBind(Element element, Map targetClassMap,
Set erasedTargetNames,
Elements elementUtils, Types typeUtils, Messager messager) {
TypeMirror elementType = element.asType();
// 判断是一个 View 还是列表
if (elementType.getKind() == TypeKind.ARRAY) {
_parseBindMany(element, targetClassMap, erasedTargetNames, elementUtils, messager);
} else if (LIST_TYPE.equals(_doubleErasure(elementType, typeUtils))) {
_parseBindMany(element, targetClassMap, erasedTargetNames, elementUtils, messager);
} else if (_isSubtypeOfType(elementType, ITERABLE_TYPE)) {
_error(messager, element, "@%s must be a List or array. (%s.%s)", Bind.class.getSimpleName(),
((TypeElement) element.getEnclosingElement()).getQualifiedName(),
element.getSimpleName());
} else {
_parseBindOne(element, targetClassMap, erasedTargetNames, elementUtils, messager);
}
}
/*************************************************************************/
/**
* 先通过 Types 工具对元素类型进行形式参数擦除,再通过字符比对进行二次擦除如果必要的话
* 例:java.util.List -> java.util.List
*
* @param elementType 元素类型
* @param typeUtils 类型工具
* @return 类型完全限定名
*/
private static String _doubleErasure(TypeMirror elementType, Types typeUtils) {
String name = typeUtils.erasure(elementType).toString();
int typeParamStart = name.indexOf('<'); if="" (typeParamStart="" !="-1)" {="" name="name.substring(0," typeParamStart);="" }="" return="" name;="" **="" *="" 判断该类型是否为="" otherType="" 的子类型="" @param="" typeMirror="" 元素类型="" 比对类型="" @return="" private="" static="" boolean="" _isSubtypeOfType(TypeMirror="" typeMirror,="" String="" otherType)="" (otherType.equals(typeMirror.toString()))="" true;="" (typeMirror.getKind()="" false;="" DeclaredType="" declaredType="(DeclaredType)" typeMirror;="" 判断泛型列表="" List typeArguments = declaredType.getTypeArguments();
if (typeArguments.size() > 0) {
StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
typeString.append('<'); for="" (int="" i="0;" <="" typeArguments.size();="" i++)="" {="" if="" (i=""> 0) {
typeString.append(',');
}
typeString.append('?');
}
typeString.append('>');
if (typeString.toString().equals(otherType)) {
return true;
}
}
// 判断是否为类或接口类型
Element element = declaredType.asElement();
if (!(element instanceof TypeElement)) {
return false;
}
// 判断父类
TypeElement typeElement = (TypeElement) element;
TypeMirror superType = typeElement.getSuperclass();
if (_isSubtypeOfType(superType, otherType)) {
return true;
}
// 判断接口
for (TypeMirror interfaceType : typeElement.getInterfaces()) {
if (_isSubtypeOfType(interfaceType, otherType)) {
return true;
}
}
return false;
}
/**
* 解析单个View绑定
*
* @param element
* @param targetClassMap
* @param erasedTargetNames
*/
private static void _parseBindOne(Element element, Map targetClassMap,
Set erasedTargetNames, Elements elementUtils, Messager messager) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
TypeMirror elementType = element.asType();
if (elementType.getKind() == TypeKind.TYPEVAR) {
// 处理泛型,取它的上边界,例: -> TextView
TypeVariable typeVariable = (TypeVariable) elementType;
elementType = typeVariable.getUpperBound();
}
// 不是View的子类型,且不是接口类型则报错
if (!_isSubtypeOfType(elementType, VIEW_TYPE) && !_isInterface(elementType)) {
_error(messager, element, "@%s fields must extend from View or be an interface. (%s.%s)",
Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
// 资源ID只能有一个
int[] ids = element.getAnnotation(Bind.class).value();
if (ids.length != 1) {
_error(messager, element, "@%s for a view must only specify one ID. Found: %s. (%s.%s)",
Bind.class.getSimpleName(), Arrays.toString(ids), enclosingElement.getQualifiedName(),
element.getSimpleName());
return;
}
// 获取或创建绑定类
int id = ids[0];
BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils);
FieldViewBinding existViewBinding = bindingClass.isExistViewBinding(id);
if (existViewBinding != null) {
// 存在重复使用的ID
_error(messager, element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
Bind.class.getSimpleName(), id, existViewBinding.getName(),
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
String name = element.getSimpleName().toString();
TypeName type = TypeName.get(elementType);
// 生成资源信息
FieldViewBinding binding = new FieldViewBinding(name, type, true);
// 给BindingClass添加资源信息
bindingClass.addViewBinding(id, binding);
erasedTargetNames.add(enclosingElement);
}
/**
* 解析 View 列表
* @param element
* @param targetClassMap
* @param erasedTargetNames
* @param elementUtils
* @param messager
*/
private static void _parseBindMany(Element element, Map targetClassMap,
Set erasedTargetNames,
Elements elementUtils, Messager messager) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
TypeMirror elementType = element.asType();
TypeMirror viewType = null;
FieldCollectionViewBinding.Kind kind;
if (elementType.getKind() == TypeKind.ARRAY) {
ArrayType arrayType = (ArrayType) elementType;
// 获取数组里面包含的View类型
viewType = arrayType.getComponentType();
kind = FieldCollectionViewBinding.Kind.ARRAY;
} else {
// 默认不是数组就只能是 List
DeclaredType declaredType = (DeclaredType) elementType;
List typeArguments = declaredType.getTypeArguments();
if (typeArguments.size() != 1) {
// 列表的参数只能有一个
_error(messager, element, "@%s List must have a generic component. (%s.%s)",
Bind.class.getSimpleName(), enclosingElement.getQualifiedName(),
element.getSimpleName());
return;
} else {
// 获取 View 类型
viewType = typeArguments.get(0);
}
kind = FieldCollectionViewBinding.Kind.LIST;
}
// 处理泛型
if (viewType != null && viewType.getKind() == TypeKind.TYPEVAR) {
TypeVariable typeVariable = (TypeVariable) viewType;
viewType = typeVariable.getUpperBound();
}
// 不是View的子类型,且不是接口类型则报错
if (viewType != null && !_isSubtypeOfType(viewType, VIEW_TYPE) && !_isInterface(viewType)) {
_error(messager, element, "@%s List or array type must extend from View or be an interface. (%s.%s)",
Bind.class.getSimpleName(), enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
assert viewType != null; // Always false as hasError would have been true.
int[] ids = element.getAnnotation(Bind.class).value();
if (ids.length == 0) {
_error(messager, element, "@%s must specify at least one ID. (%s.%s)", Bind.class.getSimpleName(),
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
// 检测是否有重复 ID
Integer duplicateId = _findDuplicate(ids);
if (duplicateId != null) {
_error(messager, element, "@%s annotation contains duplicate ID %d. (%s.%s)", Bind.class.getSimpleName(),
duplicateId, enclosingElement.getQualifiedName(), element.getSimpleName());
}
String name = element.getSimpleName().toString();
TypeName typeName = TypeName.get(viewType); // 这边取得是View的类型不是列表类型
BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils);
FieldCollectionViewBinding binding = new FieldCollectionViewBinding(name, typeName, kind);
bindingClass.addFieldCollection(ids, binding);
erasedTargetNames.add(enclosingElement);
}
/**
* 判断是否为接口
*
* @param typeMirror
* @return
*/
private static boolean _isInterface(TypeMirror typeMirror) {
return typeMirror instanceof DeclaredType
&& ((DeclaredType) typeMirror).asElement().getKind() == ElementKind.INTERFACE;
}
/**
* 检测重复ID
*/
private static Integer _findDuplicate(int[] array) {
Set seenElements = new LinkedHashSet<>();
for (int element : array) {
if (!seenElements.add(element)) {
return element;
}
}
return null;
}
}Copy the code
_parseBindOne() and _parseBindMany() correspond to the binding of a single View and the binding of a complex View, respectively. As for how to choose which method is handled by determining the type of the element, the _parseBindMany() method is called if the element type is ARRAY or LIST_TYPE, and _parseBindOne() is called otherwise. (ARRAY) (DECLARED) (LIST) (DECLARED) (ARRAY) (ARRAY) (DECLARED) (LIST) (DECLARED) (ARRAY) (ARRAY) (DECLARED) In the _doubleErasure() method we do a formal type parameter erasure to get the fully qualified name of the class type and then do a string comparison.
The following describes the binding process for a single View_parseBindOne()If the element is generic, take its upper boundary. For example:
public class MyTestView {
@Bind(R.id.my_view)
T mView;
}Copy the code
This is the time to do the generic processing and the obtained element type should be DECLARED or, more accurately, TextView. VIEW_TYPE = “android.view. view “subtype or interface type. The subtype of View should be easy to understand, because we are dealing with View injection, so it should be a subclass of View, so why interface also can be? You should know that Java is interface oriented, you can convert an object display to the type of the interface it implements, and you can call the corresponding methods of the interface. If you convert to an unimplemented interface type, you’ll get an error when you convert, as you’ll see later.
This is mainly about subtype judgment_isSubtypeOfType(), the type must be a class or interfaceTypeKind.DECLAREDIf the current element type does not match, then its parent type and interface are determined. If neither of them match, then the type does not match.
And once we’ve done that, we can get the value of the annotation property, because now we’re at
A single View is injected, so there can only be one ID value. Once we get the ID value, we can generate the FieldViewBinding letterAn ID can only correspond to one View. Look at the below
FieldViewBindingDefinition:
/** * final class FieldViewBinding implements ViewBinding {private final String name; private final TypeName type; private final boolean required; FieldViewBinding(String name, TypeName type, boolean required) { this.name = name; this.type = type; this.required = required; } public String getName() { return name; } public TypeName getType() { return type; } @Override public String getDescription() { return "field '" + name + "'"; } public boolean isRequired() { return required; } public boolean requiresCast() { return ! VIEW_TYPE.equals(type.toString()); }}Copy the code
Name is the name of the annotated field, which is fine, and TypeName is a type provided by JavaPoet that actually corresponds to the class type of the field, which can be directly converted to the class type when Java files are generated. Required indicates whether the annotation field must be assigned a value, that is, whether it can be null. In fact, the @nullable annotation does not exist. The code ends with a requiresCast() method to determine if a conversion is required. FindViewById () returns views, which we can convert to a more accurate type such as TextView. GetDescription () is just a description to output in case of an error.
Before explaining code generation, let’s go back to the Finder class we defined earlier. There are a few things we didn’t say about the findViewById() method.
Public enum Finder {//... / * * * the findViewById, * @param source * @param ID Resource ID * @param WHO description * @param conversion type * @return */ public T findRequiredView(Object source, int id, String who) { T view = findOptionalView(source, id, who); if (view == null) { 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."); } return view; } /** * findViewById, */ public findOptionalView(Object source, int id, String who) {View View = findView(source, id); return castView(view, id, who); + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String who) { try { return (T) view; } catch (ClassCastException e) { if (who == null) { throw new AssertionError(); } 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 T castParam(Object value, String from, int fromPosition, String to, int toPosition) { try { return (T) value; } catch (ClassCastException e) { throw new IllegalStateException("Parameter #" + (fromPosition + 1) + " of method '" + from + "' was of the wrong type for parameter #" + (toPosition + 1) + " of method '" + to + "'. See cause for more info.", e); }}}Copy the code
These methods should be understood by the following comments, so let’s look directly at the handling of BindingClass.
public final class BindingClass { private static final ClassName FINDER = ClassName.get("com.dl7.butterknifelib", "Finder"); private static final ClassName VIEW_BINDER = ClassName.get("com.dl7.butterknifelib", "ViewBinder"); private static final ClassName VIEW = ClassName.get("android.view", "View"); private final List viewBindings = new ArrayList<>(); private final Map viewIdMap = new LinkedHashMap<>(); /** @return MethodSpec */ private MethodSpec _createBindMethod() { if (_hasViewBinding()) { // View for (Map.Entry entry : viewIdMap.entrySet()) { int id = entry.getKey(); FieldViewBinding viewBinding = entry.getValue(); result.addStatement("target.$L = finder.findRequiredView(source, $L, $S)", viewBinding.getName(), id, viewBinding.getDescription()); } } return result.build(); } /** * addViewBinding ** @param binding */ public void addViewBinding(int id) FieldViewBinding binding) { FieldViewBinding fieldViewBinding = viewIdMap.get(id); if (fieldViewBinding == null) { viewBindings.add(binding); viewIdMap.put(id, binding); } } private boolean _hasViewBinding() { return ! (viewBindings.isEmpty() && collectionBindings.isEmpty()); } public FieldViewBinding isExistViewBinding(int ID) {return viewIdMap.get(id); }}Copy the code
Take a look at the key code, which uses viewIdMap to store the set ID and FieldViewBinding key-value pairs to determine if the setting is repeated. When generating code, the finder.findrequiredView () method is called to set up the View, which is null judged.
Now let’s look at the injection of multiple views.
Complex View injection
So let’s go back to solving complex views
_parseBindMany()Methods:
1, here is the first to determine what is an ARRAY or a LIST, and take the LIST contains parameter viewType type, FieldCollectionViewBinding here. The Kind is an enumeration values, such as the mentions;
2,Such asfruitviewType If it’s a generic parameterGenerics and determines if it is a child of ViewType or interface;
3. Get a list of ID values for annotations and determine whether they contain duplicate ids;
4,intoFieldCollectionViewBinding and added to the BindingClass;
Look atFieldCollectionViewBindingDefinition:
/ * * * * / final list View information class FieldCollectionViewBinding implements ViewBinding {enum Kind {ARRAY, LIST } private final String name; private final TypeName type; private final Kind kind; FieldCollectionViewBinding(String name, TypeName type, Kind kind) { this.name = name; this.type = type; this.kind = kind; } public String getName() { return name; } public TypeName getType() { return type; } public Kind getKind() { return kind; } @Override public String getDescription() { return "field '" + name + "'"; }}Copy the code
Like the FieldViewBinding, there is a Kind enumeration that specifies whether it is an ARRAY or a LIST. Before code generation, we need to introduce two classes in the Butterknifelib library: Utils and ImmutableList. First take a look at Utils:
@SuppressWarnings("deprecation") //
public final class Utils {
private Utils() {
throw new AssertionError("No instances.");
}
@SafeVarargs
public static T[] arrayOf(T... views) {
return filterNull(views);
}
@SafeVarargs
public static List listOf(T... views) {
return new ImmutableList<>(filterNull(views));
}
private static T[] filterNull(T[] views) {
int end = 0;
int length = views.length;
for (int i = 0; i < length; i++) {
T view = views[i];
if (view != null) {
views[end++] = view;
}
}
if (end == length) {
return views;
}
//noinspection unchecked
T[] newViews = (T[]) Array.newInstance(views.getClass().getComponentType(), end);
System.arraycopy(views, 0, newViews, 0, end);
return newViews;
}
}Copy the code
It provides two interfaces to assist in View list injection, namely arrayOf(T… Views) and listOf (T… Views), ARRAY or LIST. This will automatically remove null views from the list. To help with this, we define an ImmutableList class, which is a lightweight ImmutableList class, defined as follows:
final class ImmutableList extends AbstractList implements RandomAccess { private final T[] views; ImmutableList(T[] views) { this.views = views; } @Override public T get(int index) { return views[index]; } @Override public int size() { return views.length; } @Override public boolean contains(Object o) { for (T view : views) { if (view == o) { return true; } } return false; }}Copy the code
And one last thing
BindingClass Processing:
public final class BindingClass { private static final ClassName FINDER = ClassName.get("com.dl7.butterknifelib", "Finder"); private static final ClassName VIEW_BINDER = ClassName.get("com.dl7.butterknifelib", "ViewBinder"); private static final ClassName UTILS = ClassName.get("com.dl7.butterknifelib", "Utils"); private final Map collectionBindings = new LinkedHashMap<>(); /** @return MethodSpec */ private MethodSpec _createBindMethod() { // ViewList for (Map.Entry entry : collectionBindings.entrySet()) { String ofName; / / UTILS method name FieldCollectionViewBinding binding = entry. The getKey (); int[] ids = entry.getValue(); / / get the method name if (binding getKind () = = FieldCollectionViewBinding. Kind. The ARRAY) {ofName = "arrayOf"; } else if (binding.getKind() == FieldCollectionViewBinding.Kind.LIST) { ofName = "listOf"; } else { throw new IllegalStateException("Unknown kind: " + binding.getKind()); } // Fill in the plural View code as an argument to Utils method codeblock. Builder Builder = codeblock. Builder (); for (int i = 0; i < ids.length; i++) { if (i > 0) { builder.add(", "); } builder.add("\nfinder.<$T>findRequiredView(source, $L, $S)", binding.getType(), ids[i], binding.getDescription()); Result.addstatement ("target.$L = $t.$L ($L)", binding.getName(), Utils, ofName, builder.build()); } / / slightly... return result.build(); } /** * add ViewBinding ** @param binding resource */ void addFieldCollection(int[] ids, FieldCollectionViewBinding binding) { collectionBindings.put(binding, ids); }}Copy the code
It’s going to go first
Break isARRAY orLISTThen call the corresponding Utils method to inject the View, codeblock. Builderis
JavaPoetCode block writing method, detailed use to see the official example.
We use the following annotations in our code:
public class MainActivity extends AppCompatActivity { @Bind(R.id.tv_desc) TextView textView; @Bind(R.id.fl_view) FrameLayout view; @Bind({R.id.btn_one, R.id.btn_two, R.id.btn_three}) List mButtons; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); }}Copy the code
The generated code looks like this:
public class MainActivity$ViewBinder implements ViewBinder { @Override @SuppressWarnings("ResourceType") public void bind(final Finder finder, final T target, Object source) { target.textView = finder.findRequiredView(source, 2131492948, "field 'textView'"); target.view = finder.findRequiredView(source, 2131492944, "field 'view'"); target.mButtons = Utils.listOf( finder.findRequiredView(source, 2131492945, "field 'mButtons'"), finder.findRequiredView(source, 2131492946, "field 'mButtons'"), finder.findRequiredView(source, 2131492947, "field 'mButtons'")); }}Copy the code
Try to understand how the entire code generation process works by referring to the process described above to better understand how it works.
Source: ButterKnifeStudy