Minutes to read ButterKnife's source code

Why write this series of blogs?

Because in the Android development process, generics, reflection, annotations will be used, almost all frameworks will use at least one or two of the above knowledge, such as Gson, generics, reflection, annotations, Retrofit also used generics, reflection, annotations. It is very important for us to learn these knowledge well, especially when we read the open source framework source code or develop the open source framework ourselves.


ButterKnife, an open source repository, has been on fire for some time. At first, its implementation principle was based on reflection, and its performance was poor. In later versions, annotations + radiation are gradually used to achieve better performance.

ButterKnife is a compile-time framework that helps us eliminate the hassle of writing FindViewById every time. By 2017.5.1, the start on github has exceeded 15,000.

The source code of ButterKnife to be analyzed in this blog includes three parts, version number is 8.5.1.

  • butterknife-annotations
  • butterknife-compiler
  • butterknife

butterknife-annotations library is mainly used to store custom annotations; butterknife-compiler is mainly used to scan where to use our custom annotations, and to process and generate template code; butterknife is mainly used to inject our code.

Let's start with how to use butterknife:
Basic use of ButterKnife
Adding dependencies to 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.setStartOffset(index * 100);

  @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) {

    // 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);

Calling the gradle build command, we will see the generation of code like this in the corresponding directory.

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

  private View view2130968578;

  private View view2130968579;

  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() {
      public void doClick(View p0) {
    view.setOnLongClickListener(new View.OnLongClickListener() {
      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() {
      public void onItemClick(AdapterView<?> p0, View p1, int p2, long p3) {
    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'"));

  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 = null;
    ((AdapterView<?>) view2130968579).setOnItemClickListener(null);
    view2130968579 = null;

    this.target = null;

ButterKnife's execution process
Generally speaking, it can be divided into the following steps:

  • Scanning annotations at compilation time, and doing corresponding processing, generate Java code, generate Java code is generated by calling the javapoet library.
  • When we call ButterKnife.bind(this); when a method is called, it finds the corresponding code and executes it according to the fully qualified type of the class. Complete findViewById and setOnClick, setOnLongClick and other operations.

Step 1: Scan annotations at compilation time and do corresponding processing to generate java code. This step can be divided into several small steps:

  • Define our annotations, state whether our annotations are saved in java doc, which areas can be acted on (Filed, Class, etc.), and whether they are source-time annotations, compile-time annotations, or run-time annotations, etc.
  • Inherit AbstractProcessor to indicate which types of annotations are supported and which versions are supported.
  • Rewrite the process method, process related annotations, and store them in the Map collection
  • According to the scanned annotation information (that is, the Map collection), Java code is generated by calling the javapoet library.


We know that ButterKnife customizes a lot of annotations, such as BindArray, BindBitmap, BindColor, BindView, etc. Here we take BindView as an example to explain OK, and the others are basically similar, so we will not explain here.

//Compile-time annotations
//Member variables (including enum constants) 
public @interface BindView {
  /** View ID to which the field will be bound. */
  @IdRes int value();

Processor parser description
Let's first look at some basic methods: get some auxiliary tool classes in the init method, which has the advantage of ensuring that the tool class is singleton, because the init method will only be invoked at initialization time.

public synchronized void init(ProcessingEnvironment env) {


    //Auxiliary Tool Class
    elementUtils = env.getElementUtils();
    typeUtils = env.getTypeUtils();
    filer = env.getFiler();


Then rewrite the getSupportedAnnotationTypes method to return the annotation types we support.

public Set<String> getSupportedAnnotationTypes() {
    Set<String> types = new LinkedHashSet<>();
    for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
    //Returns the type of supporting annotations
    return types;

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


    return annotations;

Next, let's look at our focus, the process method. What we do is probably to get all our annotation information, store it in the map set, traverse the map set, do the corresponding processing, and generate java code.

public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //  Get all the annotation information, TypeElement as key, BindingSet as value
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    // Traverse all the information in the map and generate java code
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingSet binding = entry.getValue();

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

    return false;

Here we go into the find AndParseTargets method to see how annotation information is stored in the map collection.

findAndParseTargets method for each custom annotation (BindArray, BindBitmap, BindColor, BindView) has been processed, here we focus on @BindView processing can be. The same is true of other annotations.

Let's first look at the first half of the findAndParseTargets method, traverse the env. getElements Annotated With (BindView. class) collection, and invoke the parseBindView method to transform.

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


    // 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);


    // The latter part, I'll talk about it later.


You can see that the main logic of the stumbling part is in the parseBindView method, which mainly does the following steps:

  • Judging whether a member variable modified by the annotation @BindView is legal or not, private ly or static ally, is wrong.

    private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
                           Set<TypeElement> erasedTargetNames) {
    TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
    // An error occurs when judging whether an attribute is annotated, if it is modified by private or static
    // If the package name begins with "android" or "java", an error will occur.
    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();
    // Determine whether the element is View and its subclasses or 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 there is an error, return it directly
    if (hasError) {
    // Assemble information on the field.
    int id = element.getAnnotation(BindView.class).value();
    // Find builder based on the class element
    BindingSet.Builder builder = builderMap.get(enclosingElement);
    QualifiedId qualifiedId = elementToQualifiedId(element, id);
    // If the corresponding builder already exists
    if (builder != null) {
        // Verify that the ID has been bound
        String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
        // Binded, Error, Return
        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());
    } else {
        // If there is no corresponding builder, it needs to be regenerated and not stored in the builder Map.
        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.

    After parseBindView method has been analyzed, let's look back at the second half of findAndParseTargets method. The main task is to reorder the bindingMap.

    private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
    // Omit the first half
    // 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();
        //Get TypeElement of the parent class of type
        TypeElement parentType = findParentType(type, erasedTargetNames);
        // Empty, stored in map
        if (parentType == null) {
            bindingMap.put(type, builder.build());
        } else {
             // Get the BindingSet of parentType
            BindingSet parentBinding = bindingMap.get(parentType);
            if (parentBinding != null) {
                bindingMap.put(type, builder.build());
            } else {
                // Has a superclass binding but we haven't built it yet. Re-enqueue for later.
                // Empty, add to the end of the queue, wait for the next processing
    return bindingMap;

    So far, we have analyzed how ButterKnifeProcessor handles annotations and stored them in the map collection. Now let's go back to the process method and see how to generate java template code.

    public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    //  Get all the annotation information, TypeElement as key, BindingSet as value
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
    // Traverse all the information in the map and generate java code
    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingSet binding = entry.getValue();
         // Generating javaFile objects
        JavaFile javaFile = binding.brewJava(sdk);
        try {
             //  Generate java template code              
        } catch (IOException e) {
            error(typeElement, "Unable to write binding for type %s: %s", typeElement, e
    return false;

    There are only a few lines at the core of the generated code

    // Generating javaFile objects
    JavaFile javaFile = binding.brewJava(sdk);
    try {
     //  Generate java template code
    } catch (IOException e) {
    error(typeElement, "Unable to write binding for type %s: %s", typeElement, e

    Tracking in, we found that the code was generated by calling square's open source library javapoet. For the use of javaPoet, you can refer to the official address.

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

private TypeSpec createType(int sdk) {
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
if (isFinal) {

if (parentBinding != null) {
} else {

if (hasTargetField()) {
    result.addField(targetTypeName, "target", PRIVATE);
// If it's a View or a subclass of View, add a constructor
if (isView) {
} else if (isActivity) { // If it's an Activity or a subclass of Activity, add a constructor
} else if (isDialog) {  // If it's a Dialog or a subclass of Dialog, add a constructor
//  If the constructor does not need the View parameter, add the constructor that needs the View parameter
if (!constructorNeedsView()) {
    // Add a delegating constructor with a target type + view signature for reflective use.

if (hasViewBindings() || parentBinding == null) {
    //Generating unBind Method

return result.build();


Then let's take a look at the createBindingConstructor(sdk) method. Probably what we do is

- Determine if there is a listener, and if there is a listener, set View to final
 - Traversing through viewBindings, call addViewBinding to generate code in the form of findViewById.

private MethodSpec createBindingConstructor(int sdk) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
// If you have method bindings, such as @onClick, add a targetTypeName type method parameter target, which is final type
if (hasMethodBindings()) {
constructor.addParameter(targetTypeName, "target", FINAL);
} Other {// If not, not final
constructor.addParameter(targetTypeName, "target");
// If there is a commented View, add the VIEW type source parameter
if (constructorNeedsView()) {
constructor.addParameter(VIEW, "source");
} else {
// Add context parameters of Context type
constructor.addParameter(CONTEXT, "context");

if (hasUnqualifiedResourceBindings()) {
    // Aapt can change IDs out from underneath us, just suppress since all will work at
    // runtime.
            .addMember("value", "$S", "ResourceType")
// If @OnTouch binds View, add @SuppressLint ("Clickable View Accessibility")
if (hasOnTouchMethodBindings()) {
            .addMember("value", "$S", "ClickableViewAccessibility")
// If parentBinding is not empty, call the construction method of the parent class
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)");
//  Add member variables
if (hasTargetField()) {
    constructor.addStatement("this.target = target");

if (hasViewBindings()) {
    if (hasViewLocal()) {
        // Local variable in which all views will be temporarily stored.
        constructor.addStatement("$T view", VIEW);
    //   Traversing through viewBindings to generate source.findViewById($L) code
    for (ViewBinding binding : viewBindings) {
        addViewBinding(constructor, binding);
    for (FieldCollectionViewBinding binding : collectionBindings) {
        constructor.addStatement("$L", binding.render());

    if (!resourceBindings.isEmpty()) {

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();


Let's take a look at how the addViewBinding method generates code.

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();
// Note that the target. form is used directly here, so the attribute must not be 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("(source, $L", binding.getId().code);
        if (fieldBinding.isRequired() || requiresCast) {
            builder.add(", $S", asHumanDescription(singletonList(fieldBinding)));
        if (requiresCast) {
            builder.add(", $T.class", fieldBinding.getRawType());
    result.addStatement("$L", builder.build());
How ButterKnife Implements Code Injection
 People who have used ButterKnife basically know that we use the bind method to achieve injection, that is, automatically help us find ViewById, liberate our hands, improve work efficiency. Let's take a look at how the bind method implements injection.

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

You can see that the bind method is very simple, and the logic is basically left to the createBinding method to complete. Let's go into the createBinding method and see what we've done.

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());
// Find constructor from Class
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

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

//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
    // Reflection instantiation construction method
    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);


In fact, for createBinding, the main thing is to do these things.

- Input class, instantiate constructor through findBindingConstructorForClass method
 - Initialization of constructor objects by reflection
 - Failure to initialize constructor throws an exception

Let's take a look at how the findBindingConstructorForClass method is implemented.

private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
// Read the cache, if not empty, return directly
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
// If it's Android, Java Native file, not processed
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
// Find the original class
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View
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());
// In the original class search, the search can not be found, to the parent class to find
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
// Store in LinkedHashMap Cache
BINDINGS.put(cls, bindingCtor);
return bindingCtor;

Its realization idea is as follows:
- Read the cache, if the cache hits, return directly, which is conducive to improving efficiency. As you can see from the code, caching is achieved by storing in the map collection.
- Whether it's our target file, yes, process it, if not, return it directly and print the corresponding log.
- Class loader is used to load the class file generated by ourselves, and get its construction method, get it, and return it directly. If we can't get it, an exception will be thrown. In the handling of the exception, we will look for it from the parent class of the current class file. The results are stored in the map set and cached.

So far is our analysis of ButterKnife.

** Off-topic remarks**
This blog mainly analyses the main principle and implementation of ButterKnife, but does not analyze in detail some implementation details of ButterKnife. But it's enough for us to understand the code. In the next series, we will mainly explain the implementation principle of Coordinator Layout and how to customize the behavior of Coordinator Layout to achieve the effect of page discovery imitating Sina Weibo. Please look forward to it.
** About me **
More information can be clicked on [About me] (https://www.jianshu.com/p/78f6f0b5bce4)
I hope to communicate with you and make progress together.

Tags: Android ButterKnife Java SDK

Posted on Thu, 10 Oct 2019 00:03:37 -0700 by new2phpcode