浅析ButterKnife的实现 (三) —— BindView

    xiaoxiao2025-07-26  8

    相关文章:

    浅析ButterKnife的实现 (一) —— 搭建开发框架

    浅析ButterKnife的实现 (二) —— BindResource

    这里开始讲解最常用的绑定View的注解了,这个会比资源绑定注解复杂一点,不过大体流程都是相似的。

    @Bind

    定义个用来注入View资源的注解:

    /** * View绑定 */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface Bind { @IdRes int[] value(); }这里用  @IdRes 限定了属性的取值范围为  R.id.xxx,并且属性值是个数组,因为这个注解不仅可以绑定单个View还可以同时绑定多个View。

    来看下注解处理器:

    @AutoService(Processor.class) public class ButterKnifeProcessor extends AbstractProcessor { @Override public boolean process(Set<? extends TypeElement> 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<String> getSupportedAnnotationTypes() { Set<String> 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; } }

    还是要看注解的检测和解析,对于该注解的检测并没有做特别的处理,所以代码我就不贴了,主要来看解析过程。

    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<TypeElement, BindingClass> targetClassMap, Set<TypeElement> 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.lang.String> -> 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 元素类型 * @param otherType 比对类型 * @return */ private static boolean _isSubtypeOfType(TypeMirror typeMirror, String otherType) { if (otherType.equals(typeMirror.toString())) { return true; } if (typeMirror.getKind() != TypeKind.DECLARED) { return false; } DeclaredType declaredType = (DeclaredType) typeMirror; // 判断泛型列表 List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments(); if (typeArguments.size() > 0) { StringBuilder typeString = new StringBuilder(declaredType.asElement().toString()); typeString.append('<'); for (int i = 0; i < 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<TypeElement, BindingClass> targetClassMap, Set<TypeElement> erasedTargetNames, Elements elementUtils, Messager messager) { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); TypeMirror elementType = element.asType(); if (elementType.getKind() == TypeKind.TYPEVAR) { // 处理泛型,取它的上边界,例:<T extends TextView> -> 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<TypeElement, BindingClass> targetClassMap, Set<TypeElement> 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<? extends TypeMirror> 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<Integer> seenElements = new LinkedHashSet<>(); for (int element : array) { if (!seenElements.add(element)) { return element; } } return null; } }

    东西还是比较多的,不过主要关注 _parseBindOne()_parseBindMany() 两个方法,分别对应单个View的绑定和复数View的绑定处理。对于如何选择哪个方法是通过判断元素的类型来处理,如果元素类型为 ARRAY或 LIST_TYPE则调用 _parseBindMany() 方法,否则调用 _parseBindOne()。对于元素类型的判断,ARRAY比较简单可以直接判断,但是LIST就麻烦点,因为 TypeKind 里面并没有包含LIST类型,更确切地说这个应该算 DECLARED 类型,在判断上我们要做点处理。在 _doubleErasure() 方法中我们进行了形式类型参数的擦除获取到类类型的完全限定名,然后进行字符串比较就行了。

    下面先来说明单个View的绑定处理过程_parseBindOne(),首先进行元素泛型处理,如果该元素是个泛型的话就取它的上边界,举个例子:

    public class MyTestView<T extends TextView> { @Bind(R.id.my_view) T mView; }

    这个时候就要进行泛型处理,而获取到的元素类型应该为 DECLARED 类型,更确切是 TextView。泛型判断完就判断该类型是不是 VIEW_TYPE = "android.view.View" 的子类型或是个接口类型,不是就报错。对于View的子类型应该好理解,因为我们现在是在处理View的注入,所以应该为View的子类,那为什么接口也可以呢?大家应该知道Java是面向接口编程的,我们可以把对象显示转换为它实现的接口类型,并可以调用接口相应的方法,如果转换为未实现的接口类型在转换的时候会报错,这个等下后面会看到。

    这里主要来讲子类型的判断 _isSubtypeOfType(),这里用来判断的类型必须为类或接口 TypeKind.DECLARED,参数列表也进行判断,如果当前元素类型不符合则再去判断它的父类型和接口,如果都不是则表明该类型确实不符合。

    这些判断完就可以获取注解属性值了,因为我们现在在处 理单个View的注入,所以只能有一个ID值。获取到ID值后我们就可以生成 FieldViewBinding 信息了,前面还要对这个ID是否已存在进行判断,一个ID只能对应一个View视图。下面看下  FieldViewBinding的定义:

    /** * View绑定信息 */ 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()); } }

    其中 name 就是使用注解的字段名称,这个没什么问题,然后是 TypeName这是JavaPoet提供的一个类型,其实它对应的就是字段对应的类类型,这个在生成Java文件的时候可以直接转换为对应类类型。required的话是在表明注解字段是否必须赋值,也就是是否可以为null,实际是判断是否存在@Nullable 注解,为了方便我这里强制定义字段必须不能为null,有兴趣可以看下源码对这个参数的处理。代码最后有个 requiresCast() 方法用来判断是否需要进行类型转换,通过 findViewById() 返回的都是View类型,我们可以转换为更准确的类型如TextViewgetDescription() 只是用来出错时候输出的描述信息,这里简单了解下即可。

    在对代码生成进行说明前,先回过头来看下我们之前定义的 Finder 这个类,里面有几个关于 findViewById()方法没给出。

    public enum Finder { // 略... /** * findViewById,会进行 null 判断 * @param source * @param id 资源ID * @param who 描述信息 * @param <T> 转换类型 * @return */ public <T> 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,不进行 null 判断 */ public <T> T findOptionalView(Object source, int id, String who) { View view = findView(source, id); return castView(view, id, who); } /** * 类型转换 */ @SuppressWarnings("unchecked") // That's the point. public <T> T castView(View view, int id, 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> 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); } } }

    这几个方法大家看下注释说明应该都能理解,下面直接来看下 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<FieldViewBinding> viewBindings = new ArrayList<>(); private final Map<Integer, FieldViewBinding> viewIdMap = new LinkedHashMap<>(); /** * 创建方法 * * @return MethodSpec */ private MethodSpec _createBindMethod() { // 略... if (_hasViewBinding()) { // View for (Map.Entry<Integer, FieldViewBinding> 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(); } /** * 添加 ViewBinding * * @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()); } /** * 判断 id 是否已经绑定 View * * @param id 资源ID * @return */ public FieldViewBinding isExistViewBinding(int id) { return viewIdMap.get(id); } }

    看下关键代码,这里用 viewIdMap 来存储设置过的ID 和 FieldViewBinding键值对,用来判断是否重复设置。在生成代码的时候,调用了 finder.findRequiredView() 方法来进行View的设置,这个是会进行null 判断的。

    这里也处理完就可以正常注入单个View了,下面来看下复数View的注入处理。

    复数View注入

    回过头看下解析复数View的 _parseBindMany() 方法:

    1、在这里首先要判断到底是 ARRAY还是LIST,并且取到列表所包含参数的 viewType类型,这里 FieldCollectionViewBinding.Kind 是个枚举值,等下会提到;

    2、果 viewType 是泛型参数的话进行泛型处理,并判断是否是View的子类型或接口;

    3、获取注解的ID值列表,并判断是否包含重复ID;

    4、生成 FieldCollectionViewBinding并添加到 BindingClass中

    来看下 FieldCollectionViewBinding的定义:

    /** * View列表信息 */ final 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 + "'"; } }

    和前面的 FieldViewBinding 类似,多了个 Kind枚举值来指明是 ARRAY 还是 LIST。在进行代码生成前,我们需要在 butterknifelib库中引入两个类:UtilsImmutableList。先看下 Utils

    @SuppressWarnings("deprecation") // public final class Utils { private Utils() { throw new AssertionError("No instances."); } @SafeVarargs public static <T> T[] arrayOf(T... views) { return filterNull(views); } @SafeVarargs public static <T> List<T> listOf(T... views) { return new ImmutableList<>(filterNull(views)); } private static <T> 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; } }

    它提供了两个接口来辅助实现View列表的注入,分别为 arrayOf(T... views)listOf(T... views),对ARRAY 还是 LIST。在这里面会自动去除列表中 null 的 View。在转换 LIST 时定义了个 ImmutableList来帮助完成这个工作,它是一个轻量级不可更改的LIST ,定义如下:

    final class ImmutableList<T> extends AbstractList<T> 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; } }最后来看下  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 UTILS = ClassName.get("com.dl7.butterknifelib", "Utils"); private final Map<FieldCollectionViewBinding, int[]> collectionBindings = new LinkedHashMap<>(); /** * 创建方法 * * @return MethodSpec */ private MethodSpec _createBindMethod() { // 略... // ViewList for (Map.Entry<FieldCollectionViewBinding, int[]> entry : collectionBindings.entrySet()) { String ofName; // UTILS的方法名 FieldCollectionViewBinding binding = entry.getKey(); int[] ids = entry.getValue(); // 获取方法名 if (binding.getKind() == FieldCollectionViewBinding.Kind.ARRAY) { ofName = "arrayOf"; } else if (binding.getKind() == FieldCollectionViewBinding.Kind.LIST) { ofName = "listOf"; } else { throw new IllegalStateException("Unknown kind: " + binding.getKind()); } // 填充复数 View 代码,作为 Utils 方法的参数 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()); } // 调用 Utils 的方法 result.addStatement("target.$L = $T.$L($L)", binding.getName(), UTILS, ofName, builder.build()); } // 略... return result.build(); } /** * 添加 ViewBinding * * @param binding 资源信息 */ void addFieldCollection(int[] ids, FieldCollectionViewBinding binding) { collectionBindings.put(binding, ids); } }这里会先判 断是 ARRAY 还是 LIST,然后调用 Utils对应的方法来进行View的注入,CodeBlock.BuilderJavaPoet中代码块的写法,详细使用看官方例子。

    样例

    我们在代码中使用注解如下:

    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<Button> mButtons; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); ButterKnife.bind(this); } }

    生成的代码如下:

    public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> { @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.<Button>findRequiredView(source, 2131492945, "field 'mButtons'"), finder.<Button>findRequiredView(source, 2131492946, "field 'mButtons'"), finder.<Button>findRequiredView(source, 2131492947, "field 'mButtons'")); } }

    你再对照前面说讲的流程来理解整个代码生成的过程是怎么来的,这样可以更好地理解它的实现原理。

    源码:ButterKnifeStudy

    转载请注明原文地址: https://ju.6miu.com/read-1301072.html
    最新回复(0)