Analysis of Butterknife Principle

Posted by www.phphub.com on Thu, 16 May 2019 16:21:41 +0200

For reprinting, please indicate the source: [Gu Linhai's blog]

Preface

Butterknife is a View injection framework focusing on Android system, which can simplify the code, such as findViewById, event monitoring, resource binding, and so on. At the same time, the framework uses compile-time annotations. Maybe when you hear compile-time annotations, you think that this way will affect performance. In fact, compile-time annotations will not affect the performance of applications, because compile-time annotations are in the code. In the compilation process, annotations are processed and codes are generated. In addition to compilation-time annotations, there is also a runtime annotation, which obtains information about classes, methods, parameters and so on through reflection in the running process. Therefore, runtime annotations will have performance problems.

Principle analysis

Usually when using Butterknife, you need to call Butterknife's bind method. Butterknife provides the following kinds of bind methods:

The above six methods will eventually call the createBinding method, which takes the bind(Activity) code as an example:

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

The method obtains DecorView by passing in Activity, which is the top view of the whole View tree. It contains the title bar and ContentView. The ContentView is the view view we defined. After getting DecorView, we call the createBinding method and pass the target Activity and DecorView to the past.

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

The method obtains the target Class, and obtains the constructor through the findBindingConstructorForClass method. The code of the findBindingConstructorForClass method is as follows:

@Nullable @CheckResult @UiThread
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;
  }
  //Create the following file
  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);
  }
  BINDINGS.put(cls, bindingCtor);
  return bindingCtor;
}

BINDINGS is a Map for caching. It is convenient to extract from the collection during the next use. If not, it loads the name of the current class plus _ViewBinding, such as MainActivity_ViewBinding, loads and obtains the class, and then caches the loaded class into the BINDINGS collection after obtaining its two parameters. Then it is instantiated by the new Instance in the createBinding method.

From the binding method executed by Butterknife above, we can know that we need to load the class name+_ViewBinding first and instantiate it, but this class is not written, it is automatically generated, that is, compile-time generation, compile-time annotations need AbstractProcessor class to achieve, need to rewrite its process method, such as the following:

public class TestProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // TODO Auto-generated method stub return false; } }
    }
}

In the process method, annotated code can be scanned and processed, and related Java files can be generated. Look at the process method in the Butterknife class ButterKnifeProcessor, which inherits AbstractProcessor:

@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
    Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);

    for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
        TypeElement typeElement = entry.getKey();
        BindingSet binding = entry.getValue();

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

    return false;
}

findAndParseTargts is a method for processing @BindViewXX annotations. There are multiple loop codes inside, and the function of these for loops is to process annotations.

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

    scanForRClasses(env);

    // Process each @BindAnim element.
    for (Element element : env.getElementsAnnotatedWith(BindAnim.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceAnimation(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindAnim.class, e);
        }
    }

    // Process each @BindArray element.
    for (Element element : env.getElementsAnnotatedWith(BindArray.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceArray(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindArray.class, e);
        }
    }

    // Process each @BindBitmap element.
    for (Element element : env.getElementsAnnotatedWith(BindBitmap.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceBitmap(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindBitmap.class, e);
        }
    }

    // Process each @BindBool element.
    for (Element element : env.getElementsAnnotatedWith(BindBool.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceBool(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindBool.class, e);
        }
    }

    // Process each @BindColor element.
    for (Element element : env.getElementsAnnotatedWith(BindColor.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceColor(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindColor.class, e);
        }
    }

    // Process each @BindDimen element.
    for (Element element : env.getElementsAnnotatedWith(BindDimen.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceDimen(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindDimen.class, e);
        }
    }

    // Process each @BindDrawable element.
    for (Element element : env.getElementsAnnotatedWith(BindDrawable.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceDrawable(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindDrawable.class, e);
        }
    }

    // Process each @BindFloat element.
    for (Element element : env.getElementsAnnotatedWith(BindFloat.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceFloat(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindFloat.class, e);
        }
    }

    // Process each @BindFont element.
    for (Element element : env.getElementsAnnotatedWith(BindFont.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceFont(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindFont.class, e);
        }
    }

    // Process each @BindInt element.
    for (Element element : env.getElementsAnnotatedWith(BindInt.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceInt(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindInt.class, e);
        }
    }

    // Process each @BindString element.
    for (Element element : env.getElementsAnnotatedWith(BindString.class)) {
        if (!SuperficialValidation.validateElement(element)) continue;
        try {
            parseResourceString(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindString.class, e);
        }
    }

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

    // Process each @BindViews element.
    for (Element element : env.getElementsAnnotatedWith(BindViews.class)) {
        // we don't SuperficialValidation.validateElement(element)
        // so that an unresolved View type can be generated by later processing rounds
        try {
            parseBindViews(element, builderMap, erasedTargetNames);
        } catch (Exception e) {
            logParsingError(element, BindViews.class, e);
        }
    }

    // Process each annotation that corresponds to a listener.
    for (Class<? extends Annotation> listener : LISTENERS) {
        findAndParseListener(env, listener, builderMap, erasedTargetNames);
    }

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

        TypeElement parentType = findParentType(type, erasedTargetNames);
        if (parentType == null) {
            bindingMap.put(type, builder.build());
        } else {
            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;
}

There are many method codes, mainly dealing with the following annotations:

After processing, JavaFile javaFile = binding.brewJava(sdk, debuggable); generates Java files.

Then look at the generated files, such as our MainActivity_ViewBinding file, under build/generated/source/apt/debug/packgename/:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.btn_test)
    Button mButton;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
        mButton.setText("test");
    }

    @OnClick(R.id.btn_test)
    public void clickButton() {
        Toast.makeText(this, "test", Toast.LENGTH_SHORT).show();
    }
}


public class MainActivity_ViewBinding implements Unbinder {
  private MainActivity target;

  private View view2131427415;

  @UiThread
  public MainActivity_ViewBinding(MainActivity target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public MainActivity_ViewBinding(final MainActivity target, View source) {
    this.target = target;

    View view;
    view = Utils.findRequiredView(source, R.id.btn_test, "field 'mButton' and method 'clickButton'");
    target.mButton = Utils.castView(view, R.id.btn_test, "field 'mButton'", Button.class);
    view2131427415 = view;
    view.setOnClickListener(new DebouncingOnClickListener() {
      @Override
      public void doClick(View p0) {
        target.clickButton();
      }
    });
  }

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

    target.mButton = null;

    view2131427415.setOnClickListener(null);
    view2131427415 = null;
  }
}


public abstract class DebouncingOnClickListener implements View.OnClickListener {
  static boolean enabled = true;

  private static final Runnable ENABLE_AGAIN = new Runnable() {
    @Override public void run() {
      enabled = true;
    }
  };

  @Override public final void onClick(View v) {
    if (enabled) {
      enabled = false;
      v.post(ENABLE_AGAIN);
      doClick(v);
    }
  }

  public abstract void doClick(View v);
}

Above, ButterKnife's bind method will be instantiated through the constructor of two parameters. In the constructor, we can see that the findViewById method will eventually be invoked and the click event will be monitored. Debouncing OnClickListener is a subclass of View.OnclickListener, which is used to prevent multiple clicks on View in a certain period of time and to execute extraction in onClick method. Like the method doClick, in the constructor of MainActivity_ViewBinding, you can see that the clickButton method of MainActivity is called when a Button is clicked, that is, the method defined by annotation @OnClick in MainActivity.

Topics: ButterKnife Java Android SDK