分分钟带你读懂 ButterKnife 的源码

基于Hadoop的NBA篮球球员大数据分析与可视化系统大数据分析系统python的项目(源码+lw+部署文档+讲解等) 本系统以 Hadoop 为基础,致力于对 NBA 篮球球员的海量数据进行深入分析与可视化呈现。它能够收集来自多个数据源的丰富数据,包括球员比赛统计数据、身体素质数据、技术特点数据等。利用 Hadoop 的分布式存储和计算能力,对这些大规模数据进行高效处理。通过数据分析算法,挖掘球员的潜在价值和特点,如球员的得分能力趋势、篮板球效率变化、不同位置球员的技术差异等。同时,借助先进的可视化技术,将分析结果以直观的图表、图形等形式展示出来。教练和球队管理层可以通过可视化界面清晰地了解球员的综合表现和发展态势,从而制 阅读详情

为什么要写这一系列的博客呢?

因为在 Android 开发的过程中, 泛型,反射,注解这些知识进场会用到,几乎所有的框架至少都会用到上面的一两种知识,如 Gson 就用到泛型,反射,注解,Retrofit 也用到泛型,反射,注解 。学好这些知识对我们进阶非常重要,尤其是阅读开源框架源码或者自己开发开源框架。

前言

ButterKnife 这个开源库火了有一段时间了,刚开始它的实现原理是使用反射实现的,性能较差。再后面的 版本中逐渐使用注解+放射实现,性能提高了不少。

ButterKnife是基于编译时的框架,它能够帮助我们减去每次写 FindViewById 的麻烦,截止到 2017.5.1 ,在 github 上面的 start 已经超过 15000.

本篇博客要分析的 ButterKnife 的源码主要包括以下三个部分,版本号是8.5.1

  • butterknife-annotations
  • butterknife-compiler
  • butterknife

其中 butterknife-annotations 库主要用来存放自定义注解;butterknife-compiler 主要是用来扫描哪些地方使用到我们的自定义注解,并进行相应的处理,生成模板代码等;butterknife 主要是用来注入我们的代码的。

我们先来先一下要怎样使用 butterknife:

ButterKnife 的基本使用

在 moudle 的 build.gradle 增加依赖

dependencies {
  compile 'com.jakewharton:butterknife:8.5.1'
  annotationProcessor 'com.jakewharton:butterknife-compiler:8.5.1'
}
public class SimpleActivity extends Activity {
  private static final ButterKnife.Action<View> ALPHA_FADE = new ButterKnife.Action<View>() {
    @Override public void apply(@NonNull View view, int index) {
      AlphaAnimation alphaAnimation = new AlphaAnimation(0, 1);
      alphaAnimation.setFillBefore(true);
      alphaAnimation.setDuration(500);
      alphaAnimation.setStartOffset(index * 100);
      view.startAnimation(alphaAnimation);
    }
  };

  @BindView(R2.id.title) TextView title;
  @BindView(R2.id.subtitle) TextView subtitle;
  @BindView(R2.id.hello) Button hello;
  @BindView(R2.id.list_of_things) ListView listOfThings;
  @BindView(R2.id.footer) TextView footer;

  @BindViews({ R2.id.title, R2.id.subtitle, R2.id.hello }) List<View> headerViews;

  private SimpleAdapter adapter;

  @OnClick(R2.id.hello) void sayHello() {
    Toast.makeText(this, "Hello, views!", LENGTH_SHORT).show();
    ButterKnife.apply(headerViews, ALPHA_FADE);
  }

  @OnLongClick(R2.id.hello) boolean sayGetOffMe() {
    Toast.makeText(this, "Let go of me!", LENGTH_SHORT).show();
    return true;
  }

  @OnItemClick(R2.id.list_of_things) void onItemClick(int position) {
    Toast.makeText(this, "You clicked: " + adapter.getItem(position), LENGTH_SHORT).show();
  }

  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);

    // Contrived code to use the bound fields.
    title.setText("Butter Knife");
    subtitle.setText("Field and method binding for Android views.");
    footer.setText("by Jake Wharton");
    hello.setText("Say Hello");

    adapter = new SimpleAdapter(this);
    listOfThings.setAdapter(adapter);
  }
}

调用 gradle build 命令,我们在相应的目录下将可以看到生成类似这样的代码。

public class SimpleActivity_ViewBinding<T extends SimpleActivity> implements Unbinder {
  protected T target;

  private View view2130968578;

  private View view2130968579;

  @UiThread
  public SimpleActivity_ViewBinding(final T target, View source) {
    this.target = target;

    View view;
    target.title = Utils.findRequiredViewAsType(source, R.id.title, "field 'title'", TextView.class);
    target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
    view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
    target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
    view2130968578 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.sayHello();
      }
    });
    view.setOnLongClickListener(new View.OnLongClickListener() {
      @Override
      public boolean onLongClick(View p0) {
        return target.sayGetOffMe();
      }
    });
    view = Utils.findRequiredView(source, R.id.list_of_things, "field 'listOfThings' and method 'onItemClick'");
    target.listOfThings = Utils.castView(view, R.id.list_of_things, "field 'listOfThings'", ListView.class);
    view2130968579 = view;
    ((AdapterView<?>) view).setOnItemClickListener(new AdapterView.OnItemClickListener() {
      @Override
      public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {
        target.onItemClick(p2);
      }
    });
    target.footer = Utils.findRequiredViewAsType(source, R.id.footer, "field 'footer'", TextView.class);
    target.headerViews = Utils.listOf(
        Utils.findRequiredView(source, R.id.title, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.subtitle, "field 'headerViews'"), 
        Utils.findRequiredView(source, R.id.hello, "field 'headerViews'"));
  }

  @Override
  @CallSuper
  public void unbind() {
    T target = this.target;
    if (target == null) throw new IllegalStateException("Bindings already cleared.");

    target.title = null;
    target.subtitle = null;
    target.hello = null;
    target.listOfThings = null;
    target.footer = null;
    target.headerViews = null;

    view2130968578.setOnClickListener(null);
    view2130968578.setOnLongClickListener(null);
    view2130968578 = null;
    ((AdapterView<?>) view2130968579).setOnItemClickListener(null);
    view2130968579 = null;

    this.target = null;
  }
}
ButterKnife 的执行流程

总的来说,大概可以分为以下几步:

  • 在编译的时候扫描注解,并做相应的处理,生成 java 代码,生成 Java 代码是调用 javapoet 库生成的。
  • 当我们调用 ButterKnife.bind(this); 方法的时候,他会根据类的全限定类型,找到相应的代码,并执行。完成 findViewById 和 setOnClick ,setOnLongClick 等操作。

第一步:在编译的时候扫描注解,并做相应的处理,生成 java 代码。这一步,可以拆分为几个小步骤:

  • 定义我们的注解,声明我们的注解是否保存到 java doc 中,可以作用于哪些区域(Filed ,Class等),以及是源码时注解,编译时注解还是运行时注解等)
  • 继承 AbstractProcessor,表示支持哪些类型的注解,支持哪些版本,
  • 重写 process 方法,处理相关的注解,存进 Map 集合中
  • 根据扫描到的注解信息(即 Map 集合),调用 javapoet 库生成 Java 代码。
butterknife-annotations 讲解


我们知道 ButterKnife 自定义很多的注解,有 BindArray,BindBitmap,BindColor,BindView 等,这里我们以 BindView 为例子讲解就 OK 了,其他的也是基本类似的,这里就不再讲解了。

//编译时注解
@Retention(CLASS)
//成员变量, (includes enum constants) 
@Target(FIELD)
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();
}
Processor 解析器说明

我们先来看一些基本方法:在 init 方法里面得到一些辅助工具类,这样有一个好处,确保工具类是单例的,因为 init 方法只会在初始化的时候调用。

public synchronized void init(ProcessingEnvironment env) {
    super.init(env);

    ---
   
    //辅助工具类
    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();
   
   ---
}

接着重写 getSupportedAnnotationTypes 方法,返回我们支持的注解类型。

@Override
public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
        types.add(annotation.getCanonicalName());
    }
    //返回支持注解的类型
    return types;
}

private Set<Class<? extends Annotation>> getSupportedAnnotations() {
    Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();

    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(BindInt.class);
    annotations.add(BindString.class);
    annotations.add(BindView.class);
    annotations.add(BindViews.class);
    annotations.addAll(LISTENERS);

    return annotations;
}

**接下来来看我们的重点, process 方法。**所做的工作大概就是拿到我们所有的注解信息,存进 map 集合,遍历 map 集合,做相应的 处理,生成 java 代码。

@Override
public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //  拿到所有的注解信息,TypeElement 作为 key,BindingSet 作为 value
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    // 遍历 map 里面的所有信息,并生成 java 代码
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingSet binding = entry.getValue();

        JavaFile javaFile = binding.brewJava(sdk);
        try {
            javaFile.writeTo(filer);
        } catch (IOException e) {
            error(typeElement, "Unable to write binding for type %s: %s", typeElement, e
                    .getMessage());
        }
    }

    return false;
}

这里我们进入 findAndParseTargets 方法,看里面到底是怎样将注解信息存进 map 集合的?

findAndParseTargets 方法里面 针对每一个自定义注解(BindArray,BindBitmap,BindColor,BindView) 等都做了处理,这里我们重点关注 @BindView 的处理即可。其他注解的处理思想也是一样的。

我们先来看一下 findAndParseTargets 方法的前半部分,遍历 env.getElementsAnnotatedWith(BindView.class) 集合,并调用 parseBindView 方法去转化。

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
    Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();

    scanForRClasses(env);

   
    // Process each @BindView element.
    for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
        // we don't SuperficialValidation.validateElement(element)
        // so that an unresolved View type can be generated by later processing rounds
        try {
            parseBindView(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindView.class, e);
        }
    }
    
    ---
    
    // 后半部分,待会再讲

}

可以看到牵绊部分的主要逻辑在 parseBindView 方法里面,主要做了以下几步操作:

  • 判断被注解 @BindView 修饰的成员变量是不是合法的,private 或者 static 修饰的,则出错。
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
                           Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

    // 判断是否被注解在属性上,如果该属性是被 private 或者 static 修饰的,则出错
    // 判断是否被注解在错误的包中,若包名以“android”或者“java”开头,则出错
    boolean hasError = isInaccessibleViaGeneratedCode(BindView.class, "fields", element)
            || isBindingInWrongPackage(BindView.class, element);

    // Verify that the target type extends from View.
    TypeMirror elementType = element.asType();
    if (elementType.getKind() == TypeKind.TYPEVAR) {
        TypeVariable typeVariable = (TypeVariable) elementType;
        elementType = typeVariable.getUpperBound();
    }
    Name qualifiedName = enclosingElement.getQualifiedName();
    Name simpleName = element.getSimpleName();
    // 判断元素是不是View及其子类或者Interface
    if (!isSubtypeOfType(elementType, VIEW_TYPE) && !isInterface(elementType)) {
        if (elementType.getKind() == TypeKind.ERROR) {
            note(element, "@%s field with unresolved type (%s) "
                            + "must elsewhere be generated as a View or interface. (%s.%s)",
                    BindView.class.getSimpleName(), elementType, qualifiedName, simpleName);
        } else {
            error(element, "@%s fields must extend from View or be an interface. (%s.%s)",
                    BindView.class.getSimpleName(), qualifiedName, simpleName);
            hasError = true;
        }
    }
    // 如果有错误,直接返回
    if (hasError) {
        return;
    }

    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();
    // 根据所在的类元素去查找 builder
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    // 如果相应的 builder 已经存在
    if (builder != null) {
        // 验证 ID 是否已经被绑定
        String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
        // 被绑定了,出错,返回
        if (existingBindingName != null) {
            error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
                    BindView.class.getSimpleName(), id, existingBindingName,
                    enclosingElement.getQualifiedName(), element.getSimpleName());
            return;
        }
    } else {
        // 如果没有相应的 builder,就需要重新生成,并别存放到  builderMap 中
        builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
    }

    String name = simpleName.toString();
    TypeName type = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

    builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));

    // Add the type-erased version to the valid binding targets set.
    erasedTargetNames.add(enclosingElement);
}

parseBindView 方法分析完毕之后,我们在回过头来看一下 findAndParseTargets 方法的后半部分,主要做的工作是对 bindingMap 进行重排序。

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {

    // 省略前半部分
       
    // Associate superclass binders with their subclass binders. This is a queue-based tree walk
    // which starts at the roots (superclasses) and walks to the leafs (subclasses).
    Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
            new ArrayDeque<>(builderMap.entrySet());
    Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
    while (!entries.isEmpty()) {
        Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();

        TypeElement type = entry.getKey();
        BindingSet.Builder builder = entry.getValue();
        //获取 type 的父类的 TypeElement
        TypeElement parentType = findParentType(type, erasedTargetNames);
        // 为空,存进 map
        if (parentType == null) {
            bindingMap.put(type, builder.build());
        } else {
             // 获取 parentType 的 BindingSet
            BindingSet parentBinding = bindingMap.get(parentType);
            if (parentBinding != null) {
                builder.setParent(parentBinding);
                bindingMap.put(type, builder.build());
            } else {
                // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
                // 为空,加到队列的尾部,等待下一次处理
                entries.addLast(entry);
            }
        }
    }

    return bindingMap;
}

到这里为止,我们已经分析完 ButterKnifeProcessor 是怎样处理注解的相关知识,并存进 map 集合中的,下面我们回到 process 方法,看一下是怎样生成 java 模板代码的。

public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //  拿到所有的注解信息,TypeElement 作为 key,BindingSet 作为 value
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    // 遍历 map 里面的所有信息,并生成 java 代码
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingSet binding = entry.getValue();
         // 生成 javaFile 对象
        JavaFile javaFile = binding.brewJava(sdk);
        try {
             //  生成 java 模板代码              
            javaFile.writeTo(filer);
        } catch (IOException e) {
            error(typeElement, "Unable to write binding for type %s: %s", typeElement, e
                    .getMessage());
        }
    }

    return false;
}

生成代码的核心代码只有这几行

// 生成 javaFile 对象
JavaFile javaFile = binding.brewJava(sdk);
try {
     //  生成 java 模板代码
    javaFile.writeTo(filer);
} catch (IOException e) {
    error(typeElement, "Unable to write binding for type %s: %s", typeElement, e
            .getMessage());
}

跟踪进去,发现是调用 square 公司开源的库 javapoet 开生成代码的。关于 javaPoet 的使用可以参考官网地址

JavaFile brewJava(int sdk) {
  return JavaFile.builder(bindingClassName.packageName(), createType(sdk))
      .addFileComment("Generated code from Butter Knife. Do not modify!")
      .build();
}

private TypeSpec createType(int sdk) {
    TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
            .addModifiers(PUBLIC);
    if (isFinal) {
        result.addModifiers(FINAL);
    }

    if (parentBinding != null) {
        result.superclass(parentBinding.bindingClassName);
    } else {
        result.addSuperinterface(UNBINDER);
    }

    if (hasTargetField()) {
        result.addField(targetTypeName, "target", PRIVATE);
    }
    // 如果是 View 或者是 View 的子类的话,添加构造方法
    if (isView) {
        result.addMethod(createBindingConstructorForView());
    } else if (isActivity) { // 如果是 Activity 或者是 Activity 的子类的话,添加构造方法
        result.addMethod(createBindingConstructorForActivity());
    } else if (isDialog) {  // 如果是 Dialog 或者是 Dialog 的子类的话,添加构造方法
        result.addMethod(createBindingConstructorForDialog());
    }
    //  如果构造方法不需要 View 参数,添加 需要 View 参数的构造方法
    if (!constructorNeedsView()) {
        // Add a delegating constructor with a target type + view signature for reflective use.
        result.addMethod(createBindingViewDelegateConstructor());
    }
    result.addMethod(createBindingConstructor(sdk));

    if (hasViewBindings() || parentBinding == null) {
        //生成unBind方法
        result.addMethod(createBindingUnbindMethod(result));
    }

    return result.build();
}

接着我们一起来看一下 createBindingConstructor(sdk) 方法,大概做的事情就是

  • 判断是否有设置监听,如果有监听,将 View 设置为 final
  • 遍历 viewBindings ,调用 addViewBinding 生成 findViewById 形式的代码。
private MethodSpec createBindingConstructor(int sdk) {
    MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
            .addAnnotation(UI_THREAD)
            .addModifiers(PUBLIC);
    // 如果有方法绑定,比如 @onClick,那么增加一个 targetTypeName 类型  的方法参数 target,并且是 final 类型的
    if (hasMethodBindings()) {
        constructor.addParameter(targetTypeName, "target", FINAL);
    } else { // 如果没有 ,不是 final 类型的
        constructor.addParameter(targetTypeName, "target");
    }
    //如果有注解的 View,那么添加 VIEW 类型 source 参数
    if (constructorNeedsView()) {
        constructor.addParameter(VIEW, "source");
    } else {
        //  添加 Context 类型的 context 参数
        constructor.addParameter(CONTEXT, "context");
    }

    if (hasUnqualifiedResourceBindings()) {
        // Aapt can change IDs out from underneath us, just suppress since all will work at
        // runtime.
        constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
                .addMember("value", "$S", "ResourceType")
                .build());
    }
    // 如果 @OnTouch 绑定 View,添加 @SuppressLint("ClickableViewAccessibility")
    if (hasOnTouchMethodBindings()) {
        constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
                .addMember("value", "$S", "ClickableViewAccessibility")
                .build());
    }
    // 如果 parentBinding 不为空,调用父类 的构造方法
    if (parentBinding != null) {
        if (parentBinding.constructorNeedsView()) {
            constructor.addStatement("super(target, source)");
        } else if (constructorNeedsView()) {
            constructor.addStatement("super(target, source.getContext())");
        } else {
            constructor.addStatement("super(target, context)");
        }
        constructor.addCode("\n");
    }
    //  添加成员变量
    if (hasTargetField()) {
        constructor.addStatement("this.target = target");
        constructor.addCode("\n");
    }

    if (hasViewBindings()) {
        if (hasViewLocal()) {
            // Local variable in which all views will be temporarily stored.
            constructor.addStatement("$T view", VIEW);
        }
        //   遍历  viewBindings,生成  source.findViewById($L) 代码
        for (ViewBinding binding : viewBindings) {
            addViewBinding(constructor, binding);
        }
        for (FieldCollectionViewBinding binding : collectionBindings) {
            constructor.addStatement("$L", binding.render());
        }

        if (!resourceBindings.isEmpty()) {
            constructor.addCode("\n");
        }
    }

    if (!resourceBindings.isEmpty()) {
        if (constructorNeedsView()) {
            constructor.addStatement("$T context = source.getContext()", CONTEXT);
        }
        if (hasResourceBindingsNeedingResource(sdk)) {
            constructor.addStatement("$T res = context.getResources()", RESOURCES);
        }
        for (ResourceBinding binding : resourceBindings) {
            constructor.addStatement("$L", binding.render(sdk));
        }
    }

    return constructor.build();
}

下面我们一起来看一下 addViewBinding 方法是怎样生成代码的。

private void addViewBinding(MethodSpec.Builder result, ViewBinding binding) {
    if (binding.isSingleFieldBinding()) {
        // Optimize the common case where there's a single binding directly to a field.
        FieldViewBinding fieldBinding = binding.getFieldBinding();
        // 注意这里直接使用了 target. 的形式,所以属性肯定是不能 private 的
        CodeBlock.Builder builder = CodeBlock.builder()
                .add("target.$L = ", fieldBinding.getName());

        boolean requiresCast = requiresCast(fieldBinding.getType());
        if (!requiresCast && !fieldBinding.isRequired()) {
            builder.add("source.findViewById($L)", binding.getId().code);
        } else {
            builder.add("$T.find", UTILS);
            builder.add(fieldBinding.isRequired() ? "RequiredView" : "OptionalView");
            if (requiresCast) {
                builder.add("AsType");
            }
            builder.add("(source, $L", binding.getId().code);
            if (fieldBinding.isRequired() || requiresCast) {
                builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
            }
            if (requiresCast) {
                builder.add(", $T.class", fieldBinding.getRawType());
            }
            builder.add(")");
        }
        result.addStatement("$L", builder.build());
        return;
    }

ButterKnife 是怎样实现代码注入的

使用过 ButterKnife 得人基本都知道,我们是通过 bind 方法来实现注入的,即自动帮我们 findViewById ,解放我们的双手,提高工作效率。下面我们一起来看一下 bind 方法是怎样实现注入的。

@NonNull
@UiThread
public static Unbinder bind(@NonNull Activity target) {
    View sourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
}

可以看到 bind 方法很简单,逻辑基本都交给 createBinding 方法去完成。我们一起进入 createBinding 方法来看一下到底做了什么。

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
    Class<?> targetClass = target.getClass();
    if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
    // 从 Class 中查找 constructor
    Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

    if (constructor == null) {
        return Unbinder.EMPTY;
    }

    //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
    try {
        // 反射实例化构造方法
        return constructor.newInstance(target, source);
    } catch (IllegalAccessException e) {
        throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InstantiationException e) {
        throw new RuntimeException("Unable to invoke " + constructor, e);
    } catch (InvocationTargetException e) {
        Throwable cause = e.getCause();
        if (cause instanceof RuntimeException) {
            throw (RuntimeException) cause;
        }
        if (cause instanceof Error) {
            throw (Error) cause;
        }
        throw new RuntimeException("Unable to create binding instance.", cause);
    }
}

其实 createBinding 来说,主要做了这几件事情

  • 传入 class ,通过 findBindingConstructorForClass 方法来实例化 constructor
  • 利用反射来初始化 constructor 对象
  • 初始化 constructor 失败会抛出异常

下面我们一起来看一下 findBindingConstructorForClass 方法是怎样实现的。

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
    //  读取缓存,如果不为空,直接返回
    Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
    if (bindingCtor != null) {
        if (debug) Log.d(TAG, "HIT: Cached in binding map.");
        return bindingCtor;
    }
    // 如果是 android ,java 原生的文件,不处理
    String clsName = cls.getName();
    if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
        if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
        return null;
    }
    try {
        Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
        //noinspection unchecked
        // 在原来所在的类查找
        bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View
                .class);
        if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
    } catch (ClassNotFoundException e) {
        if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
        //  在原来的类查找,查找不到,到父类去查找
        bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
    } catch (NoSuchMethodException e) {
        throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
    }
    // 存进 LinkedHashMap 缓存
    BINDINGS.put(cls, bindingCtor);
    return bindingCtor;
}

它的实现思想是这样的:

  • 读取缓存,若缓存命中,直接返回,这样有利于提高效率。从代码中可以看到,缓存是通过存进 map 集合实现的。
  • 是否是我们目标文件,是的话,进行处理,不是的话,直接返回,并打印相应的日志
  • 利用类加载器加载我们自己生成的 class 文件,并获取其构造方法,获取到,直接返回。获取不到,会抛出异常,在异常的处理中,我们再从当前 class 文件的父类去查找。并把结果存进 map 集合中,做缓存处理。

我们对 ButterKnife 的分析到此为止。

题外话

这篇博客主要是分析了 ButterKnife 的主要原理实现,对 ButterKnife 里面的一些实现细节并未详细分析。不过对我们读懂代码已经足够了。下一个系列,主要讲解 CoordinatorLayout 的实现原理及怎样自定义 CoordinatorLayout 的 behavior 实现仿新浪微博发现页面的效果,敬请期待。

关于我

更多信息可以点击关于我 , 非常希望和大家一起交流 , 共同进步

基于simulink的电气弹簧ES稳压控制模型仿真 若要补偿母线电压的谐波,需控制ES上的谐波,即在单闭环母线电压有效值控制的基础上,增加谐波抑制环节来进行谐波控制,现有的各种检测和抑制谐波的方法有采用模拟滤波器、傅里叶分析进行谐波分析、基于瞬时无功功率理论的方法等。当关键负载ZC为电流源型非线性负载时,电路原理图如图7所示,由于电流源型非线性负载可以等效为谐波电流源,关键负载电流ic便会含有谐波分量,导致输电线电流is和母线电压VS同样会含有谐波分量,导致供电质量的严重下降,对用电设备的正常工作造成影响。分别为流过线路阻抗、非关键负载、关键负载的电流, 阅读详情

相关推荐

ExpressLRS 915MHz vs 2.4GHz:实测对比与TX12遥控器的最佳搭配方案

本文深入对比了ExpressLRS 915MHz与2.4GHz两种频段在穿透能力、延迟、抗干扰等方面的实测表现,并结合RadioMaster TX12遥控器的硬件特性,提供了从微型花飞到极限远航等不同场景下的最佳搭配方案与EdgeTX系统配置指南,帮助FPV玩家根据核心飞行需求做出精准选择。

milk8的博客 444

ButterKnife的使用和原理

ButterKnife的使用和原理 参考文档: ButterKnife的使用:https://segmentfault.com/a/1190000016460847 ButterKnife的原理:https://juejin.cn/post/6844904181158019079 一、前言 ButterKnife是一个专注于Android系统的View注入框架,以前总是要写很多findViewById来找到View对象,有了ButterKnife可以很轻松的省去这些步骤。 使用ButterKnife对性能基

weixin_43750332的博客 1万+

DSP配置I2C通讯(非IO口软件模拟时序)

dsp即数字信号处理器,是一种广泛应用的嵌入式处理器,主要应用是实时快速地实现各种数字信号处理算法,目前,国际主要的dsp供应商是ti公司,其tms32系列产品占据了dsp市场近一半的份额,本文以TMS320F28377D为例,使用ti公司dsp开发工具ccs10.1配置i2c模块。 写这篇文章的缘由,因为市面上很多DSP其实内部是具有硬件I2C接口的,但由于网络上大部分资料和例程都是利用IO口模拟I2C时序进而实现I2C通讯的。给很多想学习硬件I2C通讯的小伙伴带来了困难。 下面先介绍一下配置步骤 1.E

weixin_44387442的博客 5544

ButterKnife(黄油刀)基本使用与源码解析

前言 ButterKnife又称黄油刀,是一款快速绑定Android视图中字段和方法的注解框架,也是Android开发中比较常用的一款快速注解框架,通过ButterKnife的合理运用,我们可以避免重复地书写findViewById,在各种场合下快速地绑定view中的多种事件,大大提高开发的效率,它具有如下几点主要的功能: 通过@BindView注解消除findViewById的使用 将多个v...

ai-exception的博客 5172

使用cubeMX生成USB HID设备

本文主要介绍如何使用stm32开发板和cubeMX生成HID设备,供参考。

专注音频,高效工作,快乐生活! 970

python 中心线提取_二值分析 | OpenCV + skimage如何提取中心线

点击上方蓝字关注我们微信公众号:OpenCV学堂关注获取更多计算机视觉与深度学习知识问题前几天有个人问了我一个问题,问题是这样的,他有如下的一张二值图像:怎么得到白色Blob中心线,他希望的效果如下:显然OpenCV中常见的轮廓分析无法获得上面的中心红色线段,本质上这个问题是如何提取二值对象的骨架,提取骨架的方法在OpenCV的扩展模块中,另外skimage包也支持图像的骨架提取。这里就...

weixin_42204453的博客 4290

安卓“小试牛刀”:ButterKnife——牛油刀

android牛油刀大解析冲冲冲!

m0_46579394的博客 6354

YOLO26全网首发:CVPR2026 FAA| 解决YOLO Neck跨尺度方向冲突--- FAAFusion频域对齐融合模块 | 暴力涨点篇

本文改进:FAAFusion引入YOLO neck ,代替nn.Upsample和Concat

①答疑群聊服务;②YOLO大模型知识问答系统;③计算机视觉论文生成智能体; 638

ButterKnife库(一个高效的工具库,小白只记录了一个用法-不深入研究,嘻嘻)

ButterKnife库(一个高效的工具库) 替代findViewById()最基本的使用,它不会干扰软件运行效率!!!而且看着高效(`通俗点:代码比较干净整洁`)

qq_37567841的博客 2721

SAP CPI 如何代理第三方接口?

代理接口是指在系统 A 和系统 B 之间引入一个中间层(如 SAP CPI),由中间层负责接收来自系统 A 的请求,进行必要的处理或转换,然后将请求转发给系统 B。由于 OData 服务通常使用 JSON 格式的数据,而某些内部系统可能需要 XML 格式,因此我们添加一个 JSON 到 XML 的转换器,以确保数据格式的兼容性。为了确保返回的数据格式符合内部系统的要求,添加一个 XML 到 JSON 的转换器,将 OData 服务的响应从 XML 转换为 JSON。在集成包中,添加一个新的集成流,命名为。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1748

File Load Error for Untitled.ipynb Unhandled error 解决方案

本文主要介绍了File Load Error for Untitled.ipynb Unhandled error 解决方案,希望能对使用jupyter notebook的同学们有所帮助。 文章目录 1. 问题描述 2. 解决方案

weixin_43178406的博客 1万+

butterknife 源码分析

因为在 Android 开发的过程中, 泛型,反射,注解这些知识进场会用到,几乎所有的框架至少都会用到上面的一两种知识,如 Gson 就用到泛型,反射,注解,Retrofit 也用到泛型,反射,注解 。学好这些知识对我们进阶非常重要,尤其是阅读开源框架源码或者自己开发开源框架。今天,这篇博客来带大家一起来阅读以下 ButterKnife 的源码

徐公,微信公众号同名 4363

C语言实现惯性导航初始对准全部核心代码

惯性导航初始对准包括间接粗对准和卡尔曼滤波精对准,其中。为基于卡尔曼滤波的精对准,其中。

十八与她的博客 2283

分分钟带你读懂-ButterKnife-的源码

//编译时注解 @Retention(CLASS) //成员变量, (includes enum constants) @Target(FIELD) public @interface BindView { /** View ID to which the field will be bound. */ @IdRes int value(); }#####Processor 解析器说明 我们先来看一些基本方法:在 init 方法里面得到一些辅助工具类,这样有一个好处,确保工具类是单例的,因为 init 方法

2401_84149814的博客 971

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

分分钟带你读懂-ButterKnife-的源码(1)

今天关于面试的分享就到这里,还是那句话,有些东西你不仅要懂,而且要能够很好地表达出来,能够让面试官认可你的理解,例如Handler机制,这个是面试必问之题。有些晦涩的点,或许它只活在面试当中,实际工作当中你压根不会用到它,但是你要知道它是什么东西。最后在这里小编分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司2021年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。还有。

2401_84132496的博客 846

IEM模型,地表粗糙度模拟

利用微波反演土壤水分时候,利用IEM模型可以实现对随机地表的模拟

分分钟带你读懂-ButterKnife-的源码(3)

笔者之前工作是在金融公司可能并不是特别追求技术,而笔者又是喜欢追求技术的人,所以格格不入,只能把目标放在互联网大厂了。也希望大家都去敢于尝试和追逐自己的梦想!BATJ大厂Android高频面试题《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!笔者之前工作是在金融公司可能并不是特别追求技术,而笔者又是喜欢追求技术的人,所以格格不入,只能把目标放在互联网大厂了。也希望大家都去敢于尝试和追逐自己的梦想!BATJ大厂Android高频面试题。

2401_84132232的博客 890

WINCC7.3破解文件

WINCC7.3硬件够破解文件,有需要的下载!WINCC7.3硬件够破解文件,有需要的下载!

分分钟带你读懂-ButterKnife-的源码(2)

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含。同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含。**接下来来看我们的重点, process 方法。第一步:在编译的时候扫描注解,并做相应的处理,生成 java 代码。

2401_84132204的博客 826

分分钟带你读懂-ButterKnife-的源码,深度解析跳槽从开始到结束完整流程

如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。GitHub地址PS:我GitHub中有大量高阶Android学习视频资料和面试资料包~source");} else {// 添加 Context 类型的 context 参数如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。[外链图片转存中…(img-cNa6GgCk-1727088456733)]GitHub地址。

2401_87545685的博客 1032

Android中ButterKnife的使用

简介 ButterKnife是注解中相对简单易懂的很不错的开源框架,使用ButterKnife可以提升我们的开发效率,不需要写很多findViewById来找到View对象,可以为我们省去很多的时间。 特点 1.强大的View绑定和Click事件处理功能,简化代码,提升开发效率 2.方便的处理Adapter里的ViewHolder绑定问题 3.运行时不会影响APP效率,使用配置方便 4...

wen_haha的博客 1万+

ButterKnife使用方法详解

介绍 ButterKnife是一个专注于Android系统的View注入框架,以前总是要写很多findViewById来找到View对象,有了ButterKnife可以很轻松的省去这些步骤。使用ButterKnife对性能基本没有损失,因为ButterKnife用到的注解并不是在运行时反射的,而是在编译的时候生成新的class。 GitHub地址:https://github.com/Jake...

孤独的冥王星 5万+

Butterknife全方位解析

而ButterKnife使用的则是编译器时期注入,在使用的时候,需要配置classpath ‘com.neenbedankt.gradle.plugins:android-apt:1.8’ , 这个配置说明,在编译的时候,进行注解处理。知晓了注解可以在编译的时候进行处理,那么,我们就可以得到注解的字段属性与所在类 , 进而生成注入文件,生成一个注入类的内部类,再进行字段处理 , 编译之后就会合并到注入类中,达到植入新代码段的目的。生成Java File , 根据获取的属性与类,创建一个注入类的内部类。

Android 开发架构 498
上一篇: 腾讯三面:ButterKnife为什么执行效率为什么比其他注入框架高?它的原理是什么
下一篇: 一道腾讯面试官指点:组件之间的跳转和组件通信原理机制
初壹十五a
博客等级 码龄8年 146粉丝 257原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值