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

相关文章:

浅析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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值