The premise

This article mainly tells the story behind the ButterKnife IOC framework. Although there are many such posts on the Internet, this article will explain each field in detail (version=8.5.1, the principle is the same, but the version may be different, and some internal implementation may be different), so it should be a bit of suspense. @bindView What a line of code does for me. The purpose of this framework is to save you from the tedious block of findViewById every time. What is the story behind it? Now listen to me…

Annotation

Ha ha ha ha come up to tell the principle, don’t tell the principle how can you know the story behind ah, you say si not SI! After all, it’s an IOC framework and JAVA annotations are something you’ll see in everyday code blocks. This article is about the story behind ButterKnife. I’m not going to explain annotations in detail, but just some of the annotations used in this framework. If you want to learn more about annotations, check out this article: Portal Annotations

Whether I have read this article or not, I will first say how to customize annotations, understand the basic probably can understand this article.

Meta-annotation (Retention, Target)

@rentention is to mean time to hold the annotation, and there are three options

1. Reserved SOURCE code. Such annotations are mostly used for verification, such as Override, Deprecated, SuppressWarnings

2.CLASS must mean compile-time, which means that our project’s Java files are being translated into CLASS when apt automatically parses

AbstractProcessor - Override the 'process' function in AbstractProcessorCopy the code

Apt automatically finds all classes that inherit from AbstractProcessor at compile time. Then call their process method to process it. (Our ButterKnife has a custom ButterKnifeProcessor class that we’ll talk about later.)

3.RUNTIME RUNTIME retention. The program uses these annotations during RUNTIME, such as @test.

@target indicates which elements an annotation can be used to modify. Optional values include TYPE, METHOD, CONSTRUCTOR, FIELD, PARAMETER, and so on

ButterKnifeProcessor

Since each annotation is a ClASS for JakeWharton, the ButterKnifeProcessor process is called at compile time for all Java files. Ok, now let’s start parsing the source code.

@Override public boolean process(Set<? Extends TypeElement> elements, RoundEnvironment env) {// this extends TypeElement> elements, RoundEnvironment env) BindingSet> bindingMap = findAndParseTargets(env); // Iterate through the corresponding xxx_ViewBinding filefor (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;
  }Copy the code

The code above is not too long. First, we can see that the first line creates a Map collection containing key = TypeElement, which is passed by the RoundEnvironment

TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();Copy the code

If you don’t understand the meaning of Elements

Elements is a utility class that works with Element. Element represents an Element of a program, such as a package, class, or method. Typeelements represent type elements in source code, such as classes, fields, methods, and so on. You can get the name of the class from TypeElement, but you can’t get information about the class, such as its parent, from a TypeMirror, which calls Element’s asType() function

So what does value = BindingSet mean? Let’s take a look at the source code. Here is BindingSet, his Builder

static final class Builder { private final TypeName targetTypeName; private final ClassName bindingClassName; private final boolean isFinal; private final boolean isView; private final boolean isActivity; private final boolean isDialog; private BindingSet parentBinding; // Store (@bindView (id)) this id private final Map< id, ViewBinding.Builder> viewIdMap = new LinkedHashMap<>(); private final ImmutableList.Builder<FieldCollectionViewBinding> collectionBindings = ImmutableList.builder(); private final ImmutableList.Builder<ResourceBinding> resourceBindings = ImmutableList.builder(); private Builder(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal, boolean isView, boolean isActivity, boolean isDialog) { this.targetTypeName = targetTypeName; this.bindingClassName = bindingClassName; this.isFinal = isFinal; this.isView = isView; this.isActivity = isActivity; this.isDialog = isDialog; }Copy the code

The reason for Posting the Builder is that it is easier to understand what the class does. It is to save a class (the current Activity) with comments about ButterKnife. The viewIdMap above is used to store the id (@bindView (ID)), and we’re looking at some methods of the inner Class Builder to give you a better idea of what it’s really doing

// use @bindView (r.idest) void addField(Id Id, FieldViewBinding binding) { getOrCreateViewBindings(id).setFieldBinding(binding); } void addFieldCollection(FieldCollectionViewBinding binding) { collectionBindings.add(binding); } // methodbind
    boolean addMethod(
        Id id,
        ListenerClass listener,
        ListenerMethod method,
        MethodViewBinding binding) {
      ViewBinding.Builder viewBinding = getOrCreateViewBindings(id);
      if (viewBinding.hasMethodBinding(listener, method) && !"void".equals(method.returnType())) {
        return false;
      }
      viewBinding.addMethodBinding(listener, method, binding);
      return true; } // for @bindbitmap@binddimen... Just some resource filesbind
    void addResource(ResourceBinding binding) {
      resourceBindings.add(binding);
    }Copy the code

The BuilderSet class saves information about the class you annotated and generates code later.

The first line of Map code in process() does exactly what it does, and the loop in process() does exactly what it does. I have also commented the above code block to generate the corresponding xxx_ViewBinding file. How do you make it? Those of you who are careful will notice that there is a filer in there, which is actually some of the tools we use when we initialize it. Here are some of the operations that ButterKnife initializes

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

    String sdk = env.getOptions().get(OPTION_SDK_INT);
    if(sdk ! = null) { try { this.sdk = Integer.parseInt(sdk); } catch (NumberFormatException e) { env.getMessager() .printMessage(Kind.WARNING,"Unable to parse supplied minSdk option '"
                + sdk
                + "'. Falling back to API 1 support."); } // Scan each Element elementUtils = env.getelementutils (); // is the utility class used to handle TypeMirrortypeUtils = env.getTypeUtils(); Filer = env.getfiler (); try { trees = Trees.instance(processingEnv); } catch (IllegalArgumentException ignored) { } }Copy the code

It’s just some initialization. Initialization of elementUtils, typeUtils, filer, etc.

I’ll leave you with this one (I’ll talk about how it’s generated later). We know that when Java files are compiled, ButterKnifeProcessor uses the process() method to generate the teammates’ xxx_ViewBinding file. The problem then is how to bind this file to our annotated file (xxxactivity.java, later replaced by xx).

How to bind xxx_ViewBinding

Those of you who have used BindKnife know that you need to bind (setContentView or OnViewCreated later) and unBind in your BaseActivity or in your current Activity. That’s the key. So let’s take @bindView as an example. So without further ado, let’s get to the code

  @NonNull @UiThread
  public static Unbinder bind(@nonnull Activity target) {// Get the outermost View ViewsourceView = target.getWindow().getDecorView();
    return createBinding(target, sourceView);
  }Copy the code

The following is an implementation of the createBinding code above

private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {// Get the current Class<? > targetClass = target.getClass();if (debug) Log.d(TAG, "Looking up binding for "+ targetClass.getName()); // Use this class to find the Constructor< for the xxx_ViewBinding file. extends Unbinder> constructor = findBindingConstructorForClass(targetClass);if (constructor == null) {
      returnUnbinder.EMPTY; // NoInspection trywithidenticaltodcatch catch at three different locations: API 19+ only type. try {// Initialize this xxx_ViewBinding filereturn constructor.newInstance(target, source); } catch (IllegalAccessException e) { .... }}Copy the code

The above code I’ve written comments, you can see the main code is findBindingConstructorForClass this method to find our the current this Activity for xxx_ViewBinding then get his construction method, and then the initial, So let’s go into this method and see what we’re doing.

@Nullable @CheckResult @UiThread private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<? > CLS) {// Get the xxx_ViewBinding Constructor from the collection (this map is used for caching and does not need to be retrieved next time). extends Unbinder> bindingCtor = BINDINGS.get(cls);if(bindingCtor ! = null) {if (debug) Log.d(TAG, "HIT: Cached in binding map.");
      returnbindingCtor; } // get clsName String clsName = cls.getName(); // Filter is not requiredif (clsName.startsWith("android.") || clsName.startsWith("java.")) {
      if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
      returnnull; } try {// Get the xxx_ViewBinding class by reflection and then get its constructor. > bindingClass = Class.forName(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); } // If the above is not fetched from the map collection, the reflected one will be added to the collection for the next direct fetch. Binding. put(CLS, bindingCtor);return bindingCtor;
  }Copy the code

See the code above? Each line is commented, and you can see that he gets the xxx_ViewBinding file by reflection and gets the constructor that created it. The BINDINGS collection is then used for caching, which reduces the time required by reflection.

What kind of file is the xxx_ViewBinding generated

public class CameraActivityRep_ViewBinding implements Unbinder {
  private CameraActivityRep target;

  @UiThread
  public CameraActivityRep_ViewBinding(CameraActivityRep target) {
    this(target, target.getWindow().getDecorView());
  }

  @UiThread
  public CameraActivityRep_ViewBinding(CameraActivityRep target, View source) { this.target = target; / / is the findviewById target. ModelPanorama = Utils. FindRequiredViewAsType (source, R.id.model_panorama, "field 'modelPanorama'", ImageView.class);
    target.modelCapture = Utils.findRequiredViewAsType(source, R.id.model_capture, "field 'modelCapture'", ImageView.class);

  }

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

    target.modelPanorama = null;
    target.modelCapture = null;

  }Copy the code

Here we finally see where our findViewById is in the constructor of his generator file, findViewById, butterknife.bind (this); This is what findViewById does, fetching the generated xxx_ViewBinding file via bind, then fetching the constructor via reflection, and initialization of the constructor. The findViewById operation is done inside the constructor.

Actually everyone might say I saw the Utils. I can’t see clearly the findViewById findRequiredViewAsType (source, R.i d.m odel_panorama, “field ‘modelPanorama”, Imageview.class), okay, so let’s go ahead and see what utils is doing and if it’s findViewById

 public static <T> T findRequiredViewAsType(View source, @idres int id, String who, Class<T> CLS) {//MD View View = findRequiredView(source, id, who);
    return castView(view, id, who, cls);
  }Copy the code

MD I haven’t seen it yet keep reading


 public static View findRequiredView(View source, @idres int id, String who) {View View = source.findViewById(id);if(view ! = null) {return view;
    }
    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.");
  }Copy the code

All right, you know what I mean? The boy’s hiding a lot.

MDZZ findViewById triggers all this stuff and that’s the unspeakable secret behind @bindView if you’re interested you can read on and see how it generates the xxx_ViewBinding, right

How to generate xxx_ViewBinding

So let’s go back to the ButterKnifeProcessor and the process () method and I don’t know if you remember the code in it but I’m going to post it again

@Override public boolean process(Set<? Extends TypeElement> elements, RoundEnvironment env) {// this extends TypeElement> elements, RoundEnvironment env) BindingSet> bindingMap = findAndParseTargets(env); // Iterate through the corresponding xxx_ViewBinding filefor (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;
  }Copy the code

I’m not going to go over the key points here, since I’ve already covered some of the parameters in this method

1. Get all the annotated elements and store the corresponding BindingSet for each TypeElement in a Map

findAndParseTargets(env)Copy the code

2. Generate the xxx_ViewBinding file to due

JavaFile javaFile = binding.brewJava(sdk);
javaFile.writeTo(filer);Copy the code

We can see these two points, so let’s start with the first one. Since it is a method, must go to the method inside, see what the source code is doing.

private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) { Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>(); Set<TypeElement> erasedTargetNames = new LinkedHashSet<>(); scanForRClasses(env); . // Find each element with @bindView to add to the collection.for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
      try {
        parseBindView(element, builderMap, erasedTargetNames);
      } catch (Exception e) {
        logParsingError(element, BindView.class, e); }}... <TypeElement, bindingSet. Builder> -> TypeElement, BindingSetbindThe annotation exists in BindingSet and is then returned to process () as a file 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(); TypeElementtype = 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; }Copy the code

This code is actually much longer than that, so I’m just going to use the BindView annotation, but everything else is similar. Have deleted the other code is too long, otherwise, we can see the code above through the env. GetElementsAnnotatedWith (BindView. Class) to find an element of the @ BindView then traversal cycle, And then what he did was he organized the elements a little bit using a method called parseBindView that mapped all the comments in an Acitvity. So let’s see what we did

private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement = (TypeElement) {Set<TypeElement> erasedTargetNames) { element.getEnclosingElement(); . // get @bindView (r.i.D.test) get the int id for this id = element.getannotation (bindView.class).value(); // Get the BindingSet corresponding to this flag, Bindingset. Builder Builder = buildermap. get(enclosingElement);if(builder ! = null) { String existingBindingName = builder.findExistingBindingName(getId(id)); // If you find that the id is already stored, directlyreturn
      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{/ / found buildSet map does not save the id that we added him builder = getOrCreateBindingBuilder (builderMap enclosingElement); } String name = element.getSimpleName().toString(); TypeNametype = TypeName.get(elementType);
    boolean required = isFieldRequired(element);

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

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

There’s a lot of explanation there, so I’m just going to give you a general idea of what this method does, first of all, we get the builderMap that’s been passed in, This map corresponds to key = TypeElement (equivalent to an identifier of the current Act) value = bindingSet.Builder (which stores all the annotations in the Act), Then we’ll look it up in the map based on the element we passed in and see if that element’s corresponding BuildSet contains that ID. If it does, we’ll return it, If not, get the corresponding BuildSet of this Element and add this ID to it (via Builder.addField).

So that’s some of the things that BindView does. So here’s a summary of what’s up there

  • Each TypeElement is equivalent to one (Activity, fragment, dialog)
  • Each BindingSet stores all the annotated information in the TypeElement

Now that we’re done, let’s look at the code generation and get the xxx_ViewBinding file for the BindSet generation

binding.brewJava(sdk).writeTo(filer)Copy the code
  JavaFile brewJava(int sdk) {
    returnJavaFile. Builder (bindingClassName. PackageName (), createType (SDK)) / / just as its name implies the meaning of adding comments. AddFileComment ("Generated code from Butter Knife. Do not modify!")
        .build();
  }Copy the code
  public void writeTo(Filer filer) throws IOException {
    String fileName = packageName.isEmpty()
        ? typeSpec.name
        : packageName + "." + typeSpec.name;
    List<Element> originatingElements = typeSpec.originatingElements; JavaFileObject filerSourceFile = filer.createSourceFile(fileName, originatingElements.toArray(new Element[originatingElements.size()])); try (Writer writer = filerSourceFile.openWriter()) { writeTo(writer); } catch (Exception e) { try { filerSourceFile.delete(); } catch (Exception ignored) { } throw e; }}Copy the code

This code is also quite a lot of I will not go in to explain, here is the use of Javapoet code to write, interested students can have a look at the multi-art skills not pressure body.

ending

My god, I feel so confused after finishing this article, but I hope this article will improve your knowledge and not waste your time (after all, it took hours to write).